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
9pub trait PrometheusExport {
11 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 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 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}