Skip to main content

fast_telemetry/export/text/
prometheus.rs

1use crate::{
2    Counter, Distribution, DynamicCounter, DynamicDistribution, DynamicGauge, DynamicGaugeI64,
3    DynamicHistogram, Gauge, GaugeF64, Histogram, LabelEnum, LabeledCounter, LabeledGauge,
4    LabeledHistogram,
5};
6use std::fmt::Write as _;
7
8/// Trait for exporting a metric in Prometheus text exposition format.
9pub trait PrometheusExport {
10    /// Export this metric to the output string.
11    ///
12    /// - `output`: String buffer to append to
13    /// - `name`: The metric name (with prefix already applied)
14    /// - `help`: The help text for this metric
15    fn export_prometheus(&self, output: &mut String, name: &str, help: &str);
16}
17
18fn push_display(output: &mut String, value: impl std::fmt::Display) {
19    let _ = write!(output, "{}", value);
20}
21
22fn write_dynamic_labels(output: &mut String, labels: &[(String, String)]) {
23    for (idx, (k, v)) in labels.iter().enumerate() {
24        if idx > 0 {
25            output.push(',');
26        }
27        output.push_str(k);
28        output.push_str("=\"");
29        output.push_str(v);
30        output.push('"');
31    }
32}
33
34impl PrometheusExport for Counter {
35    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
36        output.push_str("# HELP ");
37        output.push_str(name);
38        output.push(' ');
39        output.push_str(help);
40        output.push_str("\n# TYPE ");
41        output.push_str(name);
42        output.push_str(" counter\n");
43        output.push_str(name);
44        output.push(' ');
45        push_display(output, self.sum());
46        output.push('\n');
47    }
48}
49
50impl PrometheusExport for Gauge {
51    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
52        output.push_str("# HELP ");
53        output.push_str(name);
54        output.push(' ');
55        output.push_str(help);
56        output.push_str("\n# TYPE ");
57        output.push_str(name);
58        output.push_str(" gauge\n");
59        output.push_str(name);
60        output.push(' ');
61        push_display(output, self.get());
62        output.push('\n');
63    }
64}
65
66impl PrometheusExport for GaugeF64 {
67    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
68        output.push_str("# HELP ");
69        output.push_str(name);
70        output.push(' ');
71        output.push_str(help);
72        output.push_str("\n# TYPE ");
73        output.push_str(name);
74        output.push_str(" gauge\n");
75        output.push_str(name);
76        output.push(' ');
77        push_display(output, self.get());
78        output.push('\n');
79    }
80}
81
82impl PrometheusExport for Histogram {
83    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
84        output.push_str("# HELP ");
85        output.push_str(name);
86        output.push(' ');
87        output.push_str(help);
88        output.push_str("\n# TYPE ");
89        output.push_str(name);
90        output.push_str(" histogram\n");
91
92        for (bound, count) in self.buckets_cumulative() {
93            output.push_str(name);
94            output.push_str("_bucket{le=\"");
95            if bound == u64::MAX {
96                output.push_str("+Inf");
97            } else {
98                push_display(output, bound);
99            }
100            output.push_str("\"} ");
101            push_display(output, count);
102            output.push('\n');
103        }
104
105        output.push_str(name);
106        output.push_str("_sum ");
107        push_display(output, self.sum());
108        output.push('\n');
109
110        output.push_str(name);
111        output.push_str("_count ");
112        push_display(output, self.count());
113        output.push('\n');
114    }
115}
116
117impl PrometheusExport for Distribution {
118    /// Export distribution as summary (count + sum only, no quantiles).
119    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
120        output.push_str("# HELP ");
121        output.push_str(name);
122        output.push(' ');
123        output.push_str(help);
124        output.push_str("\n# TYPE ");
125        output.push_str(name);
126        output.push_str(" summary\n");
127
128        output.push_str(name);
129        output.push_str("_sum ");
130        push_display(output, self.sum());
131        output.push('\n');
132
133        output.push_str(name);
134        output.push_str("_count ");
135        push_display(output, self.count());
136        output.push('\n');
137    }
138}
139
140impl<L: LabelEnum> PrometheusExport for LabeledCounter<L> {
141    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
142        output.push_str("# HELP ");
143        output.push_str(name);
144        output.push(' ');
145        output.push_str(help);
146        output.push_str("\n# TYPE ");
147        output.push_str(name);
148        output.push_str(" counter\n");
149
150        for (label, count) in self.iter() {
151            output.push_str(name);
152            output.push('{');
153            output.push_str(L::LABEL_NAME);
154            output.push_str("=\"");
155            output.push_str(label.variant_name());
156            output.push_str("\"} ");
157            push_display(output, count);
158            output.push('\n');
159        }
160    }
161}
162
163impl<L: LabelEnum> PrometheusExport for LabeledGauge<L> {
164    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
165        output.push_str("# HELP ");
166        output.push_str(name);
167        output.push(' ');
168        output.push_str(help);
169        output.push_str("\n# TYPE ");
170        output.push_str(name);
171        output.push_str(" gauge\n");
172
173        for (label, value) in self.iter() {
174            output.push_str(name);
175            output.push('{');
176            output.push_str(L::LABEL_NAME);
177            output.push_str("=\"");
178            output.push_str(label.variant_name());
179            output.push_str("\"} ");
180            push_display(output, value);
181            output.push('\n');
182        }
183    }
184}
185
186impl<L: LabelEnum> PrometheusExport for LabeledHistogram<L> {
187    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
188        output.push_str("# HELP ");
189        output.push_str(name);
190        output.push(' ');
191        output.push_str(help);
192        output.push_str("\n# TYPE ");
193        output.push_str(name);
194        output.push_str(" histogram\n");
195
196        for (label, buckets, sum, count) in self.iter() {
197            let variant = label.variant_name();
198
199            for (bound, bucket_count) in buckets {
200                output.push_str(name);
201                output.push_str("_bucket{");
202                output.push_str(L::LABEL_NAME);
203                output.push_str("=\"");
204                output.push_str(variant);
205                output.push_str("\",le=\"");
206                if bound == u64::MAX {
207                    output.push_str("+Inf");
208                } else {
209                    push_display(output, bound);
210                }
211                output.push_str("\"} ");
212                push_display(output, bucket_count);
213                output.push('\n');
214            }
215
216            output.push_str(name);
217            output.push_str("_sum{");
218            output.push_str(L::LABEL_NAME);
219            output.push_str("=\"");
220            output.push_str(variant);
221            output.push_str("\"} ");
222            push_display(output, sum);
223            output.push('\n');
224
225            output.push_str(name);
226            output.push_str("_count{");
227            output.push_str(L::LABEL_NAME);
228            output.push_str("=\"");
229            output.push_str(variant);
230            output.push_str("\"} ");
231            push_display(output, count);
232            output.push('\n');
233        }
234    }
235}
236
237impl PrometheusExport for DynamicCounter {
238    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
239        output.push_str("# HELP ");
240        output.push_str(name);
241        output.push(' ');
242        output.push_str(help);
243        output.push_str("\n# TYPE ");
244        output.push_str(name);
245        output.push_str(" counter\n");
246
247        self.visit_series(|labels, count| {
248            output.push_str(name);
249            output.push('{');
250            write_dynamic_labels(output, labels);
251            output.push_str("} ");
252            push_display(output, count);
253            output.push('\n');
254        });
255    }
256}
257
258impl PrometheusExport for DynamicGauge {
259    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
260        output.push_str("# HELP ");
261        output.push_str(name);
262        output.push(' ');
263        output.push_str(help);
264        output.push_str("\n# TYPE ");
265        output.push_str(name);
266        output.push_str(" gauge\n");
267
268        self.visit_series(|labels, value| {
269            output.push_str(name);
270            output.push('{');
271            write_dynamic_labels(output, labels);
272            output.push_str("} ");
273            push_display(output, value);
274            output.push('\n');
275        });
276    }
277}
278
279impl PrometheusExport for DynamicGaugeI64 {
280    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
281        output.push_str("# HELP ");
282        output.push_str(name);
283        output.push(' ');
284        output.push_str(help);
285        output.push_str("\n# TYPE ");
286        output.push_str(name);
287        output.push_str(" gauge\n");
288
289        self.visit_series(|labels, value| {
290            output.push_str(name);
291            output.push('{');
292            write_dynamic_labels(output, labels);
293            output.push_str("} ");
294            push_display(output, value);
295            output.push('\n');
296        });
297    }
298}
299
300impl PrometheusExport for DynamicHistogram {
301    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
302        output.push_str("# HELP ");
303        output.push_str(name);
304        output.push(' ');
305        output.push_str(help);
306        output.push_str("\n# TYPE ");
307        output.push_str(name);
308        output.push_str(" histogram\n");
309
310        self.visit_series(|labels, series| {
311            for (bound, bucket_count) in series.buckets_cumulative_iter() {
312                output.push_str(name);
313                output.push_str("_bucket{");
314                write_dynamic_labels(output, labels);
315                if !labels.is_empty() {
316                    output.push(',');
317                }
318                output.push_str("le=\"");
319                if bound == u64::MAX {
320                    output.push_str("+Inf");
321                } else {
322                    push_display(output, bound);
323                }
324                output.push_str("\"} ");
325                push_display(output, bucket_count);
326                output.push('\n');
327            }
328
329            output.push_str(name);
330            output.push_str("_sum{");
331            write_dynamic_labels(output, labels);
332            output.push_str("} ");
333            push_display(output, series.sum());
334            output.push('\n');
335
336            output.push_str(name);
337            output.push_str("_count{");
338            write_dynamic_labels(output, labels);
339            output.push_str("} ");
340            push_display(output, series.count());
341            output.push('\n');
342        });
343    }
344}
345
346impl PrometheusExport for DynamicDistribution {
347    /// Export distribution as summary (count + sum only, no quantiles).
348    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
349        output.push_str("# HELP ");
350        output.push_str(name);
351        output.push(' ');
352        output.push_str(help);
353        output.push_str("\n# TYPE ");
354        output.push_str(name);
355        output.push_str(" summary\n");
356
357        self.visit_series(|labels, count, sum, _snap| {
358            output.push_str(name);
359            output.push_str("_sum{");
360            write_dynamic_labels(output, labels);
361            output.push_str("} ");
362            push_display(output, sum);
363            output.push('\n');
364
365            output.push_str(name);
366            output.push_str("_count{");
367            write_dynamic_labels(output, labels);
368            output.push_str("} ");
369            push_display(output, count);
370            output.push('\n');
371        });
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::PrometheusExport;
378    use crate::{Counter, Distribution, DynamicCounter, DynamicHistogram, Gauge, Histogram};
379
380    #[test]
381    fn test_prometheus_counter() {
382        let counter = Counter::new(4);
383        counter.inc();
384        counter.inc();
385
386        let mut output = String::new();
387        counter.export_prometheus(&mut output, "test_counter", "A test counter");
388
389        assert!(output.contains("# HELP test_counter A test counter"));
390        assert!(output.contains("# TYPE test_counter counter"));
391        assert!(output.contains("test_counter 2"));
392    }
393
394    #[test]
395    fn test_prometheus_gauge() {
396        let gauge = Gauge::new();
397        gauge.set(42);
398
399        let mut output = String::new();
400        gauge.export_prometheus(&mut output, "test_gauge", "A test gauge");
401
402        assert!(output.contains("# HELP test_gauge A test gauge"));
403        assert!(output.contains("# TYPE test_gauge gauge"));
404        assert!(output.contains("test_gauge 42"));
405    }
406
407    #[test]
408    fn test_prometheus_histogram() {
409        let histogram = Histogram::new(&[10, 100], 4);
410        histogram.record(5);
411        histogram.record(50);
412        histogram.record(500);
413
414        let mut output = String::new();
415        histogram.export_prometheus(&mut output, "test_hist", "A test histogram");
416
417        assert!(output.contains("# HELP test_hist A test histogram"));
418        assert!(output.contains("# TYPE test_hist histogram"));
419        assert!(output.contains("test_hist_bucket{le=\"10\"} 1"));
420        assert!(output.contains("test_hist_bucket{le=\"100\"} 2"));
421        assert!(output.contains("test_hist_bucket{le=\"+Inf\"} 3"));
422        assert!(output.contains("test_hist_count 3"));
423    }
424
425    #[test]
426    fn test_prometheus_distribution() {
427        let dist = Distribution::new(4);
428        dist.record(100);
429        dist.record(200);
430        dist.record(300);
431
432        let mut output = String::new();
433        dist.export_prometheus(&mut output, "latency", "Request latency");
434
435        assert!(output.contains("# HELP latency Request latency"));
436        assert!(output.contains("# TYPE latency summary"));
437        assert!(output.contains("latency_sum 600"));
438        assert!(output.contains("latency_count 3"));
439    }
440
441    #[test]
442    fn test_prometheus_dynamic_counter() {
443        let counter = DynamicCounter::new(4);
444        counter.add(&[("endpoint", "ep1"), ("method", "GET")], 3);
445
446        let mut output = String::new();
447        counter.export_prometheus(&mut output, "requests", "Requests by endpoint");
448
449        assert!(output.contains("# HELP requests Requests by endpoint"));
450        assert!(output.contains("# TYPE requests counter"));
451        assert!(output.contains("requests{endpoint=\"ep1\",method=\"GET\"} 3"));
452    }
453
454    #[test]
455    fn test_prometheus_dynamic_histogram() {
456        let h = DynamicHistogram::new(&[100], 4);
457        h.record(&[("endpoint", "ep1")], 50);
458        h.record(&[("endpoint", "ep1")], 150);
459
460        let mut output = String::new();
461        h.export_prometheus(&mut output, "latency", "Latency by endpoint");
462
463        assert!(output.contains("# TYPE latency histogram"));
464        assert!(output.contains("latency_bucket{endpoint=\"ep1\",le=\"100\"} 1"));
465        assert!(output.contains("latency_bucket{endpoint=\"ep1\",le=\"+Inf\"} 2"));
466        assert!(output.contains("latency_sum{endpoint=\"ep1\"} 200"));
467        assert!(output.contains("latency_count{endpoint=\"ep1\"} 2"));
468    }
469}