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}