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