Skip to main content

fast_telemetry/export/text/
prometheus.rs

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