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<'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 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 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 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}