Skip to main content

cbtop/prometheus/
types.rs

1//! Prometheus metric types and values.
2
3/// Default max labels per metric
4pub const DEFAULT_MAX_LABELS: usize = 10;
5
6/// Default histogram buckets
7pub const DEFAULT_BUCKETS: [f64; 11] = [
8    0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
9];
10
11/// Metric type
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum MetricType {
14    /// Gauge (instantaneous value)
15    Gauge,
16    /// Counter (cumulative)
17    Counter,
18    /// Histogram (distribution)
19    Histogram,
20}
21
22impl MetricType {
23    /// Get type name for Prometheus format
24    pub fn name(&self) -> &'static str {
25        match self {
26            Self::Gauge => "gauge",
27            Self::Counter => "counter",
28            Self::Histogram => "histogram",
29        }
30    }
31}
32
33/// Metric labels
34#[derive(Debug, Clone, Default)]
35pub struct Labels {
36    pairs: Vec<(String, String)>,
37}
38
39impl Labels {
40    /// Create empty labels
41    pub fn new() -> Self {
42        Self { pairs: Vec::new() }
43    }
44
45    /// Add label
46    pub fn add(mut self, key: &str, value: &str) -> Self {
47        self.pairs.push((key.to_string(), value.to_string()));
48        self
49    }
50
51    /// Format as Prometheus label string
52    pub fn format(&self) -> String {
53        if self.pairs.is_empty() {
54            return String::new();
55        }
56
57        let parts: Vec<String> = self
58            .pairs
59            .iter()
60            .map(|(k, v)| format!("{}=\"{}\"", k, escape_label_value(v)))
61            .collect();
62
63        format!("{{{}}}", parts.join(","))
64    }
65
66    /// Get label count
67    pub fn len(&self) -> usize {
68        self.pairs.len()
69    }
70
71    /// Check if empty
72    pub fn is_empty(&self) -> bool {
73        self.pairs.is_empty()
74    }
75}
76
77/// Escape label value for Prometheus format
78pub fn escape_label_value(s: &str) -> String {
79    s.replace('\\', "\\\\")
80        .replace('"', "\\\"")
81        .replace('\n', "\\n")
82}
83
84/// Histogram buckets
85#[derive(Debug, Clone)]
86pub struct HistogramBuckets {
87    /// Bucket boundaries
88    pub boundaries: Vec<f64>,
89    /// Counts per bucket
90    pub counts: Vec<u64>,
91    /// Total sum
92    pub sum: f64,
93    /// Total count
94    pub count: u64,
95}
96
97impl Default for HistogramBuckets {
98    fn default() -> Self {
99        Self::with_buckets(&DEFAULT_BUCKETS)
100    }
101}
102
103impl HistogramBuckets {
104    /// Create with custom buckets
105    pub fn with_buckets(boundaries: &[f64]) -> Self {
106        Self {
107            boundaries: boundaries.to_vec(),
108            counts: vec![0; boundaries.len()],
109            sum: 0.0,
110            count: 0,
111        }
112    }
113
114    /// Observe a value
115    pub fn observe(&mut self, value: f64) {
116        self.sum += value;
117        self.count += 1;
118
119        for (i, &boundary) in self.boundaries.iter().enumerate() {
120            if value <= boundary {
121                self.counts[i] += 1;
122            }
123        }
124    }
125
126    /// Format as Prometheus histogram
127    pub fn format(&self, name: &str, labels: &Labels) -> String {
128        let mut lines = Vec::new();
129        let label_str = labels.format();
130
131        // Bucket lines
132        let mut cumulative = 0u64;
133        for (i, &boundary) in self.boundaries.iter().enumerate() {
134            cumulative += self.counts[i];
135            let bucket_label = if label_str.is_empty() {
136                format!("{{le=\"{}\"}}", boundary)
137            } else {
138                format!(
139                    "{{le=\"{}\",{}}}",
140                    boundary,
141                    &label_str[1..label_str.len() - 1]
142                )
143            };
144            lines.push(format!("{}_bucket{} {}", name, bucket_label, cumulative));
145        }
146
147        // +Inf bucket
148        let inf_label = if label_str.is_empty() {
149            "{le=\"+Inf\"}".to_string()
150        } else {
151            format!("{{le=\"+Inf\",{}}}", &label_str[1..label_str.len() - 1])
152        };
153        lines.push(format!("{}_bucket{} {}", name, inf_label, self.count));
154
155        // Sum and count
156        lines.push(format!("{}_sum{} {}", name, label_str, self.sum));
157        lines.push(format!("{}_count{} {}", name, label_str, self.count));
158
159        lines.join("\n")
160    }
161}
162
163/// Single metric definition
164#[derive(Debug, Clone)]
165pub struct MetricDef {
166    /// Metric name
167    pub name: String,
168    /// Help text
169    pub help: String,
170    /// Metric type
171    pub metric_type: MetricType,
172}
173
174impl MetricDef {
175    /// Create new metric definition
176    pub fn new(name: &str, help: &str, metric_type: MetricType) -> Self {
177        Self {
178            name: name.to_string(),
179            help: help.to_string(),
180            metric_type,
181        }
182    }
183
184    /// Format HELP line
185    pub fn format_help(&self) -> String {
186        format!("# HELP {} {}", self.name, self.help)
187    }
188
189    /// Format TYPE line
190    pub fn format_type(&self) -> String {
191        format!("# TYPE {} {}", self.name, self.metric_type.name())
192    }
193}
194
195/// Gauge metric value
196#[derive(Debug, Clone)]
197pub struct GaugeValue {
198    /// Value
199    pub value: f64,
200    /// Labels
201    pub labels: Labels,
202    /// Timestamp (optional, milliseconds)
203    pub timestamp: Option<u64>,
204}
205
206impl GaugeValue {
207    /// Create new gauge value
208    pub fn new(value: f64) -> Self {
209        Self {
210            value,
211            labels: Labels::new(),
212            timestamp: None,
213        }
214    }
215
216    /// With labels
217    pub fn with_labels(mut self, labels: Labels) -> Self {
218        self.labels = labels;
219        self
220    }
221
222    /// With timestamp
223    pub fn with_timestamp(mut self, ts: u64) -> Self {
224        self.timestamp = Some(ts);
225        self
226    }
227
228    /// Format as Prometheus line
229    pub fn format(&self, name: &str) -> String {
230        let label_str = self.labels.format();
231        let ts_str = self
232            .timestamp
233            .map(|t| format!(" {}", t))
234            .unwrap_or_default();
235        format!("{}{} {}{}", name, label_str, self.value, ts_str)
236    }
237}
238
239/// Counter metric value
240#[derive(Debug, Clone)]
241pub struct CounterValue {
242    /// Value (monotonically increasing)
243    pub value: u64,
244    /// Labels
245    pub labels: Labels,
246}
247
248impl CounterValue {
249    /// Create new counter
250    pub fn new(value: u64) -> Self {
251        Self {
252            value,
253            labels: Labels::new(),
254        }
255    }
256
257    /// With labels
258    pub fn with_labels(mut self, labels: Labels) -> Self {
259        self.labels = labels;
260        self
261    }
262
263    /// Format as Prometheus line
264    pub fn format(&self, name: &str) -> String {
265        let label_str = self.labels.format();
266        format!("{}{} {}", name, label_str, self.value)
267    }
268}
269
270/// Histogram metric value
271#[derive(Debug, Clone)]
272pub struct HistogramValue {
273    /// Buckets
274    pub buckets: HistogramBuckets,
275    /// Labels
276    pub labels: Labels,
277}
278
279impl HistogramValue {
280    /// Create new histogram
281    pub fn new() -> Self {
282        Self {
283            buckets: HistogramBuckets::default(),
284            labels: Labels::new(),
285        }
286    }
287
288    /// With custom buckets
289    pub fn with_buckets(boundaries: &[f64]) -> Self {
290        Self {
291            buckets: HistogramBuckets::with_buckets(boundaries),
292            labels: Labels::new(),
293        }
294    }
295
296    /// With labels
297    pub fn with_labels(mut self, labels: Labels) -> Self {
298        self.labels = labels;
299        self
300    }
301
302    /// Observe value
303    pub fn observe(&mut self, value: f64) {
304        self.buckets.observe(value);
305    }
306
307    /// Format as Prometheus lines
308    pub fn format(&self, name: &str) -> String {
309        self.buckets.format(name, &self.labels)
310    }
311}
312
313impl Default for HistogramValue {
314    fn default() -> Self {
315        Self::new()
316    }
317}
318
319/// Validate metric name (snake_case, alphanumeric + underscores)
320pub fn validate_metric_name(name: &str) -> bool {
321    if name.is_empty() {
322        return false;
323    }
324
325    let first = name.chars().next().expect("non-empty string");
326    if !first.is_ascii_lowercase() && first != '_' {
327        return false;
328    }
329
330    name.chars()
331        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
332}