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