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