diff --git a/Cargo.toml b/Cargo.toml index 3ba5af07..455387ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,9 @@ members = [ "foundations", "foundations-macros", - "foundations-sentry", + "foundations-metrics", "foundations-metrics-registry", + "foundations-sentry", "examples", "tools/gen-syscall-enum", ] @@ -43,6 +44,7 @@ anyhow = "1.0.102" backtrace = "0.3.76" foundations = { version = "5.8.0", path = "./foundations", default-features = false } foundations-macros = { version = "=5.8.0", path = "./foundations-macros", default-features = false } +foundations-metrics-registry = { version = "0.1.0", path = "./foundations-metrics-registry" } bindgen = { version = "0.72.1", default-features = false } cc = "1.2.61" cf-rustracing = "1.4.0" diff --git a/foundations-metrics-registry/src/proto/mod.rs b/foundations-metrics-registry/src/proto/mod.rs index 0b124a77..d250dd1c 100644 --- a/foundations-metrics-registry/src/proto/mod.rs +++ b/foundations-metrics-registry/src/proto/mod.rs @@ -9,7 +9,7 @@ #[allow(missing_docs, unreachable_pub, clippy::all)] mod model; -pub use model::{BucketSpan, Histogram, Metric, MetricFamily, MetricType}; +pub use model::{BucketSpan, Counter, Gauge, Histogram, Metric, MetricFamily, MetricType}; #[cfg(test)] mod tests { diff --git a/foundations-metrics/Cargo.toml b/foundations-metrics/Cargo.toml new file mode 100644 index 00000000..bb63af01 --- /dev/null +++ b/foundations-metrics/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "foundations-metrics" +version = "0.1.0" +repository.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +foundations-metrics-registry = { workspace = true } +prometheus-client = "0.25.0" + +[lints] +workspace = true diff --git a/foundations-metrics/src/lib.rs b/foundations-metrics/src/lib.rs new file mode 100644 index 00000000..21344c09 --- /dev/null +++ b/foundations-metrics/src/lib.rs @@ -0,0 +1,17 @@ +//! The evolving layer of the `foundations` metrics stack. +//! +//! This crate provides the concrete metric types ([`Counter`], [`Gauge`], ...) +//! and the logic that encodes them into the Prometheus protobuf data model. It +//! builds on the slow-moving `foundations-metrics-registry` crate, which owns the +//! shared process-global registry and the stable wire format. +#![warn(missing_docs)] + +pub mod metrics; +mod registered; +mod value; + +pub use foundations_metrics_registry::{ + EncodeMetric, IntoMetrics, MetricFamily, RegistrationMetadata, register, +}; +pub use metrics::{Counter, CounterAtomic, Gauge, GaugeAtomic, GaugeGuard, RangeGauge}; +pub use registered::NamedMetric; diff --git a/foundations-metrics/src/metrics/counter.rs b/foundations-metrics/src/metrics/counter.rs new file mode 100644 index 00000000..31aaa84d --- /dev/null +++ b/foundations-metrics/src/metrics/counter.rs @@ -0,0 +1,230 @@ +use std::marker::PhantomData; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use foundations_metrics_registry::proto::{self, MetricType}; + +use super::IntoF64; + +use crate::{MetricFamily, value::EncodeMetricValue}; + +/// A monotonically increasing value, such as a request count or bytes served. +/// +/// A [`Counter`] may only ever be incremented; it never decreases (aside from a +/// reset to zero on process restart). It is a cheap handle over shared atomic +/// storage: [`Clone`] hands out another reference to the *same* series, so the +/// same counter can be incremented from many places and read back as a single +/// total. +/// +/// # Examples +/// +/// ``` +/// use foundations_metrics::Counter; +/// +/// let requests: Counter = Counter::default(); +/// requests.inc(); +/// requests.inc_by(4); +/// assert_eq!(requests.get(), 5); +/// +/// // Clones share storage. +/// let alias = requests.clone(); +/// alias.inc(); +/// assert_eq!(requests.get(), 6); +/// ``` +#[derive(Debug)] +pub struct Counter { + val: Arc, + marker: PhantomData, +} + +/// Atomic storage backing a [`Counter`]. +/// +/// Implemented for the numeric types a counter can hold. Foundations provides +/// implementations for `u64` and `f64` over [`AtomicU64`]; downstream code may +/// implement it for custom storage. +pub trait CounterAtomic { + /// Increments the value by one, returning the previous value. + fn inc(&self) -> N; + + /// Increments the value by `v`, returning the previous value. + fn inc_by(&self, v: N) -> N; + + /// Loads the current value. + fn get(&self) -> N; +} + +impl CounterAtomic for AtomicU64 { + #[inline] + fn inc(&self) -> u64 { + self.inc_by(1) + } + + #[inline] + fn inc_by(&self, v: u64) -> u64 { + self.fetch_add(v, Ordering::Relaxed) + } + + #[inline] + fn get(&self) -> u64 { + self.load(Ordering::Relaxed) + } +} + +impl CounterAtomic for AtomicU64 { + #[inline] + fn inc(&self) -> f64 { + self.inc_by(1.0) + } + + #[inline] + fn inc_by(&self, v: f64) -> f64 { + super::update_f64(self, |old| old + v) + } + + #[inline] + fn get(&self) -> f64 { + f64::from_bits(self.load(Ordering::Relaxed)) + } +} + +impl> Counter { + /// Increments the counter by one, returning the previous value. + #[inline] + pub fn inc(&self) -> N { + self.val.inc() + } + + /// Increments the counter by `v`, returning the previous value. + #[inline] + pub fn inc_by(&self, v: N) -> N { + self.val.inc_by(v) + } + + /// Returns the current value. + #[inline] + pub fn get(&self) -> N { + self.val.get() + } + + /// Returns a reference to the underlying atomic storage. + #[inline] + pub fn inner(&self) -> &A { + self.val.as_ref() + } +} + +impl Clone for Counter { + fn clone(&self) -> Self { + Self { + val: Arc::clone(&self.val), + marker: PhantomData, + } + } +} + +impl Default for Counter { + fn default() -> Self { + Self { + val: Arc::new(A::default()), + marker: PhantomData, + } + } +} + +/// Builds the protobuf `MetricFamily` for a counter value. +/// +/// The name/help are left empty here; they are populated at registration and +/// encode time. +fn encode_counter(value: f64) -> Vec { + vec![MetricFamily { + name: Some(String::new()), + help: None, + r#type: Some(MetricType::Counter as i32), + metric: vec![proto::Metric { + counter: Some(proto::Counter { + value: Some(value), + ..Default::default() + }), + ..Default::default() + }], + unit: None, + }] +} + +impl EncodeMetricValue for Counter +where + N: IntoF64, + A: CounterAtomic + Send + Sync + 'static, +{ + fn encode_metric_value(&self) -> Vec { + encode_counter(self.get().into_f64()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn encoded_value(counter: Counter) -> f64 + where + N: IntoF64, + A: CounterAtomic + Send + Sync + 'static, + { + let families = counter.encode_metric_value(); + let family = &families[0]; + + assert_eq!(family.r#type, Some(MetricType::Counter as i32)); + assert_eq!(family.metric.len(), 1); + + family.metric[0] + .counter + .as_ref() + .and_then(|counter| counter.value) + .expect("encoded counter has a value") + } + + #[test] + fn inc_inc_by_and_get() { + let counter = Counter::::default(); + assert_eq!(counter.get(), 0); + + assert_eq!(counter.inc(), 0); + assert_eq!(counter.get(), 1); + + assert_eq!(counter.inc_by(4), 1); + assert_eq!(counter.get(), 5); + } + + #[test] + fn f64_counter_inc_and_get() { + let counter = Counter::::default(); + assert_eq!(counter.get(), 0.0); + + assert_eq!(counter.inc(), 0.0); + assert_eq!(counter.inc_by(1.5), 1.0); + assert_eq!(counter.get(), 2.5); + } + + #[test] + fn clones_share_storage() { + let counter: Counter = Counter::default(); + let alias = counter.clone(); + + counter.inc(); + alias.inc(); + + assert_eq!(counter.get(), 2); + assert_eq!(alias.get(), 2); + } + + #[test] + fn encodes_counter_values() { + let unsigned = Counter::::default(); + unsigned.inc_by(7); + assert_eq!(encoded_value(unsigned), 7.0); + + let float = Counter::::default(); + float.inc_by(1.5); + assert_eq!(encoded_value(float), 1.5); + } +} diff --git a/foundations-metrics/src/metrics/gauge.rs b/foundations-metrics/src/metrics/gauge.rs new file mode 100644 index 00000000..533df4ee --- /dev/null +++ b/foundations-metrics/src/metrics/gauge.rs @@ -0,0 +1,591 @@ +use std::marker::PhantomData; +use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; + +use foundations_metrics_registry::proto::{self, MetricType}; + +use crate::{MetricFamily, value::EncodeMetricValue}; + +use super::IntoF64; + +/// A metric whose value may increase, decrease, or be set directly. +/// +/// It is a cheap handle over shared atomic storage: [`Clone`] hands out another +/// reference to the *same* series, so the gauge can be updated from many places +/// and read back as a single current value. +/// +/// # Examples +/// +/// ``` +/// use foundations_metrics::Gauge; +/// +/// let connections: Gauge = Gauge::default(); +/// connections.inc(); +/// connections.inc_by(4); +/// connections.dec_by(2); +/// assert_eq!(connections.get(), 3); +/// +/// // Clones share storage. +/// let alias = connections.clone(); +/// alias.set(10); +/// assert_eq!(connections.get(), 10); +/// ``` +#[derive(Debug)] +pub struct Gauge { + val: Arc, + marker: PhantomData, +} + +impl Clone for Gauge { + fn clone(&self) -> Self { + Self { + val: Arc::clone(&self.val), + marker: PhantomData, + } + } +} + +impl Default for Gauge { + fn default() -> Self { + Self { + val: Arc::new(A::default()), + marker: PhantomData, + } + } +} + +/// Atomic storage backing a [`Gauge`]. +/// +/// Implemented for the numeric types a gauge can hold. Foundations provides +/// implementations over the standard library's 64-bit atomics (`i64`, `u64`, +/// and `f64`); downstream code may implement it for custom storage. +/// +/// Every method returns the value held *before* the operation was applied. +pub trait GaugeAtomic { + /// Increases the value by one, returning the previous value. + fn inc(&self) -> N; + + /// Increases the value by `v`, returning the previous value. + fn inc_by(&self, v: N) -> N; + + /// Decreases the value by one, returning the previous value. + fn dec(&self) -> N; + + /// Decreases the value by `v`, returning the previous value. + fn dec_by(&self, v: N) -> N; + + /// Sets the value to `v`, returning the previous value. + fn set(&self, v: N) -> N; + + /// Loads the current value. + fn get(&self) -> N; +} + +impl GaugeAtomic for AtomicI64 { + #[inline] + fn inc(&self) -> i64 { + self.inc_by(1) + } + + #[inline] + fn inc_by(&self, v: i64) -> i64 { + self.fetch_add(v, Ordering::Relaxed) + } + + #[inline] + fn dec(&self) -> i64 { + self.dec_by(1) + } + + #[inline] + fn dec_by(&self, v: i64) -> i64 { + self.fetch_sub(v, Ordering::Relaxed) + } + + #[inline] + fn set(&self, v: i64) -> i64 { + self.swap(v, Ordering::Relaxed) + } + + #[inline] + fn get(&self) -> i64 { + self.load(Ordering::Relaxed) + } +} + +impl GaugeAtomic for AtomicU64 { + #[inline] + fn inc(&self) -> u64 { + self.inc_by(1) + } + + #[inline] + fn inc_by(&self, v: u64) -> u64 { + self.fetch_add(v, Ordering::Relaxed) + } + + #[inline] + fn dec(&self) -> u64 { + self.dec_by(1) + } + + #[inline] + fn dec_by(&self, v: u64) -> u64 { + self.fetch_sub(v, Ordering::Relaxed) + } + + #[inline] + fn set(&self, v: u64) -> u64 { + self.swap(v, Ordering::Relaxed) + } + + #[inline] + fn get(&self) -> u64 { + self.load(Ordering::Relaxed) + } +} + +impl GaugeAtomic for AtomicU64 { + #[inline] + fn inc(&self) -> f64 { + self.inc_by(1.0) + } + + #[inline] + fn inc_by(&self, v: f64) -> f64 { + super::update_f64(self, |old| old + v) + } + + #[inline] + fn dec(&self) -> f64 { + self.dec_by(1.0) + } + + #[inline] + fn dec_by(&self, v: f64) -> f64 { + super::update_f64(self, |old| old - v) + } + + #[inline] + fn set(&self, v: f64) -> f64 { + f64::from_bits(self.swap(v.to_bits(), Ordering::Relaxed)) + } + + #[inline] + fn get(&self) -> f64 { + f64::from_bits(self.load(Ordering::Relaxed)) + } +} + +impl> Gauge { + /// Increases the gauge by one, returning the previous value. + #[inline] + pub fn inc(&self) -> N { + self.val.inc() + } + + /// Increases the gauge by `v`, returning the previous value. + #[inline] + pub fn inc_by(&self, v: N) -> N { + self.val.inc_by(v) + } + + /// Decreases the gauge by one, returning the previous value. + #[inline] + pub fn dec(&self) -> N { + self.val.dec() + } + + /// Decreases the gauge by `v`, returning the previous value. + #[inline] + pub fn dec_by(&self, v: N) -> N { + self.val.dec_by(v) + } + + /// Sets the gauge to `v`, returning the previous value. + #[inline] + pub fn set(&self, v: N) -> N { + self.val.set(v) + } + + /// Returns the current value. + #[inline] + pub fn get(&self) -> N { + self.val.get() + } + + /// Returns a reference to the underlying atomic storage. + #[inline] + pub fn inner(&self) -> &A { + self.val.as_ref() + } +} + +impl EncodeMetricValue for Gauge +where + N: IntoF64, + A: GaugeAtomic + Send + Sync + 'static, +{ + fn encode_metric_value(&self) -> Vec { + vec![MetricFamily { + name: Some(String::new()), + help: None, + r#type: Some(MetricType::Gauge as i32), + metric: vec![proto::Metric { + gauge: Some(proto::Gauge { + value: Some(self.get().into_f64()), + }), + ..Default::default() + }], + unit: None, + }] + } +} + +/// A gauge that also records the minimum and maximum values seen since the last +/// scrape. +/// +/// This gives visibility into the full range of a value within a scrape interval +/// with less overhead than a histogram. Reading the metric at encode time resets +/// the tracked minimum and maximum. It exports three series: the current value, +/// `_min`, and `_max`. +/// +/// # Examples +/// +/// ```ignore +/// use foundations_metrics::{metrics, RangeGauge}; +/// +/// #[metrics] +/// pub mod my_app_metrics { +/// /// Number of requests awaiting a response. +/// pub fn inflight_requests() -> RangeGauge; +/// } +/// +/// fn usage() { +/// for _ in 0..10 { +/// my_app_metrics::inflight_requests().inc(); +/// } +/// for _ in 0..8 { +/// my_app_metrics::inflight_requests().dec(); +/// } +/// +/// // If scraped now, the metric exports these three series: +/// // inflight_requests 2 +/// // inflight_requests_min 0 +/// // inflight_requests_max 10 +/// } +/// ``` +#[derive(Debug, Clone, Default)] +pub struct RangeGauge { + gauge: Gauge, + min: Arc, + max: Arc, + reset_cs: Arc>, +} + +impl RangeGauge { + /// Increases the gauge by one, returning the previous value. + pub fn inc(&self) -> u64 { + self.inc_by(1) + } + + /// Increases the gauge by `v`, returning the previous value. + pub fn inc_by(&self, v: u64) -> u64 { + let prev = self.gauge.inc_by(v); + self.update_max(prev + v); + prev + } + + /// Decreases the gauge by one, returning the previous value. + pub fn dec(&self) -> u64 { + self.dec_by(1) + } + + /// Decreases the gauge by `v`, returning the previous value. + pub fn dec_by(&self, v: u64) -> u64 { + let prev = self.gauge.dec_by(v); + self.update_min(prev - v); + prev + } + + /// Sets the gauge to `v`, returning the previous value. + pub fn set(&self, v: u64) -> u64 { + let prev = self.gauge.set(v); + self.update_max(v); + self.update_min(v); + prev + } + + /// Returns the current value of the gauge. + pub fn get(&self) -> u64 { + self.gauge.get() + } + + /// Exposes the inner atomic backing the current value. + /// + /// This should only be used for advanced use-cases not directly supported by + /// the library. + pub fn inner(&self) -> &AtomicU64 { + self.gauge.inner() + } + + fn update_max(&self, new_max: u64) { + self.max.fetch_max(new_max, Ordering::AcqRel); + } + + fn update_min(&self, new_min: u64) { + self.min.fetch_min(new_min, Ordering::AcqRel); + } + + /// Returns `(min, current, max)`, guaranteeing `min <= current <= max`, and + /// resets the tracked minimum and maximum to the current value. + fn get_values(&self) -> (u64, u64, u64) { + // Avoid data races by ensuring only one thread can perform the reset operation. + let _reset_guard = self.reset_cs.lock().unwrap(); + + // First, get the current metric. + let current = self.get(); + + // Obtain min and max by swapping their contents with the current value. + // It is possible that current == min and another thread decremented current before we + // read its value, but has not yet decremented min. Enforce min <= current to account for + // that race. The same caveat applies to max. + let min = std::cmp::min(current, self.min.swap(current, Ordering::AcqRel)); + let max = std::cmp::max(current, self.max.swap(current, Ordering::AcqRel)); + + // The current value may have changed between reading it and resetting min/max. Read it + // once more and ensure subsequent scrapes retain the invariant min <= current <= max. + let current_fixup = self.get(); + self.min.fetch_min(current_fixup, Ordering::AcqRel); + self.max.fetch_max(current_fixup, Ordering::AcqRel); + + // | min | c | max | + // T1: read current | 1 | 1 | 1 | + // T2: increment by 1 | 1 | 2 | 2 | + // T3: decrement by 2 | 0 | 0 | 2 | + + (min, current, max) + } +} + +/// Builds a single-row gauge `MetricFamily` with a relative name `suffix`. +fn gauge_family(suffix: &str, value: u64) -> MetricFamily { + MetricFamily { + name: Some(suffix.to_owned()), + help: None, + r#type: Some(MetricType::Gauge as i32), + metric: vec![proto::Metric { + gauge: Some(proto::Gauge { + value: Some(value as f64), + }), + ..Default::default() + }], + unit: None, + } +} + +impl EncodeMetricValue for RangeGauge { + fn encode_metric_value(&self) -> Vec { + let (min, current, max) = self.get_values(); + + vec![ + gauge_family("", current), + gauge_family("_min", min), + gauge_family("_max", max), + ] + } +} + +/// Increments a gauge when created and decrements it when dropped. +/// +/// Useful for tracking the number of in-progress operations: hold the guard for +/// the duration of the work and the gauge reflects the live count. +/// +/// # Examples +/// +/// ```ignore +/// use foundations_metrics::{metrics, Gauge, GaugeGuard}; +/// +/// #[metrics] +/// pub mod my_app_metrics { +/// /// Number of currently connected clients. +/// pub fn client_connections_active() -> Gauge; +/// } +/// +/// fn usage() { +/// let client_metric = GaugeGuard::new(my_app_metrics::client_connections_active()); +/// // Do work where you want the metric to remain incremented. +/// // When it leaves scope, the metric will be decremented. +/// // Alternatively, move ownership to another scope to change the lifetime. +/// tokio::spawn(async move { +/// // Do work with arbitrary lifetime on another task. +/// // Manually drop to force `client_metric` ownership to this task. +/// drop(client_metric); +/// }); +/// } +/// ``` +pub struct GaugeGuard(G); + +impl GaugeGuard { + /// Creates a guard, incrementing the gauge now and decrementing it on drop. + pub fn new(gauge: G) -> Self { + gauge.inc(); + Self(gauge) + } +} + +impl Drop for GaugeGuard { + fn drop(&mut self) { + self.0.dec(); + } +} + +/// Helper trait for values supported by [`GaugeGuard`]. +pub trait GenericGauge { + /// Increases the wrapped gauge by one. + fn inc(&self); + + /// Decreases the wrapped gauge by one. + fn dec(&self); +} + +impl GenericGauge for Gauge { + fn inc(&self) { + Gauge::inc(self); + } + + fn dec(&self) { + Gauge::dec(self); + } +} + +impl GenericGauge for RangeGauge { + fn inc(&self) { + RangeGauge::inc(self); + } + + fn dec(&self) { + RangeGauge::dec(self); + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicI64, AtomicU64}; + + use super::*; + + fn encoded_value(gauge: Gauge) -> f64 + where + N: IntoF64, + A: GaugeAtomic + Send + Sync + 'static, + { + let families = gauge.encode_metric_value(); + let family = &families[0]; + + assert_eq!(family.r#type, Some(MetricType::Gauge as i32)); + assert_eq!(family.metric.len(), 1); + + family.metric[0] + .gauge + .as_ref() + .and_then(|gauge| gauge.value) + .expect("encoded gauge has a value") + } + + #[test] + fn default_gauge_uses_u64_and_clones_share_storage() { + let gauge: Gauge = Gauge::default(); + let alias = gauge.clone(); + + assert!(std::ptr::eq(gauge.inner(), alias.inner())); + assert_eq!(gauge.set(4), 0); + assert_eq!(alias.inc(), 4); + assert_eq!(gauge.inc_by(3), 5); + assert_eq!(alias.dec(), 8); + assert_eq!(gauge.dec_by(2), 7); + assert_eq!(alias.get(), 5); + } + + #[test] + fn encodes_64_bit_gauge_values() { + let signed = Gauge::::default(); + signed.set(-3); + assert_eq!(encoded_value(signed), -3.0); + + let unsigned = Gauge::::default(); + unsigned.set(7); + assert_eq!(encoded_value(unsigned), 7.0); + + let float = Gauge::::default(); + float.set(1.5); + assert_eq!(encoded_value(float), 1.5); + } + + /// Reads the encoded `(current, min, max)` triple; encoding resets min/max. + #[track_caller] + fn range_values(gauge: &RangeGauge) -> (u64, u64, u64) { + let families = gauge.encode_metric_value(); + assert_eq!(families.len(), 3); + assert_eq!(families[0].name.as_deref(), Some("")); + assert_eq!(families[1].name.as_deref(), Some("_min")); + assert_eq!(families[2].name.as_deref(), Some("_max")); + + let value = |family: &MetricFamily| { + family.metric[0] + .gauge + .as_ref() + .and_then(|gauge| gauge.value) + .expect("encoded range gauge has a value") as u64 + }; + + ( + value(&families[0]), + value(&families[1]), + value(&families[2]), + ) + } + + #[test] + fn range_gauge_tracks_and_resets_min_max() { + let gauge = RangeGauge::default(); + + assert_eq!(range_values(&gauge), (0, 0, 0)); + + gauge.inc(); + assert_eq!(range_values(&gauge), (1, 0, 1)); + assert_eq!(range_values(&gauge), (1, 1, 1)); + + gauge.dec(); + assert_eq!(range_values(&gauge), (0, 0, 1)); + assert_eq!(range_values(&gauge), (0, 0, 0)); + + gauge.inc_by(3); + gauge.dec_by(2); + assert_eq!(range_values(&gauge), (1, 0, 3)); + + gauge.inc_by(1); + gauge.dec_by(2); + assert_eq!(range_values(&gauge), (0, 0, 2)); + } + + #[test] + fn gauge_guard_inc_on_new_dec_on_drop() { + let gauge: Gauge = Gauge::default(); + { + let _guard = GaugeGuard::new(gauge.clone()); + assert_eq!(gauge.get(), 1); + } + assert_eq!(gauge.get(), 0); + } + + #[test] + fn gauge_guard_supports_range_gauge() { + let gauge = RangeGauge::default(); + { + let _guard = GaugeGuard::new(gauge.clone()); + assert_eq!(gauge.get(), 1); + } + assert_eq!(gauge.get(), 0); + } +} diff --git a/foundations-metrics/src/metrics/mod.rs b/foundations-metrics/src/metrics/mod.rs new file mode 100644 index 00000000..6302dbad --- /dev/null +++ b/foundations-metrics/src/metrics/mod.rs @@ -0,0 +1,74 @@ +//! The metrics module that contains scalar metrics (like Counter, Gauge, ...) and non-scalars, +//! like Histogram, NativeHistogram, and also Family. +//! +//! # License +//! +//! Substantial parts of the code in this module were adapted from +//! [`prometheus-client`] subject to their licensing terms: +//! +//! ```text +//! MIT License +//! +//! Copyright (c) 2020 Max Inden +//! +//! Permission is hereby granted, free of charge, to any person obtaining a copy +//! of this software and associated documentation files (the "Software"), to deal +//! in the Software without restriction, including without limitation the rights +//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//! copies of the Software, and to permit persons to whom the Software is +//! furnished to do so, subject to the following conditions: +//! +//! The above copyright notice and this permission notice shall be included in all +//! copies or substantial portions of the Software. +//! +//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//! SOFTWARE. +//! ``` +//! +//! [`prometheus-client`]: https://github.com/prometheus/client_rust + +use std::sync::atomic::{AtomicU64, Ordering}; + +mod counter; +mod gauge; + +pub use counter::{Counter, CounterAtomic}; +pub use gauge::{Gauge, GaugeAtomic, GaugeGuard, RangeGauge}; + +fn update_f64(atomic: &AtomicU64, f: impl Fn(f64) -> f64) -> f64 { + let mut old_bits = atomic.load(Ordering::Relaxed); + + loop { + let old = f64::from_bits(old_bits); + let new_bits = f(old).to_bits(); + + match atomic.compare_exchange_weak(old_bits, new_bits, Ordering::Relaxed, Ordering::Relaxed) + { + Ok(_) => return old, + Err(actual) => old_bits = actual, + } + } +} + +trait IntoF64: Send + Sync + 'static { + fn into_f64(self) -> f64; +} + +macro_rules! impl_into_f64 { + ($($ty:ty),+ $(,)?) => { + $( + impl IntoF64 for $ty { + fn into_f64(self) -> f64 { + self as f64 + } + } + )+ + }; +} + +impl_into_f64!(i64, u64, f64); diff --git a/foundations-metrics/src/registered.rs b/foundations-metrics/src/registered.rs new file mode 100644 index 00000000..473a5b87 --- /dev/null +++ b/foundations-metrics/src/registered.rs @@ -0,0 +1,108 @@ +use crate::{EncodeMetric, MetricFamily, value::EncodeMetricValue}; + +/// A metric paired with the name and help text it is exported under. +/// +/// Storage types (`Counter`, `Gauge`, ...) hold only their value and encode +/// themselves with relative names (a suffix such as `""`, `_min`, or `_max`). +/// `NamedMetric` supplies the registered base name and help text, bridging the +/// internal `EncodeMetricValue` storage trait to the public +/// [`EncodeMetric`] registry trait. +pub struct NamedMetric { + name: &'static str, + help: &'static str, + metric: M, +} + +impl NamedMetric { + /// Wraps `metric` with the `name` and `help` it is exported under. + pub fn new(name: &'static str, help: &'static str, metric: M) -> Self { + Self { name, help, metric } + } +} + +impl EncodeMetric for NamedMetric +where + M: EncodeMetricValue, +{ + fn encode(&self) -> Vec { + let mut families = self.metric.encode_metric_value(); + + for family in &mut families { + let name = family.name.get_or_insert_default(); + name.insert_str(0, self.name); + + if family.help.is_none() { + family.help = Some(self.help.to_owned()); + } + } + + families + } +} + +#[cfg(test)] +mod tests { + use foundations_metrics_registry::proto::MetricType; + + use super::*; + use crate::{Counter, RangeGauge}; + + #[test] + fn rewrites_relative_name_and_fills_help() { + let counter = Counter::::default(); + let named = NamedMetric::new( + "http_requests_total", + "Number of requests.", + counter.clone(), + ); + + counter.inc_by(5); + + let families = named.encode(); + assert_eq!(families.len(), 1); + + let family = &families[0]; + assert_eq!(family.name.as_deref(), Some("http_requests_total")); + assert_eq!(family.help.as_deref(), Some("Number of requests.")); + assert_eq!(family.r#type, Some(MetricType::Counter as i32)); + assert_eq!( + family.metric[0].counter.as_ref().and_then(|c| c.value), + Some(5.0) + ); + } + + #[test] + fn prefixes_each_range_gauge_series() { + let gauge = RangeGauge::default(); + gauge.inc_by(3); + gauge.dec_by(2); + + let named = NamedMetric::new( + "inflight_requests", + "Number of requests awaiting a response.", + gauge, + ); + + let families = named.encode(); + let expected = [ + ("inflight_requests", 1.0), + ("inflight_requests_min", 0.0), + ("inflight_requests_max", 3.0), + ]; + + assert_eq!(families.len(), expected.len()); + + for (family, (name, value)) in families.iter().zip(expected) { + assert_eq!(family.name.as_deref(), Some(name)); + assert_eq!( + family.help.as_deref(), + Some("Number of requests awaiting a response.") + ); + assert_eq!(family.r#type, Some(MetricType::Gauge as i32)); + assert_eq!( + family.metric[0].gauge.as_ref().and_then(|g| g.value), + Some(value) + ); + } + } +} diff --git a/foundations-metrics/src/value.rs b/foundations-metrics/src/value.rs new file mode 100644 index 00000000..ed98433b --- /dev/null +++ b/foundations-metrics/src/value.rs @@ -0,0 +1,12 @@ +use crate::MetricFamily; + +/// Encodes metric values before registration metadata is applied. +/// +/// Each returned [`MetricFamily`] must set `name` to a relative suffix. The +/// primary series uses `Some("")`; additional series use names such as +/// `Some("_min")` and `Some("_max")`. [`NamedMetric`](crate::NamedMetric) +/// prepends the registered metric name and fills in missing help text. +pub(crate) trait EncodeMetricValue: Send + Sync + 'static { + /// Encodes the current value into one or more relatively named families. + fn encode_metric_value(&self) -> Vec; +}