From 1ee530c200a2b73a1f81afcce440f89189d4aa7d Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Wed, 20 Sep 2023 15:42:01 +0300 Subject: [PATCH] feat: Client improvements (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ Improves the client crate in various ways: - Supports `Gauge`s for more types, e.g. `usize` / `isize`. - Supports raw identifiers (e.g., `r#type`) as label names. - Supports encoding C-style enums as label values and transforming their variant names like in `serde`. ## Why ❔ Makes the crate easier to use (with the guiding example being the main Era repo). --- crates/vise-macros/src/labels.rs | 276 +++++++++++++++++++++++++++-- crates/vise/src/collector.rs | 11 +- crates/vise/src/constructor.rs | 3 +- crates/vise/src/descriptors.rs | 2 +- crates/vise/src/lib.rs | 256 ++++++--------------------- crates/vise/src/metrics.rs | 64 +++++++ crates/vise/src/registry.rs | 7 + crates/vise/src/tests.rs | 288 +++++++++++++++++++++++++++++++ crates/vise/src/traits.rs | 217 ++++++++++++++++++----- crates/vise/src/wrappers.rs | 154 ++--------------- 10 files changed, 865 insertions(+), 413 deletions(-) create mode 100644 crates/vise/src/metrics.rs create mode 100644 crates/vise/src/tests.rs diff --git a/crates/vise-macros/src/labels.rs b/crates/vise-macros/src/labels.rs index 2c64105..4c5e24b 100644 --- a/crates/vise-macros/src/labels.rs +++ b/crates/vise-macros/src/labels.rs @@ -1,16 +1,76 @@ //! Derivation of `EncodeLabelValue` and `EncodeLabelSet` traits. -use std::fmt; +use std::{collections::HashSet, fmt}; use proc_macro::TokenStream; use quote::quote; -use syn::{Attribute, Data, DeriveInput, Field, Ident, LitStr, Path, PathArguments, Type}; +use syn::{Attribute, Data, DeriveInput, Field, Fields, Ident, LitStr, Path, PathArguments, Type}; use crate::utils::{metrics_attribute, validate_name, ParseAttribute}; +#[derive(Debug, Clone, Copy)] +#[allow(clippy::enum_variant_names)] +enum RenameRule { + LowerCase, + UpperCase, + CamelCase, + SnakeCase, + ScreamingSnakeCase, + KebabCase, + ScreamingKebabCase, +} + +impl RenameRule { + fn parse(s: &str) -> Result { + Ok(match s { + "lowercase" => Self::LowerCase, + "UPPERCASE" => Self::UpperCase, + "camelCase" => Self::CamelCase, + "snake_case" => Self::SnakeCase, + "SCREAMING_SNAKE_CASE" => Self::ScreamingSnakeCase, + "kebab-case" => Self::KebabCase, + "SCREAMING-KEBAB-CASE" => Self::ScreamingKebabCase, + _ => { + return Err( + "Invalid case specified; should be one of: lowercase, UPPERCASE, camelCase, \ + snake_case, SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE", + ) + } + }) + } + + fn transform(self, ident: &str) -> String { + debug_assert!(ident.is_ascii()); // Should be checked previously + let (spacing_char, scream) = match self { + Self::LowerCase => return ident.to_ascii_lowercase(), + Self::UpperCase => return ident.to_ascii_uppercase(), + Self::CamelCase => return ident[..1].to_ascii_lowercase() + &ident[1..], + // ^ Since `ident` is an ASCII string, indexing is safe + Self::SnakeCase => ('_', false), + Self::ScreamingSnakeCase => ('_', true), + Self::KebabCase => ('-', false), + Self::ScreamingKebabCase => ('-', true), + }; + + let mut output = String::with_capacity(ident.len()); + for (i, ch) in ident.char_indices() { + if i > 0 && ch.is_ascii_uppercase() { + output.push(spacing_char); + } + output.push(if scream { + ch.to_ascii_uppercase() + } else { + ch.to_ascii_lowercase() + }); + } + output + } +} + #[derive(Default)] struct EncodeLabelAttrs { cr: Option, + rename_all: Option, format: Option, label: Option, } @@ -20,8 +80,9 @@ impl fmt::Debug for EncodeLabelAttrs { formatter .debug_struct("EncodeLabelAttrs") .field("cr", &self.cr.as_ref().map(|_| "_")) - .field("format", &self.format.as_ref().map(|_| "_")) - .field("label", &self.label.as_ref().map(|_| "_")) + .field("rename_all", &self.rename_all) + .field("format", &self.format.as_ref().map(LitStr::value)) + .field("label", &self.label.as_ref().map(LitStr::value)) .finish() } } @@ -33,6 +94,12 @@ impl ParseAttribute for EncodeLabelAttrs { if meta.path.is_ident("crate") { attrs.cr = Some(meta.value()?.parse()?); Ok(()) + } else if meta.path.is_ident("rename_all") { + let case_str: LitStr = meta.value()?.parse()?; + let case = RenameRule::parse(&case_str.value()) + .map_err(|message| syn::Error::new(case_str.span(), message))?; + attrs.rename_all = Some(case); + Ok(()) } else if meta.path.is_ident("format") { attrs.format = Some(meta.value()?.parse()?); Ok(()) @@ -49,20 +116,115 @@ impl ParseAttribute for EncodeLabelAttrs { } } +#[derive(Debug)] +struct EnumVariant { + ident: Ident, + label_value: String, +} + +impl EnumVariant { + fn encode(&self) -> proc_macro2::TokenStream { + let ident = &self.ident; + let label_value = &self.label_value; + quote!(Self::#ident => #label_value) + } +} + +#[derive(Default)] +struct EnumVariantAttrs { + name: Option, +} + +impl fmt::Debug for EnumVariantAttrs { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("EnumVariantAttrs") + .field("name", &self.name.as_ref().map(LitStr::value)) + .finish() + } +} + +impl ParseAttribute for EnumVariantAttrs { + fn parse(raw: &Attribute) -> syn::Result { + let mut attrs = Self::default(); + raw.parse_nested_meta(|meta| { + if meta.path.is_ident("name") { + attrs.name = Some(meta.value()?.parse()?); + Ok(()) + } else { + Err(meta.error("unsupported attribute")) + } + })?; + Ok(attrs) + } +} + #[derive(Debug)] struct EncodeLabelValueImpl { attrs: EncodeLabelAttrs, name: Ident, + enum_variants: Option>, } impl EncodeLabelValueImpl { fn new(raw: &DeriveInput) -> syn::Result { + let attrs: EncodeLabelAttrs = metrics_attribute(&raw.attrs)?; + if let Some(format) = &attrs.format { + if attrs.rename_all.is_some() { + let message = "`rename_all` and `format` attributes cannot be specified together"; + return Err(syn::Error::new(format.span(), message)); + } + } + + let enum_variants = attrs + .rename_all + .map(|case| Self::extract_enum_variants(raw, case)) + .transpose()?; + Ok(Self { - attrs: metrics_attribute(&raw.attrs)?, + attrs, + enum_variants, name: raw.ident.clone(), }) } + fn extract_enum_variants(raw: &DeriveInput, case: RenameRule) -> syn::Result> { + let Data::Enum(data) = &raw.data else { + let message = "`rename_all` attribute can only be placed on enums"; + return Err(syn::Error::new_spanned(raw, message)); + }; + + let mut unique_label_values = HashSet::with_capacity(data.variants.len()); + let variants = data.variants.iter().map(|variant| { + if !matches!(variant.fields, Fields::Unit) { + let message = "To use `rename_all` attribute, all enum variants must be plain \ + (have no fields)"; + return Err(syn::Error::new_spanned(variant, message)); + } + let ident_str = variant.ident.to_string(); + if !ident_str.is_ascii() { + let message = "Variant name must consist of ASCII chars"; + return Err(syn::Error::new(variant.ident.span(), message)); + } + let attrs: EnumVariantAttrs = metrics_attribute(&variant.attrs)?; + let label_value = if let Some(name_override) = attrs.name { + name_override.value() + } else { + case.transform(&ident_str) + }; + if !unique_label_values.insert(label_value.clone()) { + let message = format!("Label value `{label_value}` is redefined"); + return Err(syn::Error::new_spanned(variant, message)); + } + + Ok(EnumVariant { + ident: variant.ident.clone(), + label_value, + }) + }); + variants.collect() + } + fn impl_value(&self) -> proc_macro2::TokenStream { let cr = if let Some(cr) = &self.attrs.cr { quote!(#cr) @@ -72,12 +234,27 @@ impl EncodeLabelValueImpl { let name = &self.name; let encoding = quote!(#cr::_reexports::encoding); - let format_lit; - let format = if let Some(format) = &self.attrs.format { - format + let encode_impl = if let Some(enum_variants) = &self.enum_variants { + let variant_hands = enum_variants.iter().map(EnumVariant::encode); + quote! { + use core::fmt::Write as _; + core::write!(encoder, "{}", match self { + #(#variant_hands,)* + }) + } } else { - format_lit = LitStr::new("{}", name.span()); - &format_lit + let format_lit; + let format = if let Some(format) = &self.attrs.format { + format + } else { + format_lit = LitStr::new("{}", name.span()); + &format_lit + }; + + quote! { + use core::fmt::Write as _; + core::write!(encoder, #format, self) + } }; quote! { @@ -86,8 +263,7 @@ impl EncodeLabelValueImpl { &self, encoder: &mut #encoding::LabelValueEncoder<'_>, ) -> core::fmt::Result { - use core::fmt::Write as _; - core::write!(encoder, #format, self) + #encode_impl } } } @@ -136,14 +312,25 @@ impl LabelField { let message = "Encoded fields must be named"; syn::Error::new_spanned(raw, message) })?; - validate_name(&name.to_string()) - .map_err(|message| syn::Error::new(name.span(), message))?; - Ok(Self { + let this = Self { name, is_option: Self::detect_is_option(&raw.ty), attrs: metrics_attribute(&raw.attrs)?, - }) + }; + validate_name(&this.label_string()) + .map_err(|message| syn::Error::new(this.name.span(), message))?; + Ok(this) + } + + /// Strips the `r#` prefix from raw identifiers. + fn label_string(&self) -> String { + let label = self.name.to_string(); + if let Some(stripped) = label.strip_prefix("r#") { + stripped.to_owned() + } else { + label + } } fn detect_is_option(ty: &Type) -> bool { @@ -163,7 +350,7 @@ impl LabelField { fn encode(&self, encoding: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { let name = &self.name; - let label = LitStr::new(&self.name.to_string(), name.span()); + let label = LitStr::new(&self.label_string(), name.span()); // Skip `Option`al fields by default if they are `None`. let default_skip: Path; @@ -205,7 +392,7 @@ struct EncodeLabelSetImpl { impl EncodeLabelSetImpl { fn new(raw: &DeriveInput) -> syn::Result { - let EncodeLabelValueImpl { attrs, name } = EncodeLabelValueImpl::new(raw)?; + let EncodeLabelValueImpl { attrs, name, .. } = EncodeLabelValueImpl::new(raw)?; let fields = if attrs.label.is_some() { None @@ -286,3 +473,56 @@ pub(crate) fn impl_encode_label_set(input: TokenStream) -> TokenStream { }; trait_impl.impl_set().into() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn renaming_rules() { + let ident = "TestIdent"; + let rules_and_expected_outcomes = [ + (RenameRule::LowerCase, "testident"), + (RenameRule::UpperCase, "TESTIDENT"), + (RenameRule::CamelCase, "testIdent"), + (RenameRule::SnakeCase, "test_ident"), + (RenameRule::ScreamingSnakeCase, "TEST_IDENT"), + (RenameRule::KebabCase, "test-ident"), + (RenameRule::ScreamingKebabCase, "TEST-IDENT"), + ]; + for (rule, expected) in rules_and_expected_outcomes { + assert_eq!(rule.transform(ident), expected); + } + } + + #[test] + fn encoding_label_set() { + let input: DeriveInput = syn::parse_quote! { + struct TestLabels { + r#type: &'static str, + #[metrics(skip = str::is_empty)] + kind: &'static str, + } + }; + let label_set = EncodeLabelSetImpl::new(&input).unwrap(); + let fields = label_set.fields.as_ref().unwrap(); + assert_eq!(fields.len(), 2); + assert_eq!(fields[0].label_string(), "type"); + assert_eq!(fields[1].label_string(), "kind"); + assert!(fields[1].attrs.skip.is_some()); + } + + #[test] + fn label_value_redefinition_error() { + let input: DeriveInput = syn::parse_quote! { + #[metrics(rename_all = "snake_case")] + enum Label { + First, + #[metrics(name = "first")] + Second, + } + }; + let err = EncodeLabelValueImpl::new(&input).unwrap_err().to_string(); + assert!(err.contains("Label value `first` is redefined"), "{err}"); + } +} diff --git a/crates/vise/src/collector.rs b/crates/vise/src/collector.rs index 4bf00f7..3ec007f 100644 --- a/crates/vise/src/collector.rs +++ b/crates/vise/src/collector.rs @@ -7,7 +7,10 @@ use prometheus_client::{ use std::{borrow::Cow, error, fmt, iter}; -use crate::{registry::MetricsVisitor, Metrics}; +use crate::{ + registry::{CollectToRegistry, MetricsVisitor, Registry}, + Metrics, +}; type CollectorFn = Box M + Send + Sync>; type CollectorItem<'a> = (Cow<'a, Descriptor>, MaybeOwned<'a, Box>); @@ -91,6 +94,12 @@ impl CollectorTrait for &'static Collector { } } +impl CollectToRegistry for Collector { + fn collect_to_registry(&'static self, registry: &mut Registry) { + registry.register_collector(self); + } +} + #[cfg(test)] mod tests { use once_cell::sync::Lazy; diff --git a/crates/vise/src/constructor.rs b/crates/vise/src/constructor.rs index ff48d79..39ae5c4 100644 --- a/crates/vise/src/constructor.rs +++ b/crates/vise/src/constructor.rs @@ -6,7 +6,8 @@ use prometheus_client::{ use std::hash::Hash; use crate::{ - wrappers::{Family, Gauge, GaugeValue, Histogram, HistogramValue}, + traits::{GaugeValue, HistogramValue}, + wrappers::{Family, Gauge, Histogram}, Buckets, }; diff --git a/crates/vise/src/descriptors.rs b/crates/vise/src/descriptors.rs index 47dad59..1a90728 100644 --- a/crates/vise/src/descriptors.rs +++ b/crates/vise/src/descriptors.rs @@ -70,7 +70,7 @@ mod tests { use std::collections::HashMap; use super::*; - use crate::{tests::TestMetrics, traits::Metrics}; + use crate::{metrics::Metrics, tests::TestMetrics}; #[test] fn describing_metrics() { diff --git a/crates/vise/src/lib.rs b/crates/vise/src/lib.rs index c0c1be2..5d21504 100644 --- a/crates/vise/src/lib.rs +++ b/crates/vise/src/lib.rs @@ -139,6 +139,36 @@ pub use prometheus_client::{metrics::counter::Counter, registry::Unit}; /// /// [`EncodeLabelValue`]: trait@prometheus_client::encoding::EncodeLabelValue /// +/// ## `rename_all` +/// +/// **Type:** string; one of `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`, +/// `kebab-case`, `SCREAMING-KEBAB-CASE` +/// +/// Specifies how enum variant names should be transformed into label values. This attribute +/// can only be placed on enums in which all variants don't have fields (aka C-style enums). +/// Mutually exclusive with the `format` attribute. +/// +/// Caveats: +/// +/// - `rename_all` assumes that original variant names are in `PascalCase` (i.e., follow Rust naming conventions). +/// - `rename_all` requires original variant names to consist of ASCII chars. +/// - Each letter of capitalized acronyms (e.g., "HTTP" in `HTTPServer`) is treated as a separate word. +/// E.g., `rename_all = "snake_case"` will rename `HTTPServer` to `h_t_t_p_server`. +/// Note that [it is recommended][clippy-acronyms] to not capitalize acronyms (i.e., use `HttpServer`). +/// - No spacing is inserted before numbers or other non-letter chars. E.g., `rename_all = "snake_case"` +/// will rename `Status500` to `status500`, not to `status_500`. +/// +/// # Variant attributes +/// +/// ## `name` +/// +/// **Type:** string +/// +/// Specifies the name override for a particular enum variant when used with the `rename_all` attribute +/// described above. +/// +/// [clippy-acronyms]: https://rust-lang.github.io/rust-clippy/master/index.html#/upper_case_acronyms +/// /// # Examples /// /// ## Default format @@ -158,13 +188,27 @@ pub use prometheus_client::{metrics::counter::Counter, registry::Unit}; /// Label value using `Hex` formatting with `0` padding and `0x` prepended. /// /// ``` -/// use derive_more::LowerHex; -/// use vise::EncodeLabelValue; -/// +/// # use derive_more::LowerHex; +/// # use vise::EncodeLabelValue; /// #[derive(Debug, LowerHex, EncodeLabelValue)] /// #[metrics(format = "0x{:02x}")] /// struct ResponseType(u8); /// ``` +/// +/// ## Using `rename_all` on C-style enum +/// +/// ``` +/// # use derive_more::LowerHex; +/// # use vise::EncodeLabelValue; +/// #[derive(Debug, EncodeLabelValue)] +/// #[metrics(rename_all = "snake_case")] +/// enum Database { +/// Postgres, // renamed to "postgres" +/// MySql, // renamed to "my_sql" +/// #[metrics(name = "rocksdb")] // explicitly overrides the name +/// RocksDB, +/// } +/// ``` pub use vise_macros::EncodeLabelValue; /// Derives the [`EncodeLabelSet`] trait for a type, which encodes a set of metric labels. @@ -305,215 +349,21 @@ mod buckets; mod collector; mod constructor; pub mod descriptors; +mod metrics; mod registry; -mod traits; +#[cfg(test)] +mod tests; +pub mod traits; mod wrappers; pub use crate::{ buckets::Buckets, collector::Collector, constructor::{ConstructMetric, DefaultConstructor}, - registry::{MetricsVisitor, Registry, METRICS_REGISTRATIONS}, - traits::{CollectToRegistry, Global, Metrics}, + metrics::{Global, Metrics}, + registry::{CollectToRegistry, MetricsVisitor, Registry, METRICS_REGISTRATIONS}, wrappers::{Family, Gauge, Histogram, LatencyObserver}, }; #[cfg(doctest)] doc_comment::doctest!("../README.md"); - -#[cfg(test)] -#[allow(clippy::float_cmp)] -mod tests { - use assert_matches::assert_matches; - use derive_more::Display; - - use std::time::Duration; - - use super::*; - - #[derive(Debug, Display, Clone, PartialEq, Eq, Hash, EncodeLabelValue, EncodeLabelSet)] - #[metrics(crate = crate, label = "method")] - struct Method(&'static str); - - impl From<&'static str> for Method { - fn from(s: &'static str) -> Self { - Self(s) - } - } - - #[derive(Debug, Metrics)] - #[metrics(crate = crate, prefix = "test")] - pub(crate) struct TestMetrics { - /// Test counter. - counter: Counter, - #[metrics(unit = Unit::Bytes)] - gauge: Gauge, - /// Test family of gauges. - family_of_gauges: Family>, - /// Histogram with inline bucket specification. - #[metrics(buckets = &[0.001, 0.002, 0.005, 0.01, 0.1])] - histogram: Histogram, - /// A family of histograms with a multiline description. - /// Note that we use a type alias to properly propagate bucket configuration. - #[metrics(unit = Unit::Seconds, buckets = Buckets::LATENCIES)] - family_of_histograms: Family>, - /// Family of histograms with a reference bucket specification. - #[metrics(buckets = Buckets::ZERO_TO_ONE)] - histograms_with_buckets: Family>, - } - - #[register] - #[metrics(crate = crate)] - static TEST_METRICS: Global = Global::new(); - - #[test] - fn metrics_registration() { - let registry = Registry::collect(); - let descriptors = registry.descriptors(); - - assert!(descriptors.metric_count() > 5); - assert_eq!(descriptors.groups().len(), 2); - // ^ We have `TestMetrics` above and `TestMetrics` in the `collectors` module - assert!(descriptors - .groups() - .any(|group| group.module_path.contains("collector"))); - - let counter_descriptor = descriptors.metric("test_counter").unwrap(); - assert_eq!(counter_descriptor.metric.help, "Test counter"); - - // Test metric registered via a `Collector` in the corresponding module tests. - let dynamic_gauge_descriptor = descriptors.metric("dynamic_gauge_bytes").unwrap(); - assert_matches!(dynamic_gauge_descriptor.metric.unit, Some(Unit::Bytes)); - } - - #[test] - fn testing_metrics() { - let test_metrics = &*TEST_METRICS; - let mut registry = Registry::empty(); - registry.register_metrics(test_metrics); - - test_metrics.counter.inc(); - assert_eq!(test_metrics.counter.get(), 1); - // ^ Counters and gauges can be easily tested - - test_metrics.gauge.set(42); - assert_eq!(test_metrics.gauge.get(), 42); - - test_metrics.family_of_gauges[&"call".into()].set(0.4); - test_metrics.family_of_gauges[&"send_transaction".into()].set(0.5); - - assert!(test_metrics.family_of_gauges.contains(&"call".into())); - let gauge = test_metrics.family_of_gauges.get(&"call".into()).unwrap(); - assert_eq!(gauge.get(), 0.4); - assert!(!test_metrics.family_of_gauges.contains(&"test".into())); - - let gauges_in_family = test_metrics.family_of_gauges.to_entries(); - assert_eq!(gauges_in_family.len(), 2); - assert_eq!(gauges_in_family[&"call".into()].get(), 0.4); - assert_eq!(gauges_in_family[&"send_transaction".into()].get(), 0.5); - - test_metrics.histogram.observe(Duration::from_millis(1)); - test_metrics.histogram.observe(Duration::from_micros(1_500)); - test_metrics.histogram.observe(Duration::from_millis(3)); - test_metrics.histogram.observe(Duration::from_millis(4)); - test_metrics.family_of_histograms[&"call".into()].observe(Duration::from_millis(20)); - - test_metrics.histograms_with_buckets[&"call".into()].observe(Duration::from_millis(350)); - test_metrics.histograms_with_buckets[&"send_transaction".into()] - .observe(Duration::from_millis(620)); - - let mut buffer = String::new(); - registry.encode_to_text(&mut buffer).unwrap(); - let lines: Vec<_> = buffer.lines().collect(); - - // `_bytes` suffix is added automatically per Prometheus naming suggestions: - // https://prometheus.io/docs/practices/naming/ - assert!(lines.contains(&"# TYPE test_gauge_bytes gauge")); - assert!(lines.contains(&"# UNIT test_gauge_bytes bytes")); - assert!(lines.contains(&"test_gauge_bytes 42")); - - // Full stop is added to the metrics description automatically. - assert!(lines.contains(&"# HELP test_family_of_gauges Test family of gauges.")); - assert!(lines.contains(&r#"test_family_of_gauges{method="call"} 0.4"#)); - assert!(lines.contains(&r#"test_family_of_gauges{method="send_transaction"} 0.5"#)); - - let histogram_lines = [ - "test_histogram_sum 0.0095", - "test_histogram_count 4", - r#"test_histogram_bucket{le="0.001"} 1"#, - r#"test_histogram_bucket{le="0.005"} 4"#, - r#"test_histogram_bucket{le="0.01"} 4"#, - ]; - for line in histogram_lines { - assert!( - lines.contains(&line), - "text output doesn't contain line `{line}`" - ); - } - - let long_description_line = - "# HELP test_family_of_histograms_seconds A family of histograms \ - with a multiline description. Note that we use a type alias to properly propagate \ - bucket configuration."; - assert!(lines.contains(&long_description_line)); - - let histogram_family_lines = [ - r#"test_histograms_with_buckets_bucket{le="0.6",method="send_transaction"} 0"#, - r#"test_histograms_with_buckets_bucket{le="0.7",method="send_transaction"} 1"#, - r#"test_histograms_with_buckets_bucket{le="0.3",method="call"} 0"#, - r#"test_histograms_with_buckets_bucket{le="0.4",method="call"} 1"#, - ]; - for line in histogram_family_lines { - assert!( - lines.contains(&line), - "text output doesn't contain line `{line}`" - ); - } - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EncodeLabelSet)] - #[metrics(crate = crate)] - struct Labels { - /// Label that is skipped when empty. - #[metrics(skip = str::is_empty)] - name: &'static str, - /// Label that is skipped when it's `None` (the default behavior). - num: Option, - } - - impl Labels { - const fn named(name: &'static str) -> Self { - Self { name, num: None } - } - - const fn num(mut self, num: u64) -> Self { - self.num = Some(num); - self - } - } - - #[derive(Debug, Metrics)] - #[metrics(crate = crate, prefix = "test")] - struct MetricsWithLabels { - /// Gauge with multiple labels. - gauges: Family>, - } - - #[test] - fn using_label_set() { - let test_metrics = MetricsWithLabels::default(); - test_metrics.gauges[&Labels::named("test")].set(1.9); - test_metrics.gauges[&Labels::named("test").num(5)].set(4.2); - test_metrics.gauges[&Labels::named("").num(3)].set(2.0); - - let mut registry = Registry::empty(); - registry.register_metrics(&test_metrics); - let mut buffer = String::new(); - registry.encode_to_text(&mut buffer).unwrap(); - let lines: Vec<_> = buffer.lines().collect(); - - assert!(lines.contains(&r#"test_gauges{num="3"} 2.0"#)); - assert!(lines.contains(&r#"test_gauges{name="test"} 1.9"#)); - assert!(lines.contains(&r#"test_gauges{name="test",num="5"} 4.2"#)); - } -} diff --git a/crates/vise/src/metrics.rs b/crates/vise/src/metrics.rs new file mode 100644 index 0000000..2316649 --- /dev/null +++ b/crates/vise/src/metrics.rs @@ -0,0 +1,64 @@ +//! Core `Metrics` trait defined by the crate. + +use once_cell::sync::Lazy; + +use std::ops; + +use crate::{ + descriptors::MetricGroupDescriptor, + registry::{CollectToRegistry, MetricsVisitor, Registry}, +}; + +/// Collection of metrics for a library or application. Should be derived using the corresponding macro. +pub trait Metrics: 'static + Send + Sync { + /// Metrics descriptor. + const DESCRIPTOR: MetricGroupDescriptor; + + #[doc(hidden)] // implementation detail + fn visit_metrics(&self, visitor: MetricsVisitor<'_>); +} + +impl Metrics for &'static M { + const DESCRIPTOR: MetricGroupDescriptor = M::DESCRIPTOR; + + fn visit_metrics(&self, visitor: MetricsVisitor<'_>) { + (**self).visit_metrics(visitor); + } +} + +impl Metrics for Option { + const DESCRIPTOR: MetricGroupDescriptor = M::DESCRIPTOR; + + fn visit_metrics(&self, visitor: MetricsVisitor<'_>) { + if let Some(metrics) = self { + metrics.visit_metrics(visitor); + } + } +} + +/// Global instance of [`Metrics`] allowing to access contained metrics from anywhere in code. +/// Should be used as a `static` item. +#[derive(Debug)] +pub struct Global(Lazy); + +impl Global { + /// Creates a new metrics instance. + pub const fn new() -> Self { + Self(Lazy::new(M::default)) + } +} + +impl ops::Deref for Global { + type Target = M; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl CollectToRegistry for Global { + fn collect_to_registry(&'static self, registry: &mut Registry) { + let metrics: &M = self; + registry.register_metrics(metrics); + } +} diff --git a/crates/vise/src/registry.rs b/crates/vise/src/registry.rs index 4783e0f..4db6a98 100644 --- a/crates/vise/src/registry.rs +++ b/crates/vise/src/registry.rs @@ -260,3 +260,10 @@ impl<'a> MetricsVisitor<'a> { } } } + +/// Collects metrics from this type to registry. This is used by the [`register`](crate::register) +/// macro to handle registration of [`Global`](crate::Global) metrics and [`Collector`]s. +pub trait CollectToRegistry: 'static + Send + Sync { + #[doc(hidden)] // implementation detail + fn collect_to_registry(&'static self, registry: &mut Registry); +} diff --git a/crates/vise/src/tests.rs b/crates/vise/src/tests.rs new file mode 100644 index 0000000..9f742b5 --- /dev/null +++ b/crates/vise/src/tests.rs @@ -0,0 +1,288 @@ +#![allow(clippy::float_cmp)] + +use assert_matches::assert_matches; +use derive_more::Display; + +use std::time::Duration; + +use super::*; + +#[derive(Debug, Display, Clone, PartialEq, Eq, Hash, EncodeLabelValue, EncodeLabelSet)] +#[metrics(crate = crate, label = "method")] +struct Method(&'static str); + +impl From<&'static str> for Method { + fn from(s: &'static str) -> Self { + Self(s) + } +} + +#[derive(Debug, Metrics)] +#[metrics(crate = crate, prefix = "test")] +pub(crate) struct TestMetrics { + /// Test counter. + counter: Counter, + #[metrics(unit = Unit::Bytes)] + gauge: Gauge, + /// Test family of gauges. + family_of_gauges: Family>, + /// Histogram with inline bucket specification. + #[metrics(buckets = &[0.001, 0.002, 0.005, 0.01, 0.1])] + histogram: Histogram, + /// A family of histograms with a multiline description. + /// Note that we use a type alias to properly propagate bucket configuration. + #[metrics(unit = Unit::Seconds, buckets = Buckets::LATENCIES)] + family_of_histograms: Family>, + /// Family of histograms with a reference bucket specification. + #[metrics(buckets = Buckets::ZERO_TO_ONE)] + histograms_with_buckets: Family>, +} + +#[register] +#[metrics(crate = crate)] +static TEST_METRICS: Global = Global::new(); + +#[test] +fn metrics_registration() { + let registry = Registry::collect(); + let descriptors = registry.descriptors(); + + assert!(descriptors.metric_count() > 5); + assert_eq!(descriptors.groups().len(), 2); + // ^ We have `TestMetrics` above and `TestMetrics` in the `collectors` module + assert!(descriptors + .groups() + .any(|group| group.module_path.contains("collector"))); + + let counter_descriptor = descriptors.metric("test_counter").unwrap(); + assert_eq!(counter_descriptor.metric.help, "Test counter"); + + // Test metric registered via a `Collector` in the corresponding module tests. + let dynamic_gauge_descriptor = descriptors.metric("dynamic_gauge_bytes").unwrap(); + assert_matches!(dynamic_gauge_descriptor.metric.unit, Some(Unit::Bytes)); +} + +#[test] +fn testing_metrics() { + let test_metrics = &*TEST_METRICS; + let mut registry = Registry::empty(); + registry.register_metrics(test_metrics); + + test_metrics.counter.inc(); + assert_eq!(test_metrics.counter.get(), 1); + // ^ Counters and gauges can be easily tested + + test_metrics.gauge.set(42); + assert_eq!(test_metrics.gauge.get(), 42); + + test_metrics.family_of_gauges[&"call".into()].set(0.4); + test_metrics.family_of_gauges[&"send_transaction".into()].set(0.5); + + assert!(test_metrics.family_of_gauges.contains(&"call".into())); + let gauge = test_metrics.family_of_gauges.get(&"call".into()).unwrap(); + assert_eq!(gauge.get(), 0.4); + assert!(!test_metrics.family_of_gauges.contains(&"test".into())); + + let gauges_in_family = test_metrics.family_of_gauges.to_entries(); + assert_eq!(gauges_in_family.len(), 2); + assert_eq!(gauges_in_family[&"call".into()].get(), 0.4); + assert_eq!(gauges_in_family[&"send_transaction".into()].get(), 0.5); + + test_metrics.histogram.observe(Duration::from_millis(1)); + test_metrics.histogram.observe(Duration::from_micros(1_500)); + test_metrics.histogram.observe(Duration::from_millis(3)); + test_metrics.histogram.observe(Duration::from_millis(4)); + test_metrics.family_of_histograms[&"call".into()].observe(Duration::from_millis(20)); + + test_metrics.histograms_with_buckets[&"call".into()].observe(Duration::from_millis(350)); + test_metrics.histograms_with_buckets[&"send_transaction".into()] + .observe(Duration::from_millis(620)); + + let mut buffer = String::new(); + registry.encode_to_text(&mut buffer).unwrap(); + let lines: Vec<_> = buffer.lines().collect(); + + // `_bytes` suffix is added automatically per Prometheus naming suggestions: + // https://prometheus.io/docs/practices/naming/ + assert!(lines.contains(&"# TYPE test_gauge_bytes gauge")); + assert!(lines.contains(&"# UNIT test_gauge_bytes bytes")); + assert!(lines.contains(&"test_gauge_bytes 42")); + + // Full stop is added to the metrics description automatically. + assert!(lines.contains(&"# HELP test_family_of_gauges Test family of gauges.")); + assert!(lines.contains(&r#"test_family_of_gauges{method="call"} 0.4"#)); + assert!(lines.contains(&r#"test_family_of_gauges{method="send_transaction"} 0.5"#)); + + let histogram_lines = [ + "test_histogram_sum 0.0095", + "test_histogram_count 4", + r#"test_histogram_bucket{le="0.001"} 1"#, + r#"test_histogram_bucket{le="0.005"} 4"#, + r#"test_histogram_bucket{le="0.01"} 4"#, + ]; + for line in histogram_lines { + assert!( + lines.contains(&line), + "text output doesn't contain line `{line}`" + ); + } + + let long_description_line = "# HELP test_family_of_histograms_seconds A family of histograms \ + with a multiline description. Note that we use a type alias to properly propagate \ + bucket configuration."; + assert!(lines.contains(&long_description_line)); + + let histogram_family_lines = [ + r#"test_histograms_with_buckets_bucket{le="0.6",method="send_transaction"} 0"#, + r#"test_histograms_with_buckets_bucket{le="0.7",method="send_transaction"} 1"#, + r#"test_histograms_with_buckets_bucket{le="0.3",method="call"} 0"#, + r#"test_histograms_with_buckets_bucket{le="0.4",method="call"} 1"#, + ]; + for line in histogram_family_lines { + assert!( + lines.contains(&line), + "text output doesn't contain line `{line}`" + ); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EncodeLabelSet)] +#[metrics(crate = crate)] +struct Labels { + /// Label that is skipped when empty. + #[metrics(skip = str::is_empty)] + name: &'static str, + /// Label that is skipped when it's `None` (the default behavior). + num: Option, +} + +impl Labels { + const fn named(name: &'static str) -> Self { + Self { name, num: None } + } + + const fn num(mut self, num: u64) -> Self { + self.num = Some(num); + self + } +} + +#[derive(Debug, Metrics)] +#[metrics(crate = crate, prefix = "test")] +struct MetricsWithLabels { + /// Gauge with multiple labels. + gauges: Family>, +} + +#[test] +fn using_label_set() { + let test_metrics = MetricsWithLabels::default(); + test_metrics.gauges[&Labels::named("test")].set(1.9); + test_metrics.gauges[&Labels::named("test").num(5)].set(4.2); + test_metrics.gauges[&Labels::named("").num(3)].set(2.0); + + let mut registry = Registry::empty(); + registry.register_metrics(&test_metrics); + let mut buffer = String::new(); + registry.encode_to_text(&mut buffer).unwrap(); + let lines: Vec<_> = buffer.lines().collect(); + + assert!(lines.contains(&r#"test_gauges{num="3"} 2.0"#)); + assert!(lines.contains(&r#"test_gauges{name="test"} 1.9"#)); + assert!(lines.contains(&r#"test_gauges{name="test",num="5"} 4.2"#)); +} + +#[test] +fn label_with_raw_ident() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EncodeLabelSet)] + #[metrics(crate = crate)] + struct LabelWithRawIdent { + r#type: &'static str, + } + + impl From<&'static str> for LabelWithRawIdent { + fn from(r#type: &'static str) -> Self { + Self { r#type } + } + } + + #[derive(Debug, Metrics)] + #[metrics(crate = crate, prefix = "test")] + struct MetricsWithLabels { + counters: Family, + } + + let test_metrics = MetricsWithLabels::default(); + test_metrics.counters[&"first".into()].inc(); + + let mut registry = Registry::empty(); + registry.register_metrics(&test_metrics); + let mut buffer = String::new(); + registry.encode_to_text(&mut buffer).unwrap(); + let lines: Vec<_> = buffer.lines().collect(); + + assert!( + lines.contains(&r#"test_counters_total{type="first"} 1"#), + "{lines:#?}" + ); +} + +#[test] +fn renamed_labels() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EncodeLabelValue, EncodeLabelSet)] + #[metrics(crate = crate, rename_all = "snake_case", label = "kind")] + enum KindLabel { + First, + #[metrics(name = "2nd")] + Second, + ThirdOrMore, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EncodeLabelValue, EncodeLabelSet)] + #[metrics(crate = crate, rename_all = "SCREAMING-KEBAB-CASE", label = "kind")] + enum ScreamingLabel { + Postgres, + MySql, + } + + #[derive(Debug, Metrics)] + #[metrics(crate = crate, prefix = "test")] + struct MetricsWithLabels { + counters: Family, + gauges: Family, + } + + let test_metrics = MetricsWithLabels::default(); + test_metrics.counters[&KindLabel::First].inc(); + test_metrics.counters[&KindLabel::Second].inc_by(23); + test_metrics.counters[&KindLabel::ThirdOrMore].inc_by(42); + test_metrics.gauges[&ScreamingLabel::Postgres].set(5); + test_metrics.gauges[&ScreamingLabel::MySql].set(3); + + let mut registry = Registry::empty(); + registry.register_metrics(&test_metrics); + let mut buffer = String::new(); + registry.encode_to_text(&mut buffer).unwrap(); + let lines: Vec<_> = buffer.lines().collect(); + + assert!( + lines.contains(&r#"test_counters_total{kind="first"} 1"#), + "{lines:#?}" + ); + assert!( + lines.contains(&r#"test_counters_total{kind="2nd"} 23"#), + "{lines:#?}" + ); + assert!( + lines.contains(&r#"test_counters_total{kind="third_or_more"} 42"#), + "{lines:#?}" + ); + assert!( + lines.contains(&r#"test_gauges{kind="POSTGRES"} 5"#), + "{lines:#?}" + ); + assert!( + lines.contains(&r#"test_gauges{kind="MY-SQL"} 3"#), + "{lines:#?}" + ); +} diff --git a/crates/vise/src/traits.rs b/crates/vise/src/traits.rs index f02f43e..2970d62 100644 --- a/crates/vise/src/traits.rs +++ b/crates/vise/src/traits.rs @@ -1,78 +1,201 @@ -//! Core traits defined by the crate. +//! Traits used for metric definitions, such as [`GaugeValue`] and [`HistogramValue`]. -use once_cell::sync::Lazy; +use prometheus_client::metrics::gauge; -use std::ops; - -use crate::{ - collector::Collector, - descriptors::MetricGroupDescriptor, - registry::{MetricsVisitor, Registry}, +use std::{ + fmt, + sync::atomic::{AtomicI64, AtomicIsize, AtomicU64, AtomicUsize, Ordering}, + time::Duration, }; -/// Collection of metrics for a library or application. Should be derived using the corresponding macro. -pub trait Metrics: 'static + Send + Sync { - /// Metrics descriptor. - const DESCRIPTOR: MetricGroupDescriptor; +/// Encoded value of a gauge. +#[derive(Debug, Clone, Copy)] +pub enum EncodedGaugeValue { + /// Signed integer value. + I64(i64), + /// Floating point value. + F64(f64), +} - #[doc(hidden)] // implementation detail - fn visit_metrics(&self, visitor: MetricsVisitor<'_>); +/// Value of a [`Gauge`](crate::Gauge). +/// +/// This trait is implemented for signed and unsigned integers (`i64`, `u64`, `isize`, `usize`), +/// `f64` and [`Duration`]. To use smaller ints and floats as `Gauge` values, +/// they can be converted to their larger-sized variants (e.g., `i16` to `i64`, `u32` to `u64`, +/// and `f32` to `f64`). +pub trait GaugeValue: 'static + Copy + fmt::Debug { + /// Atomic store for the value. + type Atomic: gauge::Atomic + Default + fmt::Debug; + /// Encodes this value for exporting. + fn encode(self) -> EncodedGaugeValue; } -impl Metrics for &'static M { - const DESCRIPTOR: MetricGroupDescriptor = M::DESCRIPTOR; +impl GaugeValue for i64 { + type Atomic = AtomicI64; - fn visit_metrics(&self, visitor: MetricsVisitor<'_>) { - (**self).visit_metrics(visitor); + fn encode(self) -> EncodedGaugeValue { + EncodedGaugeValue::I64(self) } } -impl Metrics for Option { - const DESCRIPTOR: MetricGroupDescriptor = M::DESCRIPTOR; +impl GaugeValue for u64 { + type Atomic = AtomicU64Wrapper; // Can't use `AtomicU64` due to orphaning rules + + #[allow(clippy::cast_precision_loss)] // OK for reporting + fn encode(self) -> EncodedGaugeValue { + i64::try_from(self).map_or_else( + |_| EncodedGaugeValue::F64(self as f64), + EncodedGaugeValue::I64, + ) + } +} - fn visit_metrics(&self, visitor: MetricsVisitor<'_>) { - if let Some(metrics) = self { - metrics.visit_metrics(visitor); +/// Thin wrapper around [`AtomicU64`] used as atomic store for `u64`. +/// +/// A separate type is necessary to circumvent Rust orphaning rules. +#[derive(Debug, Default)] +pub struct AtomicU64Wrapper(AtomicU64); + +macro_rules! impl_atomic_wrapper { + ($wrapper:ty => $int:ty) => { + impl gauge::Atomic<$int> for $wrapper { + fn inc(&self) -> $int { + self.inc_by(1) + } + + fn inc_by(&self, v: $int) -> $int { + self.0.fetch_add(v, Ordering::Relaxed) + } + + fn dec(&self) -> $int { + self.dec_by(1) + } + + fn dec_by(&self, v: $int) -> $int { + self.0.fetch_sub(v, Ordering::Relaxed) + } + + fn set(&self, v: $int) -> $int { + self.0.swap(v, Ordering::Relaxed) + } + + fn get(&self) -> $int { + self.0.load(Ordering::Relaxed) + } } + }; +} + +impl_atomic_wrapper!(AtomicU64Wrapper => u64); + +impl GaugeValue for usize { + type Atomic = AtomicUsizeWrapper; // Can't use `AtomicUsize` due to orphaning rules + + fn encode(self) -> EncodedGaugeValue { + GaugeValue::encode(self as u64) + } +} + +/// Thin wrapper around [`AtomicUsize`] used as atomic store for `usize`. +/// +/// A separate type is necessary to circumvent Rust orphaning rules. +#[derive(Debug, Default)] +pub struct AtomicUsizeWrapper(AtomicUsize); + +impl_atomic_wrapper!(AtomicUsizeWrapper => usize); + +impl GaugeValue for isize { + type Atomic = AtomicIsizeWrapper; // Can't use `AtomicIsize` due to orphaning rules + + fn encode(self) -> EncodedGaugeValue { + EncodedGaugeValue::I64(self as i64) + } +} + +/// Thin wrapper around [`AtomicIsize`] used as atomic store for `isize`. +/// +/// A separate type is necessary to circumvent Rust orphaning rules. +#[derive(Debug, Default)] +pub struct AtomicIsizeWrapper(AtomicIsize); + +impl_atomic_wrapper!(AtomicIsizeWrapper => isize); + +impl GaugeValue for f64 { + type Atomic = AtomicU64; + + fn encode(self) -> EncodedGaugeValue { + EncodedGaugeValue::F64(self) } } -/// Global instance of [`Metrics`] allowing to access contained metrics from anywhere in code. -/// Should be used as a `static` item. -#[derive(Debug)] -pub struct Global(Lazy); +impl GaugeValue for Duration { + type Atomic = AtomicU64Wrapper; -impl Global { - /// Creates a new metrics instance. - pub const fn new() -> Self { - Self(Lazy::new(M::default)) + fn encode(self) -> EncodedGaugeValue { + EncodedGaugeValue::F64(self.as_secs_f64()) } } -impl ops::Deref for Global { - type Target = M; +impl gauge::Atomic for AtomicU64Wrapper { + fn inc(&self) -> Duration { + self.inc_by(Duration::from_secs(1)) + } + + fn inc_by(&self, v: Duration) -> Duration { + Duration::from_secs_f64(self.0.inc_by(v.as_secs_f64())) + } + + fn dec(&self) -> Duration { + self.dec_by(Duration::from_secs(1)) + } - fn deref(&self) -> &Self::Target { - &self.0 + fn dec_by(&self, v: Duration) -> Duration { + Duration::from_secs_f64(self.0.dec_by(v.as_secs_f64())) + } + + fn set(&self, v: Duration) -> Duration { + Duration::from_secs_f64(self.0.set(v.as_secs_f64())) + } + + fn get(&self) -> Duration { + Duration::from_secs_f64(self.0.get()) } } -/// Collects metrics from this type to registry. This is used by the [`register`](crate::register) -/// macro to handle registration of [`Global`] metrics and [`Collector`]s. -pub trait CollectToRegistry: 'static + Send + Sync { - #[doc(hidden)] // implementation detail - fn collect_to_registry(&'static self, registry: &mut Registry); +/// Value of a [`Histogram`](crate::Histogram). +/// +/// This trait is implemented for signed and unsigned integers (`i64`, `u64`, `isize`, `usize`), +/// `f64` and [`Duration`]. To use smaller ints and floats as `Histogram` values, +/// they can be converted to their larger-sized variants (e.g., `i16` to `i64`, `u32` to `u64`, +/// and `f32` to `f64`). +pub trait HistogramValue: 'static + Copy + fmt::Debug { + /// Encodes this value for exporting. + fn encode(self) -> f64; } -impl CollectToRegistry for Global { - fn collect_to_registry(&'static self, registry: &mut Registry) { - let metrics: &M = self; - registry.register_metrics(metrics); +impl HistogramValue for f64 { + fn encode(self) -> f64 { + self } } -impl CollectToRegistry for Collector { - fn collect_to_registry(&'static self, registry: &mut Registry) { - registry.register_collector(self); +impl HistogramValue for Duration { + fn encode(self) -> f64 { + self.as_secs_f64() } } + +macro_rules! impl_histogram_value_for_int { + ($int:ty) => { + impl HistogramValue for $int { + fn encode(self) -> f64 { + self as f64 + } + } + }; +} + +impl_histogram_value_for_int!(i64); +impl_histogram_value_for_int!(u64); +impl_histogram_value_for_int!(usize); +impl_histogram_value_for_int!(isize); diff --git a/crates/vise/src/wrappers.rs b/crates/vise/src/wrappers.rs index 7b0d176..101f67d 100644 --- a/crates/vise/src/wrappers.rs +++ b/crates/vise/src/wrappers.rs @@ -4,10 +4,8 @@ use elsa::sync::FrozenMap; use prometheus_client::{ encoding::{EncodeLabelSet, EncodeMetric, MetricEncoder}, metrics::{ - family::MetricConstructor, - gauge::{self, Gauge as GaugeInner}, - histogram::Histogram as HistogramInner, - MetricType, TypedMetric, + family::MetricConstructor, gauge::Gauge as GaugeInner, + histogram::Histogram as HistogramInner, MetricType, TypedMetric, }, }; @@ -17,122 +15,22 @@ use std::{ hash::Hash, marker::PhantomData, ops, - sync::{ - atomic::{AtomicI64, AtomicU64, Ordering}, - Arc, - }, + sync::Arc, time::{Duration, Instant}, }; -use crate::{buckets::Buckets, constructor::ConstructMetric}; - -#[derive(Debug, Clone, Copy)] -pub enum EncodedGaugeValue { - I64(i64), - F64(f64), -} - -pub trait GaugeValue: 'static + Copy + fmt::Debug { - type Atomic: gauge::Atomic + Default + fmt::Debug; - - fn encode(self) -> EncodedGaugeValue; -} - -impl GaugeValue for i64 { - type Atomic = AtomicI64; - - fn encode(self) -> EncodedGaugeValue { - EncodedGaugeValue::I64(self) - } -} - -impl GaugeValue for u64 { - type Atomic = AtomicU64Wrapper; // Can't use `AtomicU64` due to orphaning rules - - #[allow(clippy::cast_precision_loss)] // OK for reporting - fn encode(self) -> EncodedGaugeValue { - i64::try_from(self).map_or_else( - |_| EncodedGaugeValue::F64(self as f64), - EncodedGaugeValue::I64, - ) - } -} - -#[derive(Debug, Default)] -pub struct AtomicU64Wrapper(AtomicU64); - -impl gauge::Atomic for AtomicU64Wrapper { - fn inc(&self) -> u64 { - self.inc_by(1) - } - - fn inc_by(&self, v: u64) -> u64 { - self.0.fetch_add(v, Ordering::Relaxed) - } - - fn dec(&self) -> u64 { - self.dec_by(1) - } - - fn dec_by(&self, v: u64) -> u64 { - self.0.fetch_sub(v, Ordering::Relaxed) - } - - fn set(&self, v: u64) -> u64 { - self.0.swap(v, Ordering::Relaxed) - } - - fn get(&self) -> u64 { - self.0.load(Ordering::Relaxed) - } -} - -impl GaugeValue for f64 { - type Atomic = AtomicU64; - - fn encode(self) -> EncodedGaugeValue { - EncodedGaugeValue::F64(self) - } -} - -impl GaugeValue for Duration { - type Atomic = AtomicU64Wrapper; - - fn encode(self) -> EncodedGaugeValue { - EncodedGaugeValue::F64(self.as_secs_f64()) - } -} - -impl gauge::Atomic for AtomicU64Wrapper { - fn inc(&self) -> Duration { - self.inc_by(Duration::from_secs(1)) - } - - fn inc_by(&self, v: Duration) -> Duration { - Duration::from_secs_f64(self.0.inc_by(v.as_secs_f64())) - } - - fn dec(&self) -> Duration { - self.dec_by(Duration::from_secs(1)) - } - - fn dec_by(&self, v: Duration) -> Duration { - Duration::from_secs_f64(self.0.dec_by(v.as_secs_f64())) - } - - fn set(&self, v: Duration) -> Duration { - Duration::from_secs_f64(self.0.set(v.as_secs_f64())) - } - - fn get(&self) -> Duration { - Duration::from_secs_f64(self.0.get()) - } -} +use crate::{ + buckets::Buckets, + constructor::ConstructMetric, + traits::{EncodedGaugeValue, GaugeValue, HistogramValue}, +}; /// Gauge metric. /// /// Gauges are integer or floating-point values that can go up or down. Logically, a reported gauge value /// can be treated as valid until the next value is reported. +/// +/// Gauge values must implement the [`GaugeValue`] trait. pub struct Gauge(GaugeInner); impl fmt::Debug for Gauge { @@ -196,40 +94,12 @@ impl TypedMetric for Gauge { const TYPE: MetricType = MetricType::Gauge; } -pub trait HistogramValue: 'static + Copy + fmt::Debug { - fn encode(self) -> f64; -} - -impl HistogramValue for f64 { - fn encode(self) -> f64 { - self - } -} - -impl HistogramValue for Duration { - fn encode(self) -> f64 { - self.as_secs_f64() - } -} - -macro_rules! impl_histogram_value_for_int { - ($int:ty) => { - impl HistogramValue for $int { - fn encode(self) -> f64 { - self as f64 - } - } - }; -} - -impl_histogram_value_for_int!(i64); -impl_histogram_value_for_int!(u64); -impl_histogram_value_for_int!(usize); - /// Histogram metric. /// /// Histograms are floating-point values counted in configurable buckets. Logically, a histogram observes /// a certain probability distribution, and observations are transient (unlike gauge values). +/// +/// Histogram values must implement the [`HistogramValue`] trait. #[derive(Debug)] pub struct Histogram { inner: HistogramInner,