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