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        let (sum, count) = self.sum_and_count();
298
299        output.push_str(name);
300        output.push_str("_sum ");
301        push_display(output, sum);
302        output.push('\n');
303
304        output.push_str(name);
305        output.push_str("_count ");
306        push_display(output, count);
307        output.push('\n');
308    }
309}
310
311impl<L: LabelEnum> PrometheusExport for LabeledCounter<L> {
312    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
313        write_labeled_counter_series::<L, _>(
314            output,
315            name,
316            help,
317            self.iter().map(|(label, count)| (label, count as u64)),
318        );
319    }
320}
321
322impl<L: LabelEnum> PrometheusExport for LabeledGauge<L> {
323    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
324        output.push_str("# HELP ");
325        output.push_str(name);
326        output.push(' ');
327        output.push_str(help);
328        output.push_str("\n# TYPE ");
329        output.push_str(name);
330        output.push_str(" gauge\n");
331
332        for (label, value) in self.iter() {
333            output.push_str(name);
334            output.push('{');
335            output.push_str(L::LABEL_NAME);
336            output.push_str("=\"");
337            output.push_str(label.variant_name());
338            output.push_str("\"} ");
339            push_display(output, value);
340            output.push('\n');
341        }
342    }
343}
344
345impl<L: LabelEnum> PrometheusExport for LabeledHistogram<L> {
346    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
347        write_labeled_histogram_series::<L, _>(output, name, help, self.iter());
348    }
349}
350
351impl<L: LabelEnum> PrometheusExport for LabeledSampledTimer<L> {
352    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
353        let calls_name = concat_two(name, "_calls");
354        let samples_name = concat_two(name, "_samples");
355        let calls_help = concat_two(help, " total calls");
356        let samples_help = concat_two(help, " sampled latency in nanoseconds");
357
358        write_labeled_counter_series::<L, _>(
359            output,
360            &calls_name,
361            &calls_help,
362            self.iter()
363                .map(|(label, calls, _)| (label, calls.sum() as u64)),
364        );
365        write_labeled_histogram_series::<L, _>(
366            output,
367            &samples_name,
368            &samples_help,
369            self.iter().map(|(label, _, histogram)| (label, histogram)),
370        );
371    }
372}
373
374impl PrometheusExport for DynamicCounter {
375    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
376        output.push_str("# HELP ");
377        output.push_str(name);
378        output.push(' ');
379        output.push_str(help);
380        output.push_str("\n# TYPE ");
381        output.push_str(name);
382        output.push_str(" counter\n");
383
384        self.visit_series(|labels, count| {
385            output.push_str(name);
386            output.push('{');
387            write_dynamic_labels(output, labels);
388            output.push_str("} ");
389            push_display(output, count);
390            output.push('\n');
391        });
392    }
393}
394
395impl PrometheusExport for DynamicGauge {
396    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
397        output.push_str("# HELP ");
398        output.push_str(name);
399        output.push(' ');
400        output.push_str(help);
401        output.push_str("\n# TYPE ");
402        output.push_str(name);
403        output.push_str(" gauge\n");
404
405        self.visit_series(|labels, value| {
406            output.push_str(name);
407            output.push('{');
408            write_dynamic_labels(output, labels);
409            output.push_str("} ");
410            push_display(output, value);
411            output.push('\n');
412        });
413    }
414}
415
416impl PrometheusExport for DynamicGaugeI64 {
417    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
418        output.push_str("# HELP ");
419        output.push_str(name);
420        output.push(' ');
421        output.push_str(help);
422        output.push_str("\n# TYPE ");
423        output.push_str(name);
424        output.push_str(" gauge\n");
425
426        self.visit_series(|labels, value| {
427            output.push_str(name);
428            output.push('{');
429            write_dynamic_labels(output, labels);
430            output.push_str("} ");
431            push_display(output, value);
432            output.push('\n');
433        });
434    }
435}
436
437impl PrometheusExport for DynamicHistogram {
438    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
439        output.push_str("# HELP ");
440        output.push_str(name);
441        output.push(' ');
442        output.push_str(help);
443        output.push_str("\n# TYPE ");
444        output.push_str(name);
445        output.push_str(" histogram\n");
446
447        self.visit_series(|labels, series| {
448            for (bound, bucket_count) in series.buckets_cumulative_iter() {
449                output.push_str(name);
450                output.push_str("_bucket{");
451                write_dynamic_labels(output, labels);
452                if !labels.is_empty() {
453                    output.push(',');
454                }
455                output.push_str("le=\"");
456                if bound == u64::MAX {
457                    output.push_str("+Inf");
458                } else {
459                    push_display(output, bound);
460                }
461                output.push_str("\"} ");
462                push_display(output, bucket_count);
463                output.push('\n');
464            }
465
466            output.push_str(name);
467            output.push_str("_sum{");
468            write_dynamic_labels(output, labels);
469            output.push_str("} ");
470            push_display(output, series.sum());
471            output.push('\n');
472
473            output.push_str(name);
474            output.push_str("_count{");
475            write_dynamic_labels(output, labels);
476            output.push_str("} ");
477            push_display(output, series.count());
478            output.push('\n');
479        });
480    }
481}
482
483impl PrometheusExport for DynamicDistribution {
484    /// Export distribution as summary (count + sum only, no quantiles).
485    fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
486        output.push_str("# HELP ");
487        output.push_str(name);
488        output.push(' ');
489        output.push_str(help);
490        output.push_str("\n# TYPE ");
491        output.push_str(name);
492        output.push_str(" summary\n");
493
494        self.visit_series(|labels, count, sum, _snap| {
495            output.push_str(name);
496            output.push_str("_sum{");
497            write_dynamic_labels(output, labels);
498            output.push_str("} ");
499            push_display(output, sum);
500            output.push('\n');
501
502            output.push_str(name);
503            output.push_str("_count{");
504            write_dynamic_labels(output, labels);
505            output.push_str("} ");
506            push_display(output, count);
507            output.push('\n');
508        });
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::PrometheusExport;
515    use crate::{Counter, Distribution, DynamicCounter, DynamicHistogram, Gauge, Histogram};
516
517    #[test]
518    fn test_prometheus_counter() {
519        let counter = Counter::new(4);
520        counter.inc();
521        counter.inc();
522
523        let mut output = String::new();
524        counter.export_prometheus(&mut output, "test_counter", "A test counter");
525
526        assert!(output.contains("# HELP test_counter A test counter"));
527        assert!(output.contains("# TYPE test_counter counter"));
528        assert!(output.contains("test_counter 2"));
529    }
530
531    #[test]
532    fn test_prometheus_gauge() {
533        let gauge = Gauge::new();
534        gauge.set(42);
535
536        let mut output = String::new();
537        gauge.export_prometheus(&mut output, "test_gauge", "A test gauge");
538
539        assert!(output.contains("# HELP test_gauge A test gauge"));
540        assert!(output.contains("# TYPE test_gauge gauge"));
541        assert!(output.contains("test_gauge 42"));
542    }
543
544    #[test]
545    fn test_prometheus_histogram() {
546        let histogram = Histogram::new(&[10, 100], 4);
547        histogram.record(5);
548        histogram.record(50);
549        histogram.record(500);
550
551        let mut output = String::new();
552        histogram.export_prometheus(&mut output, "test_hist", "A test histogram");
553
554        assert!(output.contains("# HELP test_hist A test histogram"));
555        assert!(output.contains("# TYPE test_hist histogram"));
556        assert!(output.contains("test_hist_bucket{le=\"10\"} 1"));
557        assert!(output.contains("test_hist_bucket{le=\"100\"} 2"));
558        assert!(output.contains("test_hist_bucket{le=\"+Inf\"} 3"));
559        assert!(output.contains("test_hist_count 3"));
560    }
561
562    #[test]
563    fn test_prometheus_distribution() {
564        let dist = Distribution::new(4);
565        dist.record(100);
566        dist.record(200);
567        dist.record(300);
568
569        let mut output = String::new();
570        dist.export_prometheus(&mut output, "latency", "Request latency");
571
572        assert!(output.contains("# HELP latency Request latency"));
573        assert!(output.contains("# TYPE latency summary"));
574        assert!(output.contains("latency_sum 600"));
575        assert!(output.contains("latency_count 3"));
576    }
577
578    #[test]
579    fn test_prometheus_dynamic_counter() {
580        let counter = DynamicCounter::new(4);
581        counter.add(&[("endpoint", "ep1"), ("method", "GET")], 3);
582
583        let mut output = String::new();
584        counter.export_prometheus(&mut output, "requests", "Requests by endpoint");
585
586        assert!(output.contains("# HELP requests Requests by endpoint"));
587        assert!(output.contains("# TYPE requests counter"));
588        assert!(output.contains("requests{endpoint=\"ep1\",method=\"GET\"} 3"));
589    }
590
591    #[test]
592    fn test_prometheus_dynamic_histogram() {
593        let h = DynamicHistogram::new(&[100], 4);
594        h.record(&[("endpoint", "ep1")], 50);
595        h.record(&[("endpoint", "ep1")], 150);
596
597        let mut output = String::new();
598        h.export_prometheus(&mut output, "latency", "Latency by endpoint");
599
600        assert!(output.contains("# TYPE latency histogram"));
601        assert!(output.contains("latency_bucket{endpoint=\"ep1\",le=\"100\"} 1"));
602        assert!(output.contains("latency_bucket{endpoint=\"ep1\",le=\"+Inf\"} 2"));
603        assert!(output.contains("latency_sum{endpoint=\"ep1\"} 200"));
604        assert!(output.contains("latency_count{endpoint=\"ep1\"} 2"));
605    }
606}