Skip to main content

textfile_metrics/
labels.rs

1// Copyright (c) Ted Kaplan. All Rights Reserved.
2// SPDX-License-Identifier: MIT
3
4//! Label handling for metrics.
5//!
6//! Labels are key-value pairs that add dimensions to metrics. They are sorted
7//! lexicographically and formatted according to Prometheus conventions.
8
9use std::fmt;
10
11/// Represents a collection of metric labels (key-value pairs).
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct Labels {
14    /// Sorted label pairs for consistent output.
15    pairs: Vec<(String, String)>,
16}
17
18impl Labels {
19    /// Create a new empty label set.
20    pub fn new() -> Self {
21        Self { pairs: Vec::new() }
22    }
23
24    /// Add a label to the set.
25    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
26        self.pairs.push((key.into(), value.into()));
27        self.pairs.sort_by(|a, b| a.0.cmp(&b.0));
28        self
29    }
30
31    /// Get the number of labels.
32    pub fn len(&self) -> usize {
33        self.pairs.len()
34    }
35
36    /// Check if there are any labels.
37    pub fn is_empty(&self) -> bool {
38        self.pairs.is_empty()
39    }
40
41    /// Check if a label key exists.
42    pub fn contains_key(&self, key: &str) -> bool {
43        self.pairs.iter().any(|(k, _)| k == key)
44    }
45
46    /// Get a label value by key.
47    pub fn get(&self, key: &str) -> Option<&str> {
48        self.pairs
49            .iter()
50            .find(|(k, _)| k == key)
51            .map(|(_, v)| v.as_str())
52    }
53
54    /// Iterate over label pairs.
55    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
56        self.pairs.iter().map(|(k, v)| (k.as_str(), v.as_str()))
57    }
58}
59
60impl From<Vec<(String, String)>> for Labels {
61    fn from(mut pairs: Vec<(String, String)>) -> Self {
62        // Validate and sort labels
63        pairs.sort_by(|a, b| a.0.cmp(&b.0));
64        Self { pairs }
65    }
66}
67
68impl From<Vec<(&str, &str)>> for Labels {
69    fn from(pairs: Vec<(&str, &str)>) -> Self {
70        let mut converted: Vec<(String, String)> = pairs
71            .into_iter()
72            .map(|(k, v)| (k.to_string(), v.to_string()))
73            .collect();
74        converted.sort_by(|a, b| a.0.cmp(&b.0));
75        Self { pairs: converted }
76    }
77}
78
79impl fmt::Display for Labels {
80    /// Format labels in Prometheus convention: `{key="value",key2="value2"}`
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        if self.pairs.is_empty() {
83            return Ok(());
84        }
85
86        write!(f, "{{")?;
87
88        for (i, (key, value)) in self.pairs.iter().enumerate() {
89            if i > 0 {
90                write!(f, ",")?;
91            }
92            // Escape quotes in values
93            let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
94            write!(f, "{}=\"{}\"", key, escaped)?;
95        }
96
97        write!(f, "}}")
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_labels_from_vec_of_strings() {
107        let labels = Labels::from(vec![
108            ("method".to_string(), "GET".to_string()),
109            ("status".to_string(), "200".to_string()),
110        ]);
111
112        assert_eq!(labels.len(), 2);
113        assert_eq!(labels.get("method"), Some("GET"));
114        assert_eq!(labels.get("status"), Some("200"));
115    }
116
117    #[test]
118    fn test_labels_from_vec_of_refs() {
119        let labels = Labels::from(vec![("method", "GET"), ("status", "200")]);
120
121        assert_eq!(labels.len(), 2);
122        assert_eq!(labels.get("method"), Some("GET"));
123    }
124
125    #[test]
126    fn test_labels_sorting() {
127        let labels = Labels::from(vec![
128            ("z".to_string(), "last".to_string()),
129            ("a".to_string(), "first".to_string()),
130            ("m".to_string(), "middle".to_string()),
131        ]);
132
133        let formatted = labels.to_string();
134        let a_pos = formatted.find("a=").unwrap();
135        let m_pos = formatted.find("m=").unwrap();
136        let z_pos = formatted.find("z=").unwrap();
137
138        assert!(a_pos < m_pos && m_pos < z_pos, "Labels should be sorted");
139    }
140
141    #[test]
142    fn test_labels_empty() {
143        let labels = Labels::new();
144        assert!(labels.is_empty());
145        assert_eq!(labels.to_string(), "");
146    }
147
148    #[test]
149    fn test_labels_with_quotes() {
150        let labels = Labels::from(vec![("key".to_string(), "value\"quoted\"".to_string())]);
151
152        let formatted = labels.to_string();
153        assert!(formatted.contains("\\\""));
154    }
155
156    #[test]
157    fn test_labels_with_backslash() {
158        let labels = Labels::from(vec![("path".to_string(), "C:\\Users\\test".to_string())]);
159
160        let formatted = labels.to_string();
161        assert!(formatted.contains("\\\\"));
162    }
163
164    #[test]
165    fn test_labels_contains_key() {
166        let labels = Labels::from(vec![("method".to_string(), "GET".to_string())]);
167
168        assert!(labels.contains_key("method"));
169        assert!(!labels.contains_key("status"));
170    }
171
172    #[test]
173    fn test_labels_iterator() {
174        let labels = Labels::from(vec![
175            ("a".to_string(), "1".to_string()),
176            ("b".to_string(), "2".to_string()),
177        ]);
178
179        let pairs: Vec<_> = labels.iter().collect();
180        assert_eq!(pairs, vec![("a", "1"), ("b", "2")]);
181    }
182}