diff --git a/crates/metrics/metrics-derive/src/expand.rs b/crates/metrics/metrics-derive/src/expand.rs index 140eaf075..d816f9e77 100644 --- a/crates/metrics/metrics-derive/src/expand.rs +++ b/crates/metrics/metrics-derive/src/expand.rs @@ -28,7 +28,7 @@ pub(crate) fn derive(node: &DeriveInput) -> Result { }; let register_and_describe = match &metrics_attr.scope { MetricsScope::Static(scope) => { - let (defaults, describes): (Vec<_>, Vec<_>) = metric_fields + let (defaults, labeled_defaults, describes): (Vec<_>, Vec<_>, Vec<_>) = metric_fields .iter() .map(|metric| { let field_name = &metric.field.ident; @@ -41,6 +41,9 @@ pub(crate) fn derive(node: &DeriveInput) -> Result { quote! { #field_name: #registrar(#metric_name), }, + quote! { + #field_name: #registrar(#metric_name, labels.clone()), + }, quote! { #describe(#metric_name, #description); }, @@ -48,7 +51,12 @@ pub(crate) fn derive(node: &DeriveInput) -> Result { }) .collect::>>()? .into_iter() - .unzip(); + .fold((vec![], vec![], vec![]), |mut acc, x| { + acc.0.push(x.0); + acc.1.push(x.1); + acc.2.push(x.2); + acc + }); quote! { impl Default for #ty { @@ -60,6 +68,13 @@ pub(crate) fn derive(node: &DeriveInput) -> Result { } impl #ty { + /// Create new instance of metrics with provided labels. + #vis fn new_with_labels(labels: impl metrics::IntoLabels + Clone) -> Self { + Self { + #(#labeled_defaults)* + } + } + #describe_doc #vis fn describe() { #(#describes)* @@ -68,7 +83,7 @@ pub(crate) fn derive(node: &DeriveInput) -> Result { } } MetricsScope::Dynamic => { - let (defaults, describes): (Vec<_>, Vec<_>) = metric_fields + let (defaults, labeled_defaults, describes): (Vec<_>, Vec<_>, Vec<_>) = metric_fields .iter() .map(|metric| { let name = metric.name(); @@ -86,6 +101,9 @@ pub(crate) fn derive(node: &DeriveInput) -> Result { quote! { #field_name: #registrar(#metric_name), }, + quote! { + #field_name: #registrar(#metric_name, labels.clone()), + }, quote! { #describe(#metric_name, #description); }, @@ -93,7 +111,12 @@ pub(crate) fn derive(node: &DeriveInput) -> Result { }) .collect::>>()? .into_iter() - .unzip(); + .fold((vec![], vec![], vec![]), |mut acc, x| { + acc.0.push(x.0); + acc.1.push(x.1); + acc.2.push(x.2); + acc + }); quote! { impl #ty { @@ -104,6 +127,13 @@ pub(crate) fn derive(node: &DeriveInput) -> Result { } } + /// Create new instance of metrics with provided labels. + #vis fn new_with_labels(scope: &str, labels: impl metrics::IntoLabels + Clone) -> Self { + Self { + #(#labeled_defaults)* + } + } + #describe_doc #vis fn describe(scope: &str) { #(#describes)* diff --git a/crates/metrics/metrics-derive/tests/metrics.rs b/crates/metrics/metrics-derive/tests/metrics.rs index b03ee2883..6b1f46547 100644 --- a/crates/metrics/metrics-derive/tests/metrics.rs +++ b/crates/metrics/metrics-derive/tests/metrics.rs @@ -1,5 +1,5 @@ use metrics::{ - set_recorder, Counter, Gauge, Histogram, Key, KeyName, Recorder, SharedString, Unit, + set_recorder, Counter, Gauge, Histogram, Key, KeyName, Label, Recorder, SharedString, Unit, }; use once_cell::sync::Lazy; use reth_metrics_derive::Metrics; @@ -22,8 +22,74 @@ struct CustomMetrics { histo: Histogram, } +#[allow(dead_code)] +#[derive(Metrics)] +#[metrics(dynamic = true)] +struct DynamicScopeMetrics { + /// A gauge with doc comment description. + gauge: Gauge, + #[metric(rename = "second_gauge", describe = "A gauge with metric attribute description.")] + gauge2: Gauge, + /// Some doc comment + #[metric(describe = "Metric attribute description will be preferred over doc comment.")] + counter: Counter, + /// A renamed histogram. + #[metric(rename = "histogram")] + histo: Histogram, +} + static RECORDER: Lazy = Lazy::new(TestRecorder::new); +fn test_describe(scope: &str) { + assert_eq!(RECORDER.metrics_len(), 4); + + let gauge = RECORDER.get_metric(&format!("{scope}_gauge")); + assert!(gauge.is_some()); + assert_eq!( + gauge.unwrap(), + TestMetric { + ty: TestMetricTy::Gauge, + description: Some("A gauge with doc comment description.".to_owned()), + labels: None, + } + ); + + let second_gauge = RECORDER.get_metric(&format!("{scope}_second_gauge")); + assert!(second_gauge.is_some()); + assert_eq!( + second_gauge.unwrap(), + TestMetric { + ty: TestMetricTy::Gauge, + description: Some("A gauge with metric attribute description.".to_owned()), + labels: None, + } + ); + + let counter = RECORDER.get_metric(&format!("{scope}_counter")); + assert!(counter.is_some()); + assert_eq!( + counter.unwrap(), + TestMetric { + ty: TestMetricTy::Counter, + description: Some( + "Metric attribute description will be preferred over doc comment.".to_owned() + ), + labels: None, + } + ); + + let histogram = RECORDER.get_metric(&format!("{scope}_histogram")); + assert!(histogram.is_some()); + assert_eq!( + histogram.unwrap(), + TestMetric { + ty: TestMetricTy::Histogram, + description: Some("A renamed histogram.".to_owned()), + labels: None, + } + ); +} + #[test] #[serial] fn describe_metrics() { @@ -31,51 +97,55 @@ fn describe_metrics() { CustomMetrics::describe(); + test_describe("metrics_custom"); + + RECORDER.clear(); +} + +#[test] +#[serial] +fn describe_dynamic_metrics() { + let _ = set_recorder(&*RECORDER as &dyn Recorder); // ignore error + + let scope = "local_scope"; + + DynamicScopeMetrics::describe(scope); + + test_describe(scope); + + RECORDER.clear(); +} + +fn test_register(scope: &str) { assert_eq!(RECORDER.metrics_len(), 4); - let gauge = RECORDER.get_metric("metrics_custom_gauge"); + let gauge = RECORDER.get_metric(&format!("{scope}_gauge")); assert!(gauge.is_some()); assert_eq!( gauge.unwrap(), - TestMetric { - ty: TestMetricTy::Gauge, - description: Some("A gauge with doc comment description.".to_owned()) - } + TestMetric { ty: TestMetricTy::Gauge, description: None, labels: None } ); - let second_gauge = RECORDER.get_metric("metrics_custom_second_gauge"); + let second_gauge = RECORDER.get_metric(&format!("{scope}_second_gauge")); assert!(second_gauge.is_some()); assert_eq!( second_gauge.unwrap(), - TestMetric { - ty: TestMetricTy::Gauge, - description: Some("A gauge with metric attribute description.".to_owned()) - } + TestMetric { ty: TestMetricTy::Gauge, description: None, labels: None } ); - let counter = RECORDER.get_metric("metrics_custom_counter"); + let counter = RECORDER.get_metric(&format!("{scope}_counter")); assert!(counter.is_some()); assert_eq!( counter.unwrap(), - TestMetric { - ty: TestMetricTy::Counter, - description: Some( - "Metric attribute description will be preferred over doc comment.".to_owned() - ) - } + TestMetric { ty: TestMetricTy::Counter, description: None, labels: None } ); - let histogram = RECORDER.get_metric("metrics_custom_histogram"); + let histogram = RECORDER.get_metric(&format!("{scope}_histogram")); assert!(histogram.is_some()); assert_eq!( histogram.unwrap(), - TestMetric { - ty: TestMetricTy::Histogram, - description: Some("A renamed histogram.".to_owned()) - } + TestMetric { ty: TestMetricTy::Histogram, description: None, labels: None } ); - - RECORDER.clear(); } #[test] @@ -85,23 +155,75 @@ fn register_metrics() { let _metrics = CustomMetrics::default(); - assert_eq!(RECORDER.metrics_len(), 4); + test_register("metrics_custom"); - let gauge = RECORDER.get_metric("metrics_custom_gauge"); + RECORDER.clear(); +} + +#[test] +#[serial] +fn register_dynamic_metrics() { + let _ = set_recorder(&*RECORDER as &dyn Recorder); // ignore error + + let scope = "local_scope"; + + let _metrics = DynamicScopeMetrics::new(scope); + + test_register(scope); + + RECORDER.clear(); +} + +fn test_labels(scope: &str) { + let test_labels = vec![Label::new("key", "value")]; + + let gauge = RECORDER.get_metric(&format!("{scope}_gauge")); assert!(gauge.is_some()); - assert_eq!(gauge.unwrap(), TestMetric { ty: TestMetricTy::Gauge, description: None }); + let labels = gauge.unwrap().labels; + assert!(labels.is_some()); + assert_eq!(labels.unwrap(), test_labels.clone(),); - let second_gauge = RECORDER.get_metric("metrics_custom_second_gauge"); + let second_gauge = RECORDER.get_metric(&format!("{scope}_second_gauge")); assert!(second_gauge.is_some()); - assert_eq!(second_gauge.unwrap(), TestMetric { ty: TestMetricTy::Gauge, description: None }); + let labels = second_gauge.unwrap().labels; + assert!(labels.is_some()); + assert_eq!(labels.unwrap(), test_labels.clone(),); - let counter = RECORDER.get_metric("metrics_custom_counter"); + let counter = RECORDER.get_metric(&format!("{scope}_counter")); assert!(counter.is_some()); - assert_eq!(counter.unwrap(), TestMetric { ty: TestMetricTy::Counter, description: None }); + let labels = counter.unwrap().labels; + assert!(labels.is_some()); + assert_eq!(labels.unwrap(), test_labels.clone(),); - let histogram = RECORDER.get_metric("metrics_custom_histogram"); + let histogram = RECORDER.get_metric(&format!("{scope}_histogram")); assert!(histogram.is_some()); - assert_eq!(histogram.unwrap(), TestMetric { ty: TestMetricTy::Histogram, description: None }); + let labels = histogram.unwrap().labels; + assert!(labels.is_some()); + assert_eq!(labels.unwrap(), test_labels,); +} + +#[test] +#[serial] +fn label_metrics() { + let _ = set_recorder(&*RECORDER as &dyn Recorder); // ignore error + + let _metrics = CustomMetrics::new_with_labels(&[("key", "value")]); + + test_labels("metrics_custom"); + + RECORDER.clear(); +} + +#[test] +#[serial] +fn dynamic_label_metrics() { + let _ = set_recorder(&*RECORDER as &dyn Recorder); // ignore error + + let scope = "local_scope"; + + let _metrics = DynamicScopeMetrics::new_with_labels(scope, &[("key", "value")]); + + test_labels(scope); RECORDER.clear(); } @@ -122,6 +244,7 @@ enum TestMetricTy { struct TestMetric { ty: TestMetricTy, description: Option, + labels: Option>, } impl TestRecorder { @@ -137,11 +260,17 @@ impl TestRecorder { self.metrics.lock().expect("failed to lock metrics").get(key).cloned() } - fn record_metric(&self, key: &str, ty: TestMetricTy, description: Option) { + fn record_metric( + &self, + key: &str, + ty: TestMetricTy, + description: Option, + labels: Option>, + ) { self.metrics .lock() .expect("failed to lock metrics") - .insert(key.to_owned(), TestMetric { ty, description }); + .insert(key.to_owned(), TestMetric { ty, description, labels }); } fn clear(&self) { @@ -151,29 +280,45 @@ impl TestRecorder { impl Recorder for TestRecorder { fn describe_counter(&self, key: KeyName, _unit: Option, description: SharedString) { - self.record_metric(key.as_str(), TestMetricTy::Counter, Some(description.into_owned())) + self.record_metric( + key.as_str(), + TestMetricTy::Counter, + Some(description.into_owned()), + None, + ) } fn describe_gauge(&self, key: KeyName, _unit: Option, description: SharedString) { - self.record_metric(key.as_str(), TestMetricTy::Gauge, Some(description.into_owned())) + self.record_metric(key.as_str(), TestMetricTy::Gauge, Some(description.into_owned()), None) } fn describe_histogram(&self, key: KeyName, _unit: Option, description: SharedString) { - self.record_metric(key.as_str(), TestMetricTy::Histogram, Some(description.into_owned())) + self.record_metric( + key.as_str(), + TestMetricTy::Histogram, + Some(description.into_owned()), + None, + ) } fn register_counter(&self, key: &Key) -> Counter { - self.record_metric(key.name(), TestMetricTy::Counter, None); + let labels_vec: Vec