From 437b81f79f771b7489da3e17a186657e8bd91da8 Mon Sep 17 00:00:00 2001 From: Luca Cominardi Date: Tue, 19 May 2026 15:40:05 +0200 Subject: [PATCH 1/6] feat(metrics): add configurable system prefix to counter, gauge, and histogram macros Replace the `__key_name!` macro with a `const fn __sep()` + `concatcp!` approach so that both string literals and const paths (like `SYSTEM`) work uniformly as the system prefix. Each metric macro now accepts an optional `system:` argument. When omitted, the system defaults to `$crate::SYSTEM` ("quickwit"). All existing call sites continue to work without changes. Co-authored-by: Cursor --- .../quickwit-metrics/examples/http_service.rs | 37 +++++++++++++++++++ quickwit/quickwit-metrics/src/counter.rs | 22 ++++++++++- quickwit/quickwit-metrics/src/gauge.rs | 22 ++++++++++- quickwit/quickwit-metrics/src/histogram.rs | 28 +++++++++++--- quickwit/quickwit-metrics/src/inner.rs | 31 ++++++++-------- quickwit/quickwit-metrics/src/lib.rs | 6 +-- 6 files changed, 117 insertions(+), 29 deletions(-) diff --git a/quickwit/quickwit-metrics/examples/http_service.rs b/quickwit/quickwit-metrics/examples/http_service.rs index 5c7d76f29bf..60492e54dc5 100644 --- a/quickwit/quickwit-metrics/examples/http_service.rs +++ b/quickwit/quickwit-metrics/examples/http_service.rs @@ -67,6 +67,34 @@ static HTTP_ACTIVE_CONNECTIONS_BY_REGION: LazyGauge = lazy_gauge!( "region" => "us-east-1", ); +// ─── Custom system prefix ─── +// +// Override the default system prefix ("quickwit") with a custom value. +// This produces metric names like "myapp_db_queries_total" instead of +// "quickwit_db_queries_total". + +static DB_QUERIES_TOTAL: LazyCounter = lazy_counter!( + name: "queries_total", + description: "Total number of database queries", + system: "myapp", + subsystem: "db", +); + +static DB_QUERY_DURATION: LazyHistogram = lazy_histogram!( + name: "query_duration_seconds", + description: "Time spent executing database queries", + system: "myapp", + subsystem: "db", + buckets: vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0], +); + +static DB_CONNECTIONS: LazyGauge = lazy_gauge!( + name: "connections", + description: "Number of active database connections", + system: "myapp", + subsystem: "db", +); + // ─── LabelNames examples ─── const ROUTE_LABEL_NAMES: LabelNames<2> = label_names!("method", "path"); @@ -183,6 +211,15 @@ fn main() { println!(" set quickwit_http_requests_total absolute = 1,000,000"); println!(); + println!("=== Custom system prefix ==="); + DB_QUERIES_TOTAL.inc(); + println!(" myapp_db_queries_total = {}", DB_QUERIES_TOTAL.get()); + DB_QUERY_DURATION.observe(0.042); + println!(" myapp_db_query_duration_seconds observed 0.042"); + DB_CONNECTIONS.set(5.0); + println!(" myapp_db_connections = {}", DB_CONNECTIONS.get()); + println!(); + println!("Prometheus scrape endpoint: http://127.0.0.1:9000/metrics"); println!("Press Ctrl+C to stop."); std::thread::park(); diff --git a/quickwit/quickwit-metrics/src/counter.rs b/quickwit/quickwit-metrics/src/counter.rs index bb241fa8c78..2ee4e0b570b 100644 --- a/quickwit/quickwit-metrics/src/counter.rs +++ b/quickwit/quickwit-metrics/src/counter.rs @@ -260,17 +260,19 @@ impl CounterFn for Counter { /// ``` #[macro_export] macro_rules! counter { - // Base declaration: all-static name, labels, and key — zero allocations. + // Base declaration with explicit system and subsystem prefix - zero allocations. ( name: $name:literal, description: $description:literal, - subsystem: $subsystem:tt + system: $system:expr, + subsystem: $subsystem:expr $(, $label:literal => $value:literal)* $(,)? ) => {{ $crate::__key_info_metadata!( kind: $crate::MetricKind::Counter, name: $name, description: $description, + system: $system, subsystem: $subsystem $(, $label => $value)* ); @@ -282,6 +284,22 @@ macro_rules! counter { ) }}; + // Base declaration with subsystem only — system defaults to SYSTEM. + ( + name: $name:literal, + description: $description:literal, + subsystem: $subsystem:expr + $(, $label:literal => $value:literal)* $(,)? + ) => {{ + $crate::counter!( + name: $name, + description: $description, + system: $crate::SYSTEM, + subsystem: $subsystem + $(, $label => $value)* + ) + }}; + // Parent extension with inline dynamic labels. // Derives a child counter by inheriting the parent's name and labels, // appending new (possibly dynamic) key => value pairs. diff --git a/quickwit/quickwit-metrics/src/gauge.rs b/quickwit/quickwit-metrics/src/gauge.rs index bf98d1ca4df..d475b8716ca 100644 --- a/quickwit/quickwit-metrics/src/gauge.rs +++ b/quickwit/quickwit-metrics/src/gauge.rs @@ -331,17 +331,19 @@ impl Drop for GaugeGuard { /// ``` #[macro_export] macro_rules! gauge { - // Base declaration: all-static name, labels, and key — zero allocations. + // Base declaration with explicit system and subsystem prefix - zero allocations. ( name: $name:literal, description: $description:literal, - subsystem: $subsystem:tt + system: $system:expr, + subsystem: $subsystem:expr $(, $label:literal => $value:literal)* $(,)? ) => {{ $crate::__key_info_metadata!( kind: $crate::MetricKind::Gauge, name: $name, description: $description, + system: $system, subsystem: $subsystem $(, $label => $value)* ); @@ -353,6 +355,22 @@ macro_rules! gauge { ) }}; + // Base declaration with subsystem only — system defaults to SYSTEM. + ( + name: $name:literal, + description: $description:literal, + subsystem: $subsystem:expr + $(, $label:literal => $value:literal)* $(,)? + ) => {{ + $crate::gauge!( + name: $name, + description: $description, + system: $crate::SYSTEM, + subsystem: $subsystem + $(, $label => $value)* + ) + }}; + // Parent extension with inline dynamic labels. // Derives a child gauge by inheriting the parent's name and labels, // appending new (possibly dynamic) key => value pairs. diff --git a/quickwit/quickwit-metrics/src/histogram.rs b/quickwit/quickwit-metrics/src/histogram.rs index 95ab8380d1f..3962a4e52bb 100644 --- a/quickwit/quickwit-metrics/src/histogram.rs +++ b/quickwit/quickwit-metrics/src/histogram.rs @@ -261,30 +261,28 @@ impl Drop for HistogramTimer { /// ``` #[macro_export] macro_rules! histogram { - // Base declaration: all-static name, labels, and key — zero allocations. + // Base declaration with explicit system and subsystem prefix - zero allocations. ( name: $name:literal, description: $description:literal, - subsystem: $subsystem:tt, + system: $system:expr, + subsystem: $subsystem:expr, buckets: $buckets:expr $(, $label:literal => $value:literal)* $(,)? ) => {{ - // Expand compile-time statics: KEY_NAME, INFO, KEY, LABELS, METADATA. $crate::__key_info_metadata!( kind: $crate::MetricKind::Histogram, name: $name, description: $description, + system: $system, subsystem: $subsystem $(, $label => $value)* ); - // Link histogram bucket configuration to the metric info and register it - // with the inventory so the recorder can configure bucket boundaries. static HISTOGRAM_CONFIG: $crate::HistogramConfig = $crate::HistogramConfig { info: &INFO, buckets_fn: || $buckets, }; $crate::__inventory::submit!(HISTOGRAM_CONFIG); - // Thread-local cache + global DashMap registration. $crate::__metric_declaration!( metric_type: $crate::Histogram, register_fn: $crate::__histogram_get_or_register, @@ -293,6 +291,24 @@ macro_rules! histogram { ) }}; + // Base declaration with subsystem only — system defaults to SYSTEM. + ( + name: $name:literal, + description: $description:literal, + subsystem: $subsystem:expr, + buckets: $buckets:expr + $(, $label:literal => $value:literal)* $(,)? + ) => {{ + $crate::histogram!( + name: $name, + description: $description, + system: $crate::SYSTEM, + subsystem: $subsystem, + buckets: $buckets + $(, $label => $value)* + ) + }}; + // Parent extension with inline dynamic labels. // Derives a child histogram by inheriting the parent's name and labels, // appending new (possibly dynamic) key => value pairs. diff --git a/quickwit/quickwit-metrics/src/inner.rs b/quickwit/quickwit-metrics/src/inner.rs index 6a218297dc5..fbfbfde77b7 100644 --- a/quickwit/quickwit-metrics/src/inner.rs +++ b/quickwit/quickwit-metrics/src/inner.rs @@ -24,20 +24,7 @@ use std::hash::{Hash, Hasher}; pub use const_format::concatcp as __concatcp; use rustc_hash::FxHasher; -// ─── Helper macros ─── - -/// Builds the fully-qualified metric key name at compile time: -/// `"{SYSTEM}_{name}"` or `"{SYSTEM}_{subsystem}_{name}"`. -#[doc(hidden)] -#[macro_export] -macro_rules! __key_name { - ("", $name:literal) => { - $crate::__concatcp!($crate::SYSTEM, "_", $name) - }; - ($subsystem:literal, $name:literal) => { - $crate::__concatcp!($crate::SYSTEM, "_", $subsystem, "_", $name) - }; -} +// ─── Helper functions ─── /// Counts the number of token-tree arguments at compile time. #[doc(hidden)] @@ -65,6 +52,15 @@ macro_rules! __metadata { }; } +/// Returns `"_"` for non-empty strings, `""` for empty ones. +/// +/// Used inside `concatcp!` to conditionally insert separators when +/// composing metric key names at compile time. +#[doc(hidden)] +pub const fn __sep(s: &str) -> &str { + if s.is_empty() { "" } else { "_" } +} + /// Declares the compile-time statics that every metric declaration arm needs: /// `KEY_NAME`, `INFO`, `KEY`, `LABELS`, and `METADATA`. /// Also registers `INFO` with the `inventory` crate for runtime discovery. @@ -75,10 +71,15 @@ macro_rules! __key_info_metadata { kind: $kind:expr, name: $name:literal, description: $description:literal, + system: $system:expr, subsystem: $subsystem:tt $(, $label:literal => $value:literal)* $(,)? ) => { - const KEY_NAME: &str = $crate::__key_name!($subsystem, $name); + const KEY_NAME: &str = $crate::__concatcp!( + $system, $crate::__sep($system), + $subsystem, $crate::__sep($subsystem), + $name + ); static METADATA: $crate::__metrics::Metadata<'static> = $crate::__metadata!($subsystem); static INFO: $crate::MetricInfo = $crate::MetricInfo { key_name: KEY_NAME, diff --git a/quickwit/quickwit-metrics/src/lib.rs b/quickwit/quickwit-metrics/src/lib.rs index e7bbe4235bd..967444b217f 100644 --- a/quickwit/quickwit-metrics/src/lib.rs +++ b/quickwit/quickwit-metrics/src/lib.rs @@ -264,9 +264,7 @@ /// System-level prefix prepended to every metric name. /// /// Every metric declared via [`counter!`], [`gauge!`], or [`histogram!`] -/// has its name composed at compile time as `{SYSTEM}_{subsystem}_{name}`. -/// -/// Hardcoded for now — making this configurable is tracked separately. +/// has its name composed at compile time as `{system}_{subsystem}_{name}`. pub const SYSTEM: &str = "quickwit"; // ─── Metric modules ─── @@ -289,7 +287,7 @@ pub use gauge::__gauge_get_or_register; #[doc(hidden)] pub use histogram::__histogram_get_or_register; #[doc(hidden)] -pub use inner::{__concatcp, __key_hash}; +pub use inner::{__concatcp, __key_hash, __sep}; // Re-exports of `metrics` and `inventory` used inside macro expansions. #[doc(hidden)] From 209e5bbfc12479b2ccda578cc4b4949f46a0be23 Mon Sep 17 00:00:00 2001 From: Luca Cominardi Date: Tue, 19 May 2026 16:06:54 +0200 Subject: [PATCH 2/6] test(metrics): add key name coverage for system and subsystem cases Add explicit counter, gauge, and histogram tests for custom, default, empty, and const system/subsystem combinations, and assert emitted values for each case. Co-authored-by: Cursor --- quickwit/quickwit-metrics/tests/counter.rs | 110 +++++++++++++++++- quickwit/quickwit-metrics/tests/gauge.rs | 110 +++++++++++++++++- quickwit/quickwit-metrics/tests/histogram.rs | 116 ++++++++++++++++++- 3 files changed, 333 insertions(+), 3 deletions(-) diff --git a/quickwit/quickwit-metrics/tests/counter.rs b/quickwit/quickwit-metrics/tests/counter.rs index 91dc4f92d88..6d31fadf683 100644 --- a/quickwit/quickwit-metrics/tests/counter.rs +++ b/quickwit/quickwit-metrics/tests/counter.rs @@ -17,7 +17,7 @@ mod common; use common::with_recorder; use metrics::with_local_recorder; use metrics_util::debugging::{DebugValue, DebuggingRecorder}; -use quickwit_metrics::{Counter, counter, label_names, label_values, labels}; +use quickwit_metrics::{Counter, SYSTEM, counter, label_names, label_values, labels}; #[test] fn base_increments() { @@ -346,3 +346,111 @@ fn local_counter_clone_is_equal() { a.inc(); assert_eq!(b.get(), 1); } + +#[test] +fn custom_system_key_name() { + let entries = with_recorder(|| { + let c = counter!( + name: "requests_total", + description: "total requests", + system: "myapp", + subsystem: "http", + ); + c.inc(); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "myapp_http_requests_total"); + assert_eq!(*value, DebugValue::Counter(1)); +} + +#[test] +fn default_system_key_name() { + let entries = with_recorder(|| { + let c = counter!( + name: "requests_total", + description: "total requests", + subsystem: "http", + ); + c.inc(); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, &format!("{SYSTEM}_http_requests_total")); + assert_eq!(*value, DebugValue::Counter(1)); +} + +#[test] +fn empty_system_key_name() { + let entries = with_recorder(|| { + let c = counter!( + name: "requests_total", + description: "total requests", + system: "", + subsystem: "http", + ); + c.inc(); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "http_requests_total"); + assert_eq!(*value, DebugValue::Counter(1)); +} + +#[test] +fn empty_subsystem_key_name() { + let entries = with_recorder(|| { + let c = counter!( + name: "requests_total", + description: "total requests", + system: "myapp", + subsystem: "", + ); + c.inc(); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "myapp_requests_total"); + assert_eq!(*value, DebugValue::Counter(1)); +} + +#[test] +fn empty_system_and_subsystem_key_name() { + let entries = with_recorder(|| { + let c = counter!( + name: "requests_total", + description: "total requests", + system: "", + subsystem: "", + ); + c.inc(); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "requests_total"); + assert_eq!(*value, DebugValue::Counter(1)); +} + +#[test] +fn const_system_key_name() { + const MY_SYSTEM: &str = "custom"; + let entries = with_recorder(|| { + let c = counter!( + name: "ops_total", + description: "total ops", + system: MY_SYSTEM, + subsystem: "db", + ); + c.inc(); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "custom_db_ops_total"); + assert_eq!(*value, DebugValue::Counter(1)); +} diff --git a/quickwit/quickwit-metrics/tests/gauge.rs b/quickwit/quickwit-metrics/tests/gauge.rs index 3d49ab50f97..3774ade956e 100644 --- a/quickwit/quickwit-metrics/tests/gauge.rs +++ b/quickwit/quickwit-metrics/tests/gauge.rs @@ -17,7 +17,7 @@ mod common; use common::with_recorder; use metrics::with_local_recorder; use metrics_util::debugging::{DebugValue, DebuggingRecorder}; -use quickwit_metrics::{Gauge, GaugeGuard, gauge, label_names, label_values, labels}; +use quickwit_metrics::{Gauge, GaugeGuard, SYSTEM, gauge, label_names, label_values, labels}; #[test] fn set() { @@ -348,3 +348,111 @@ fn local_gauge_clone_is_equal() { a.set(7.0); assert_eq!(b.get(), 7.0); } + +#[test] +fn custom_system_key_name() { + let entries = with_recorder(|| { + let g = gauge!( + name: "connections", + description: "active connections", + system: "myapp", + subsystem: "db", + ); + g.set(5.0); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "myapp_db_connections"); + assert_eq!(*value, DebugValue::Gauge(5.0.into())); +} + +#[test] +fn default_system_key_name() { + let entries = with_recorder(|| { + let g = gauge!( + name: "connections", + description: "active connections", + subsystem: "db", + ); + g.set(1.0); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, &format!("{SYSTEM}_db_connections")); + assert_eq!(*value, DebugValue::Gauge(1.0.into())); +} + +#[test] +fn empty_system_key_name() { + let entries = with_recorder(|| { + let g = gauge!( + name: "connections", + description: "active connections", + system: "", + subsystem: "db", + ); + g.set(1.0); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "db_connections"); + assert_eq!(*value, DebugValue::Gauge(1.0.into())); +} + +#[test] +fn empty_subsystem_key_name() { + let entries = with_recorder(|| { + let g = gauge!( + name: "connections", + description: "active connections", + system: "myapp", + subsystem: "", + ); + g.set(1.0); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "myapp_connections"); + assert_eq!(*value, DebugValue::Gauge(1.0.into())); +} + +#[test] +fn empty_system_and_subsystem_key_name() { + let entries = with_recorder(|| { + let g = gauge!( + name: "connections", + description: "active connections", + system: "", + subsystem: "", + ); + g.set(1.0); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "connections"); + assert_eq!(*value, DebugValue::Gauge(1.0.into())); +} + +#[test] +fn const_system_key_name() { + const MY_SYSTEM: &str = "custom"; + let entries = with_recorder(|| { + let g = gauge!( + name: "pool_size", + description: "pool size", + system: MY_SYSTEM, + subsystem: "db", + ); + g.set(8.0); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "custom_db_pool_size"); + assert_eq!(*value, DebugValue::Gauge(8.0.into())); +} diff --git a/quickwit/quickwit-metrics/tests/histogram.rs b/quickwit/quickwit-metrics/tests/histogram.rs index 42a906d5a67..0fbc2cf1829 100644 --- a/quickwit/quickwit-metrics/tests/histogram.rs +++ b/quickwit/quickwit-metrics/tests/histogram.rs @@ -17,7 +17,7 @@ mod common; use common::with_recorder; use metrics::with_local_recorder; use metrics_util::debugging::{DebugValue, DebuggingRecorder}; -use quickwit_metrics::{HistogramTimer, histogram, label_names, label_values, labels}; +use quickwit_metrics::{HistogramTimer, SYSTEM, histogram, label_names, label_values, labels}; #[test] fn base_records_value() { @@ -234,3 +234,117 @@ fn timer_observe_duration_records_once() { other => panic!("expected Histogram, got {other:?}"), } } + +#[test] +fn custom_system_key_name() { + let entries = with_recorder(|| { + let h = histogram!( + name: "duration_seconds", + description: "request duration", + system: "myapp", + subsystem: "http", + buckets: vec![0.01, 0.1, 1.0] + ); + h.observe(0.05); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "myapp_http_duration_seconds"); + assert_eq!(value, &DebugValue::Histogram(vec![0.05.into()])); +} + +#[test] +fn default_system_key_name() { + let entries = with_recorder(|| { + let h = histogram!( + name: "duration_seconds", + description: "request duration", + subsystem: "http", + buckets: vec![0.01, 0.1, 1.0] + ); + h.observe(0.05); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, &format!("{SYSTEM}_http_duration_seconds")); + assert_eq!(value, &DebugValue::Histogram(vec![0.05.into()])); +} + +#[test] +fn empty_system_key_name() { + let entries = with_recorder(|| { + let h = histogram!( + name: "duration_seconds", + description: "request duration", + system: "", + subsystem: "http", + buckets: vec![0.01, 0.1, 1.0] + ); + h.observe(0.05); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "http_duration_seconds"); + assert_eq!(value, &DebugValue::Histogram(vec![0.05.into()])); +} + +#[test] +fn empty_subsystem_key_name() { + let entries = with_recorder(|| { + let h = histogram!( + name: "duration_seconds", + description: "request duration", + system: "myapp", + subsystem: "", + buckets: vec![0.01, 0.1, 1.0] + ); + h.observe(0.05); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "myapp_duration_seconds"); + assert_eq!(value, &DebugValue::Histogram(vec![0.05.into()])); +} + +#[test] +fn empty_system_and_subsystem_key_name() { + let entries = with_recorder(|| { + let h = histogram!( + name: "duration_seconds", + description: "request duration", + system: "", + subsystem: "", + buckets: vec![0.01, 0.1, 1.0] + ); + h.observe(0.05); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "duration_seconds"); + assert_eq!(value, &DebugValue::Histogram(vec![0.05.into()])); +} + +#[test] +fn const_system_key_name() { + const MY_SYSTEM: &str = "custom"; + let entries = with_recorder(|| { + let h = histogram!( + name: "latency_ms", + description: "latency", + system: MY_SYSTEM, + subsystem: "rpc", + buckets: vec![1.0, 10.0, 100.0] + ); + h.observe(5.0); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "custom_rpc_latency_ms"); + assert_eq!(value, &DebugValue::Histogram(vec![5.0.into()])); +} From d4663512b139e4c3274f7ce664e50d8b5a153f82 Mon Sep 17 00:00:00 2001 From: Luca Cominardi Date: Tue, 19 May 2026 17:01:25 +0200 Subject: [PATCH 3/6] feat(metrics): add configurable metric name separator Extend counter, gauge, and histogram base declaration macros to accept a configurable separator while defaulting to SEPARATOR, and add key-name tests for custom separators. Co-authored-by: Cursor --- quickwit/quickwit-metrics/src/counter.rs | 26 +++++++++++++++++--- quickwit/quickwit-metrics/src/gauge.rs | 26 +++++++++++++++++--- quickwit/quickwit-metrics/src/histogram.rs | 26 ++++++++++++++++++-- quickwit/quickwit-metrics/src/inner.rs | 13 +++++----- quickwit/quickwit-metrics/src/lib.rs | 1 + quickwit/quickwit-metrics/tests/counter.rs | 19 ++++++++++++++ quickwit/quickwit-metrics/tests/gauge.rs | 19 ++++++++++++++ quickwit/quickwit-metrics/tests/histogram.rs | 20 +++++++++++++++ 8 files changed, 136 insertions(+), 14 deletions(-) diff --git a/quickwit/quickwit-metrics/src/counter.rs b/quickwit/quickwit-metrics/src/counter.rs index 2ee4e0b570b..1615a5148d2 100644 --- a/quickwit/quickwit-metrics/src/counter.rs +++ b/quickwit/quickwit-metrics/src/counter.rs @@ -260,12 +260,13 @@ impl CounterFn for Counter { /// ``` #[macro_export] macro_rules! counter { - // Base declaration with explicit system and subsystem prefix - zero allocations. + // Base declaration with explicit separator, system, and subsystem prefix - zero allocations. ( name: $name:literal, description: $description:literal, system: $system:expr, - subsystem: $subsystem:expr + subsystem: $subsystem:expr, + separator: $separator:expr $(, $label:literal => $value:literal)* $(,)? ) => {{ $crate::__key_info_metadata!( @@ -273,7 +274,8 @@ macro_rules! counter { name: $name, description: $description, system: $system, - subsystem: $subsystem + subsystem: $subsystem, + separator: $separator $(, $label => $value)* ); $crate::__metric_declaration!( @@ -284,6 +286,24 @@ macro_rules! counter { ) }}; + // Base declaration with explicit system and subsystem prefix - zero allocations. + ( + name: $name:literal, + description: $description:literal, + system: $system:expr, + subsystem: $subsystem:expr + $(, $label:literal => $value:literal)* $(,)? + ) => {{ + $crate::counter!( + name: $name, + description: $description, + system: $system, + subsystem: $subsystem, + separator: $crate::SEPARATOR + $(, $label => $value)* + ) + }}; + // Base declaration with subsystem only — system defaults to SYSTEM. ( name: $name:literal, diff --git a/quickwit/quickwit-metrics/src/gauge.rs b/quickwit/quickwit-metrics/src/gauge.rs index d475b8716ca..7b671914cc1 100644 --- a/quickwit/quickwit-metrics/src/gauge.rs +++ b/quickwit/quickwit-metrics/src/gauge.rs @@ -331,12 +331,13 @@ impl Drop for GaugeGuard { /// ``` #[macro_export] macro_rules! gauge { - // Base declaration with explicit system and subsystem prefix - zero allocations. + // Base declaration with explicit separator, system, and subsystem prefix - zero allocations. ( name: $name:literal, description: $description:literal, system: $system:expr, - subsystem: $subsystem:expr + subsystem: $subsystem:expr, + separator: $separator:expr $(, $label:literal => $value:literal)* $(,)? ) => {{ $crate::__key_info_metadata!( @@ -344,7 +345,8 @@ macro_rules! gauge { name: $name, description: $description, system: $system, - subsystem: $subsystem + subsystem: $subsystem, + separator: $separator $(, $label => $value)* ); $crate::__metric_declaration!( @@ -355,6 +357,24 @@ macro_rules! gauge { ) }}; + // Base declaration with explicit system and subsystem prefix - zero allocations. + ( + name: $name:literal, + description: $description:literal, + system: $system:expr, + subsystem: $subsystem:expr + $(, $label:literal => $value:literal)* $(,)? + ) => {{ + $crate::gauge!( + name: $name, + description: $description, + system: $system, + subsystem: $subsystem, + separator: $crate::SEPARATOR + $(, $label => $value)* + ) + }}; + // Base declaration with subsystem only — system defaults to SYSTEM. ( name: $name:literal, diff --git a/quickwit/quickwit-metrics/src/histogram.rs b/quickwit/quickwit-metrics/src/histogram.rs index 3962a4e52bb..624910433eb 100644 --- a/quickwit/quickwit-metrics/src/histogram.rs +++ b/quickwit/quickwit-metrics/src/histogram.rs @@ -261,12 +261,13 @@ impl Drop for HistogramTimer { /// ``` #[macro_export] macro_rules! histogram { - // Base declaration with explicit system and subsystem prefix - zero allocations. + // Base declaration with explicit separator, system, and subsystem prefix - zero allocations. ( name: $name:literal, description: $description:literal, system: $system:expr, subsystem: $subsystem:expr, + separator: $separator:expr, buckets: $buckets:expr $(, $label:literal => $value:literal)* $(,)? ) => {{ @@ -275,7 +276,8 @@ macro_rules! histogram { name: $name, description: $description, system: $system, - subsystem: $subsystem + subsystem: $subsystem, + separator: $separator $(, $label => $value)* ); static HISTOGRAM_CONFIG: $crate::HistogramConfig = $crate::HistogramConfig { @@ -291,6 +293,26 @@ macro_rules! histogram { ) }}; + // Base declaration with explicit system and subsystem prefix - zero allocations. + ( + name: $name:literal, + description: $description:literal, + system: $system:expr, + subsystem: $subsystem:expr, + buckets: $buckets:expr + $(, $label:literal => $value:literal)* $(,)? + ) => {{ + $crate::histogram!( + name: $name, + description: $description, + system: $system, + subsystem: $subsystem, + separator: $crate::SEPARATOR, + buckets: $buckets + $(, $label => $value)* + ) + }}; + // Base declaration with subsystem only — system defaults to SYSTEM. ( name: $name:literal, diff --git a/quickwit/quickwit-metrics/src/inner.rs b/quickwit/quickwit-metrics/src/inner.rs index fbfbfde77b7..69f0858a971 100644 --- a/quickwit/quickwit-metrics/src/inner.rs +++ b/quickwit/quickwit-metrics/src/inner.rs @@ -52,13 +52,13 @@ macro_rules! __metadata { }; } -/// Returns `"_"` for non-empty strings, `""` for empty ones. +/// Returns `separator` for non-empty strings, `""` for empty ones. /// /// Used inside `concatcp!` to conditionally insert separators when /// composing metric key names at compile time. #[doc(hidden)] -pub const fn __sep(s: &str) -> &str { - if s.is_empty() { "" } else { "_" } +pub const fn __sep<'a>(s: &'a str, separator: &'a str) -> &'a str { + if s.is_empty() { "" } else { separator } } /// Declares the compile-time statics that every metric declaration arm needs: @@ -72,12 +72,13 @@ macro_rules! __key_info_metadata { name: $name:literal, description: $description:literal, system: $system:expr, - subsystem: $subsystem:tt + subsystem: $subsystem:tt, + separator: $separator:expr $(, $label:literal => $value:literal)* $(,)? ) => { const KEY_NAME: &str = $crate::__concatcp!( - $system, $crate::__sep($system), - $subsystem, $crate::__sep($subsystem), + $system, $crate::__sep($system, $separator), + $subsystem, $crate::__sep($subsystem, $separator), $name ); static METADATA: $crate::__metrics::Metadata<'static> = $crate::__metadata!($subsystem); diff --git a/quickwit/quickwit-metrics/src/lib.rs b/quickwit/quickwit-metrics/src/lib.rs index 967444b217f..bf1992ef2fd 100644 --- a/quickwit/quickwit-metrics/src/lib.rs +++ b/quickwit/quickwit-metrics/src/lib.rs @@ -266,6 +266,7 @@ /// Every metric declared via [`counter!`], [`gauge!`], or [`histogram!`] /// has its name composed at compile time as `{system}_{subsystem}_{name}`. pub const SYSTEM: &str = "quickwit"; +pub const SEPARATOR: &str = "_"; // ─── Metric modules ─── mod counter; diff --git a/quickwit/quickwit-metrics/tests/counter.rs b/quickwit/quickwit-metrics/tests/counter.rs index 6d31fadf683..09bf07bef4f 100644 --- a/quickwit/quickwit-metrics/tests/counter.rs +++ b/quickwit/quickwit-metrics/tests/counter.rs @@ -454,3 +454,22 @@ fn const_system_key_name() { assert_eq!(name, "custom_db_ops_total"); assert_eq!(*value, DebugValue::Counter(1)); } + +#[test] +fn custom_separator_key_name() { + let entries = with_recorder(|| { + let c = counter!( + name: "requests_total", + description: "total requests", + system: "myapp", + subsystem: "http", + separator: ".", + ); + c.inc(); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "myapp.http.requests_total"); + assert_eq!(*value, DebugValue::Counter(1)); +} diff --git a/quickwit/quickwit-metrics/tests/gauge.rs b/quickwit/quickwit-metrics/tests/gauge.rs index 3774ade956e..0ff44af4681 100644 --- a/quickwit/quickwit-metrics/tests/gauge.rs +++ b/quickwit/quickwit-metrics/tests/gauge.rs @@ -456,3 +456,22 @@ fn const_system_key_name() { assert_eq!(name, "custom_db_pool_size"); assert_eq!(*value, DebugValue::Gauge(8.0.into())); } + +#[test] +fn custom_separator_key_name() { + let entries = with_recorder(|| { + let g = gauge!( + name: "connections", + description: "active connections", + system: "myapp", + subsystem: "db", + separator: ".", + ); + g.set(5.0); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "myapp.db.connections"); + assert_eq!(*value, DebugValue::Gauge(5.0.into())); +} diff --git a/quickwit/quickwit-metrics/tests/histogram.rs b/quickwit/quickwit-metrics/tests/histogram.rs index 0fbc2cf1829..35854d9683d 100644 --- a/quickwit/quickwit-metrics/tests/histogram.rs +++ b/quickwit/quickwit-metrics/tests/histogram.rs @@ -348,3 +348,23 @@ fn const_system_key_name() { assert_eq!(name, "custom_rpc_latency_ms"); assert_eq!(value, &DebugValue::Histogram(vec![5.0.into()])); } + +#[test] +fn custom_separator_key_name() { + let entries = with_recorder(|| { + let h = histogram!( + name: "duration_seconds", + description: "request duration", + system: "myapp", + subsystem: "http", + separator: ".", + buckets: vec![0.01, 0.1, 1.0] + ); + h.observe(0.05); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "myapp.http.duration_seconds"); + assert_eq!(value, &DebugValue::Histogram(vec![0.05.into()])); +} From acbcae0e5146d93242332b5fa72f4ec801cb2ac9 Mon Sep 17 00:00:00 2001 From: Luca Cominardi Date: Tue, 19 May 2026 17:11:33 +0200 Subject: [PATCH 4/6] docs(metrics): add custom separator example in http_service Show how to override the default metric name separator with "." in the example service and print the resulting dotted metric key. Co-authored-by: Cursor --- .../quickwit-metrics/examples/http_service.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/quickwit/quickwit-metrics/examples/http_service.rs b/quickwit/quickwit-metrics/examples/http_service.rs index 60492e54dc5..f1abcf5ee5d 100644 --- a/quickwit/quickwit-metrics/examples/http_service.rs +++ b/quickwit/quickwit-metrics/examples/http_service.rs @@ -95,6 +95,19 @@ static DB_CONNECTIONS: LazyGauge = lazy_gauge!( subsystem: "db", ); +// ─── Custom separator ─── +// +// Override the default "_" separator with ".". +// This produces metric names like "myapp.http.requests_total". + +static HTTP_REQUESTS_DOTTED: LazyCounter = lazy_counter!( + name: "requests_total", + description: "Total HTTP requests with dotted metric name", + system: "myapp", + subsystem: "http", + separator: ".", +); + // ─── LabelNames examples ─── const ROUTE_LABEL_NAMES: LabelNames<2> = label_names!("method", "path"); @@ -220,6 +233,11 @@ fn main() { println!(" myapp_db_connections = {}", DB_CONNECTIONS.get()); println!(); + println!("=== Custom separator ==="); + HTTP_REQUESTS_DOTTED.inc_by(3); + println!(" myapp.http.requests_total = {}", HTTP_REQUESTS_DOTTED.get()); + println!(); + println!("Prometheus scrape endpoint: http://127.0.0.1:9000/metrics"); println!("Press Ctrl+C to stop."); std::thread::park(); From 0d03e9fc8e49556c41f0230f9cb6d3f69937cb37 Mon Sep 17 00:00:00 2001 From: Luca Cominardi Date: Tue, 19 May 2026 17:47:20 +0200 Subject: [PATCH 5/6] fix: cargo fmt --- quickwit/quickwit-metrics/examples/http_service.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quickwit/quickwit-metrics/examples/http_service.rs b/quickwit/quickwit-metrics/examples/http_service.rs index f1abcf5ee5d..d7dc5bf4480 100644 --- a/quickwit/quickwit-metrics/examples/http_service.rs +++ b/quickwit/quickwit-metrics/examples/http_service.rs @@ -235,7 +235,10 @@ fn main() { println!("=== Custom separator ==="); HTTP_REQUESTS_DOTTED.inc_by(3); - println!(" myapp.http.requests_total = {}", HTTP_REQUESTS_DOTTED.get()); + println!( + " myapp.http.requests_total = {}", + HTTP_REQUESTS_DOTTED.get() + ); println!(); println!("Prometheus scrape endpoint: http://127.0.0.1:9000/metrics"); From c075973b8acf9e41a00c9a6f84b71218b921ee10 Mon Sep 17 00:00:00 2001 From: Luca Cominardi Date: Wed, 20 May 2026 09:47:36 +0200 Subject: [PATCH 6/6] fix(metrics): allow separator override with default system Add subsystem+separator macro arms for counter, gauge, and histogram so callers can customize separators without explicitly overriding system, and cover this with key-name tests. Co-authored-by: Cursor --- quickwit/quickwit-metrics/src/counter.rs | 18 ++++++++++++++++++ quickwit/quickwit-metrics/src/gauge.rs | 18 ++++++++++++++++++ quickwit/quickwit-metrics/src/histogram.rs | 20 ++++++++++++++++++++ quickwit/quickwit-metrics/tests/counter.rs | 18 ++++++++++++++++++ quickwit/quickwit-metrics/tests/gauge.rs | 18 ++++++++++++++++++ quickwit/quickwit-metrics/tests/histogram.rs | 19 +++++++++++++++++++ 6 files changed, 111 insertions(+) diff --git a/quickwit/quickwit-metrics/src/counter.rs b/quickwit/quickwit-metrics/src/counter.rs index 1615a5148d2..afe02e337e3 100644 --- a/quickwit/quickwit-metrics/src/counter.rs +++ b/quickwit/quickwit-metrics/src/counter.rs @@ -304,6 +304,24 @@ macro_rules! counter { ) }}; + // Base declaration with subsystem only — system defaults to SYSTEM. + ( + name: $name:literal, + description: $description:literal, + subsystem: $subsystem:expr, + separator: $separator:expr + $(, $label:literal => $value:literal)* $(,)? + ) => {{ + $crate::counter!( + name: $name, + description: $description, + system: $crate::SYSTEM, + subsystem: $subsystem, + separator: $separator + $(, $label => $value)* + ) + }}; + // Base declaration with subsystem only — system defaults to SYSTEM. ( name: $name:literal, diff --git a/quickwit/quickwit-metrics/src/gauge.rs b/quickwit/quickwit-metrics/src/gauge.rs index 7b671914cc1..127bdda9995 100644 --- a/quickwit/quickwit-metrics/src/gauge.rs +++ b/quickwit/quickwit-metrics/src/gauge.rs @@ -375,6 +375,24 @@ macro_rules! gauge { ) }}; + // Base declaration with subsystem only — system defaults to SYSTEM. + ( + name: $name:literal, + description: $description:literal, + subsystem: $subsystem:expr, + separator: $separator:expr + $(, $label:literal => $value:literal)* $(,)? + ) => {{ + $crate::gauge!( + name: $name, + description: $description, + system: $crate::SYSTEM, + subsystem: $subsystem, + separator: $separator + $(, $label => $value)* + ) + }}; + // Base declaration with subsystem only — system defaults to SYSTEM. ( name: $name:literal, diff --git a/quickwit/quickwit-metrics/src/histogram.rs b/quickwit/quickwit-metrics/src/histogram.rs index 624910433eb..a3b2bad6757 100644 --- a/quickwit/quickwit-metrics/src/histogram.rs +++ b/quickwit/quickwit-metrics/src/histogram.rs @@ -313,6 +313,26 @@ macro_rules! histogram { ) }}; + // Base declaration with subsystem only — system defaults to SYSTEM. + ( + name: $name:literal, + description: $description:literal, + subsystem: $subsystem:expr, + separator: $separator:expr, + buckets: $buckets:expr + $(, $label:literal => $value:literal)* $(,)? + ) => {{ + $crate::histogram!( + name: $name, + description: $description, + system: $crate::SYSTEM, + subsystem: $subsystem, + separator: $separator, + buckets: $buckets + $(, $label => $value)* + ) + }}; + // Base declaration with subsystem only — system defaults to SYSTEM. ( name: $name:literal, diff --git a/quickwit/quickwit-metrics/tests/counter.rs b/quickwit/quickwit-metrics/tests/counter.rs index 09bf07bef4f..7f2d82903d0 100644 --- a/quickwit/quickwit-metrics/tests/counter.rs +++ b/quickwit/quickwit-metrics/tests/counter.rs @@ -473,3 +473,21 @@ fn custom_separator_key_name() { assert_eq!(name, "myapp.http.requests_total"); assert_eq!(*value, DebugValue::Counter(1)); } + +#[test] +fn default_system_custom_separator_key_name() { + let entries = with_recorder(|| { + let c = counter!( + name: "requests_total", + description: "total requests", + subsystem: "http", + separator: ".", + ); + c.inc(); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "quickwit.http.requests_total"); + assert_eq!(*value, DebugValue::Counter(1)); +} diff --git a/quickwit/quickwit-metrics/tests/gauge.rs b/quickwit/quickwit-metrics/tests/gauge.rs index 0ff44af4681..c7ec5e34f4f 100644 --- a/quickwit/quickwit-metrics/tests/gauge.rs +++ b/quickwit/quickwit-metrics/tests/gauge.rs @@ -475,3 +475,21 @@ fn custom_separator_key_name() { assert_eq!(name, "myapp.db.connections"); assert_eq!(*value, DebugValue::Gauge(5.0.into())); } + +#[test] +fn default_system_custom_separator_key_name() { + let entries = with_recorder(|| { + let g = gauge!( + name: "connections", + description: "active connections", + subsystem: "db", + separator: ".", + ); + g.set(5.0); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "quickwit.db.connections"); + assert_eq!(*value, DebugValue::Gauge(5.0.into())); +} diff --git a/quickwit/quickwit-metrics/tests/histogram.rs b/quickwit/quickwit-metrics/tests/histogram.rs index 35854d9683d..94c2230d09e 100644 --- a/quickwit/quickwit-metrics/tests/histogram.rs +++ b/quickwit/quickwit-metrics/tests/histogram.rs @@ -368,3 +368,22 @@ fn custom_separator_key_name() { assert_eq!(name, "myapp.http.duration_seconds"); assert_eq!(value, &DebugValue::Histogram(vec![0.05.into()])); } + +#[test] +fn default_system_custom_separator_key_name() { + let entries = with_recorder(|| { + let h = histogram!( + name: "duration_seconds", + description: "request duration", + subsystem: "http", + separator: ".", + buckets: vec![0.01, 0.1, 1.0] + ); + h.observe(0.05); + }); + + assert_eq!(entries.len(), 1); + let (name, _, value) = &entries[0]; + assert_eq!(name, "quickwit.http.duration_seconds"); + assert_eq!(value, &DebugValue::Histogram(vec![0.05.into()])); +}