Skip to main content

iroh_metrics/
labels.rs

1//! Label types for metric families.
2
3use std::{borrow::Cow, hash::Hash};
4
5/// A label value that can be encoded as a string for OpenMetrics output.
6#[derive(Debug, Clone, PartialEq)]
7pub enum LabelValue<'a> {
8    /// String value.
9    Str(Cow<'a, str>),
10    /// Signed integer.
11    Int(i64),
12    /// Unsigned integer.
13    Uint(u64),
14    /// Boolean.
15    Bool(bool),
16}
17
18impl LabelValue<'_> {
19    /// Converts to string representation for encoding.
20    pub fn as_str(&self) -> Cow<'_, str> {
21        match self {
22            LabelValue::Str(s) => Cow::Borrowed(s.as_ref()),
23            LabelValue::Int(v) => Cow::Owned(v.to_string()),
24            LabelValue::Uint(v) => Cow::Owned(v.to_string()),
25            LabelValue::Bool(v) => Cow::Borrowed(if *v { "true" } else { "false" }),
26        }
27    }
28}
29
30impl<'a> From<&'a str> for LabelValue<'a> {
31    fn from(s: &'a str) -> Self {
32        LabelValue::Str(Cow::Borrowed(s))
33    }
34}
35
36impl From<String> for LabelValue<'static> {
37    fn from(s: String) -> Self {
38        LabelValue::Str(Cow::Owned(s))
39    }
40}
41
42macro_rules! impl_from_int {
43    ($($t:ty => $variant:ident),*) => {
44        $(
45            impl From<$t> for LabelValue<'static> {
46                fn from(v: $t) -> Self {
47                    LabelValue::$variant(v as _)
48                }
49            }
50        )*
51    };
52}
53
54impl_from_int!(
55    i64 => Int, i32 => Int, i16 => Int, i8 => Int,
56    u64 => Uint, u32 => Uint, u16 => Uint, u8 => Uint
57);
58
59impl From<bool> for LabelValue<'static> {
60    fn from(v: bool) -> Self {
61        LabelValue::Bool(v)
62    }
63}
64
65/// A key-value label pair.
66pub type LabelPair<'a> = (&'static str, LabelValue<'a>);
67
68/// Encodes a single field as a [`LabelValue`].
69///
70/// Implemented for the standard label-supported types so the
71/// `#[derive(EncodeLabelSet)]` macro can borrow string fields without
72/// allocating on each scrape.
73pub trait EncodeLabelValue {
74    /// Borrows or copies `self` into a [`LabelValue`].
75    fn encode_label_value(&self) -> LabelValue<'_>;
76}
77
78impl EncodeLabelValue for str {
79    fn encode_label_value(&self) -> LabelValue<'_> {
80        LabelValue::Str(Cow::Borrowed(self))
81    }
82}
83
84impl EncodeLabelValue for String {
85    fn encode_label_value(&self) -> LabelValue<'_> {
86        LabelValue::Str(Cow::Borrowed(self.as_str()))
87    }
88}
89
90impl<T: EncodeLabelValue + ?Sized> EncodeLabelValue for &T {
91    fn encode_label_value(&self) -> LabelValue<'_> {
92        T::encode_label_value(self)
93    }
94}
95
96impl EncodeLabelValue for bool {
97    fn encode_label_value(&self) -> LabelValue<'_> {
98        LabelValue::Bool(*self)
99    }
100}
101
102macro_rules! impl_encode_label_value_int {
103    ($($t:ty => $variant:ident),*) => {
104        $(
105            impl EncodeLabelValue for $t {
106                fn encode_label_value(&self) -> LabelValue<'_> {
107                    LabelValue::$variant(*self as _)
108                }
109            }
110        )*
111    };
112}
113
114impl_encode_label_value_int!(
115    i64 => Int, i32 => Int, i16 => Int, i8 => Int,
116    u64 => Uint, u32 => Uint, u16 => Uint, u8 => Uint
117);
118
119/// Trait for types that can be encoded as a set of labels.
120///
121/// Implement this for label structs to use with [`Family`](crate::Family).
122/// The struct must also implement `Clone + Hash + Eq + Send + Sync`.
123pub trait EncodeLabelSet: Hash + Eq + Clone + Send + Sync + 'static {
124    /// Returns the labels as key-value pairs.
125    fn encode_label_pairs(&self) -> Vec<LabelPair<'_>>;
126}
127
128/// Empty label set for metrics that don't need labels.
129#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
130pub struct NoLabels;
131
132impl EncodeLabelSet for NoLabels {
133    fn encode_label_pairs(&self) -> Vec<LabelPair<'_>> {
134        Vec::new()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::EncodeLabelSet;
142
143    #[test]
144    fn test_label_value_as_str() {
145        assert_eq!(LabelValue::from("hello").as_str(), "hello");
146        assert_eq!(LabelValue::from(42i64).as_str(), "42");
147        assert_eq!(LabelValue::from(42u64).as_str(), "42");
148        assert_eq!(LabelValue::from(true).as_str(), "true");
149        assert_eq!(LabelValue::from(false).as_str(), "false");
150    }
151
152    #[test]
153    fn test_no_labels() {
154        assert!(NoLabels.encode_label_pairs().is_empty());
155    }
156
157    #[test]
158    fn test_encode_label_value() {
159        // Strings must borrow (no per-scrape alloc).
160        let s = String::from("hello");
161        assert!(matches!(
162            s.encode_label_value(),
163            LabelValue::Str(Cow::Borrowed("hello"))
164        ));
165        assert!(matches!(42u64.encode_label_value(), LabelValue::Uint(42)));
166        assert!(matches!((-7i32).encode_label_value(), LabelValue::Int(-7)));
167        assert!(matches!(true.encode_label_value(), LabelValue::Bool(true)));
168    }
169
170    #[test]
171    fn test_derive_encode_label_set() {
172        // Covers field types, `name`, `skip`, and the borrowed-string guarantee.
173        #[derive(Clone, Hash, PartialEq, Eq, crate::EncodeLabelSet)]
174        struct MyLabels {
175            method: String,
176            kind: &'static str,
177            #[label(name = "status_code")]
178            status: u16,
179            count: i64,
180            #[label(skip)]
181            _internal: u64,
182        }
183
184        let labels = MyLabels {
185            method: "GET".into(),
186            kind: "x",
187            status: 200,
188            count: -3,
189            _internal: 999,
190        };
191        let pairs = labels.encode_label_pairs();
192        assert_eq!(pairs.len(), 4);
193        assert_eq!(pairs[0], ("method", LabelValue::from("GET")));
194        assert_eq!(pairs[1].0, "kind");
195        assert_eq!(pairs[1].1.as_str(), "x");
196        assert_eq!(pairs[2].0, "status_code");
197        assert_eq!(pairs[2].1.as_str(), "200");
198        assert_eq!(pairs[3].1.as_str(), "-3");
199        // String label must be borrowed.
200        assert!(matches!(&pairs[0].1, LabelValue::Str(Cow::Borrowed(_))));
201    }
202
203    #[test]
204    fn test_derive_encode_label_value_enum() {
205        // Default: snake_case for variants. `#[label(name = ...)]` overrides.
206        #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, crate::EncodeLabelValue)]
207        enum Kind {
208            Ipv4,
209            Ipv6,
210            #[label(name = "loopback")]
211            Local,
212        }
213
214        // Enum-level rename_all also works.
215        #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, crate::EncodeLabelValue)]
216        #[label(rename_all = "kebab-case")]
217        enum Mode {
218            FastPath,
219            SlowPath,
220        }
221
222        assert_eq!(Kind::Ipv4.encode_label_value().as_str(), "ipv4");
223        assert_eq!(Kind::Ipv6.encode_label_value().as_str(), "ipv6");
224        assert_eq!(Kind::Local.encode_label_value().as_str(), "loopback");
225        assert_eq!(Mode::FastPath.encode_label_value().as_str(), "fast-path");
226        assert_eq!(Mode::SlowPath.encode_label_value().as_str(), "slow-path");
227    }
228
229    #[test]
230    fn test_derive_rename_all() {
231        // `rename_all` applies to fields without explicit `#[label(name)]`.
232        #[derive(Clone, Hash, PartialEq, Eq, crate::EncodeLabelSet)]
233        #[label(rename_all = "kebab-case")]
234        struct Kebab {
235            method_name: String,
236            #[label(name = "literal")]
237            status_code: u16,
238        }
239        #[derive(Clone, Hash, PartialEq, Eq, crate::EncodeLabelSet)]
240        #[label(rename_all = "PascalCase")]
241        struct Pascal {
242            api_method: String,
243        }
244
245        let kebab = Kebab {
246            method_name: "GET".into(),
247            status_code: 200,
248        };
249        let k = kebab.encode_label_pairs();
250        assert_eq!(k[0].0, "method-name");
251        assert_eq!(k[1].0, "literal");
252
253        let pascal = Pascal {
254            api_method: "POST".into(),
255        };
256        let p = pascal.encode_label_pairs();
257        assert_eq!(p[0].0, "ApiMethod");
258    }
259}