Skip to main content

metrics_lib/
metadata.rs

1//! Per-metric metadata: help text + unit.
2//!
3//! Exporters use this metadata to emit `# HELP` / `# TYPE` lines (Prometheus,
4//! OpenMetrics), `attributes` (OTLP), and unit suffixes (StatsD). It is
5//! optional — every metric registered through `MetricsCore::counter(name)` /
6//! `gauge(name)` / `timer(name)` / `rate(name)` / `counter_with(name, …)` /
7//! etc. exports successfully without metadata; help text and units are
8//! purely additive.
9//!
10//! Register metadata via [`crate::Registry::describe`] or the convenience
11//! shorthands `Registry::describe_counter` / `describe_gauge` /
12//! `describe_timer` / `describe_rate` / `describe_histogram`.
13
14use std::borrow::Cow;
15
16/// Metric kind tag stored alongside help/unit metadata.
17///
18/// Used by exporters to emit the correct `# TYPE` line and to choose a
19/// rendering strategy (e.g., `counter` vs. `gauge` semantics).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize))]
22pub enum MetricKind {
23    /// Monotonic counter (resets only on process restart / explicit reset).
24    Counter,
25    /// Arbitrary-direction gauge.
26    Gauge,
27    /// Timing distribution.
28    Timer,
29    /// Rate over a tumbling window.
30    Rate,
31    /// Histogram of observations bucketed by value.
32    Histogram,
33}
34
35impl MetricKind {
36    /// Lower-case Prometheus/OpenMetrics `# TYPE` token.
37    #[inline]
38    pub const fn as_prometheus_type(self) -> &'static str {
39        match self {
40            // `Timer` and `Rate` aren't first-class Prometheus types; we
41            // export them as `histogram`-shaped and `gauge`-shaped
42            // respectively in the Prometheus exporter, but the metadata
43            // kind preserves the original semantic.
44            MetricKind::Counter => "counter",
45            MetricKind::Gauge => "gauge",
46            MetricKind::Timer => "histogram",
47            MetricKind::Rate => "gauge",
48            MetricKind::Histogram => "histogram",
49        }
50    }
51}
52
53/// Unit of measurement for a metric value.
54///
55/// Exporters use the unit for two things:
56/// 1. Emit OpenMetrics `# UNIT` lines and Prometheus name suffixes
57///    (`_seconds`, `_bytes`, `_total`, …).
58/// 2. Normalise values where appropriate (e.g., Timer values exported in
59///    seconds rather than nanoseconds).
60///
61/// `Unit::Custom` carries a free-form static string for units not enumerated
62/// here.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize))]
65pub enum Unit {
66    /// No unit / dimensionless quantity.
67    #[default]
68    None,
69    /// SI seconds.
70    Seconds,
71    /// SI milliseconds.
72    Milliseconds,
73    /// SI microseconds.
74    Microseconds,
75    /// SI nanoseconds.
76    Nanoseconds,
77    /// Bytes (binary, 1024-scale used by the `MemoryGauge` helpers).
78    Bytes,
79    /// Kilobytes (1024 bytes).
80    Kilobytes,
81    /// Megabytes (1024² bytes).
82    Megabytes,
83    /// Gigabytes (1024³ bytes).
84    Gigabytes,
85    /// Percentage 0..=100.
86    Percent,
87    /// Ratio 0.0..=1.0.
88    Ratio,
89    /// Free-form unit name (e.g., `"requests"`, `"connections"`).
90    Custom(&'static str),
91}
92
93impl Unit {
94    /// Lower-case Prometheus/OpenMetrics unit name. Returns `""` for
95    /// [`Unit::None`].
96    pub const fn as_str(self) -> &'static str {
97        match self {
98            Unit::None => "",
99            Unit::Seconds => "seconds",
100            Unit::Milliseconds => "milliseconds",
101            Unit::Microseconds => "microseconds",
102            Unit::Nanoseconds => "nanoseconds",
103            Unit::Bytes => "bytes",
104            Unit::Kilobytes => "kilobytes",
105            Unit::Megabytes => "megabytes",
106            Unit::Gigabytes => "gigabytes",
107            Unit::Percent => "percent",
108            Unit::Ratio => "ratio",
109            Unit::Custom(s) => s,
110        }
111    }
112}
113
114/// Per-metric metadata stored in the [`crate::Registry`].
115#[derive(Debug, Clone, PartialEq, Eq, Hash)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize))]
117pub struct MetricMetadata {
118    /// Free-form help text rendered as `# HELP` in Prometheus exports and
119    /// `description` in OTLP exports.
120    pub help: Cow<'static, str>,
121    /// Unit of the metric value.
122    pub unit: Unit,
123    /// Declared metric kind (informational; exporters infer the actual
124    /// shape from the metric type in the registry).
125    pub kind: MetricKind,
126}
127
128impl MetricMetadata {
129    /// Construct metadata with the given kind, help text, and unit.
130    pub fn new(kind: MetricKind, help: impl Into<Cow<'static, str>>, unit: Unit) -> Self {
131        Self {
132            kind,
133            help: help.into(),
134            unit,
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn unit_strings_match_prometheus_conventions() {
145        assert_eq!(Unit::Seconds.as_str(), "seconds");
146        assert_eq!(Unit::Bytes.as_str(), "bytes");
147        assert_eq!(Unit::None.as_str(), "");
148        assert_eq!(Unit::Custom("foo").as_str(), "foo");
149    }
150
151    #[test]
152    fn kind_prometheus_type_tokens_are_lowercase() {
153        for k in [
154            MetricKind::Counter,
155            MetricKind::Gauge,
156            MetricKind::Timer,
157            MetricKind::Rate,
158            MetricKind::Histogram,
159        ] {
160            let s = k.as_prometheus_type();
161            assert!(s.chars().all(|c| c.is_ascii_lowercase()));
162        }
163    }
164}