fast_telemetry/export/text/
prometheus.rs1use crate::{
2 Counter, Distribution, DynamicCounter, DynamicDistribution, DynamicGauge, DynamicGaugeI64,
3 DynamicHistogram, Gauge, GaugeF64, Histogram, LabelEnum, LabeledCounter, LabeledGauge,
4 LabeledHistogram,
5};
6use std::fmt::Write as _;
7
8pub trait PrometheusExport {
10 fn export_prometheus(&self, output: &mut String, name: &str, help: &str);
16}
17
18fn push_display(output: &mut String, value: impl std::fmt::Display) {
19 let _ = write!(output, "{}", value);
20}
21
22fn write_dynamic_labels(output: &mut String, labels: &[(String, String)]) {
23 for (idx, (k, v)) in labels.iter().enumerate() {
24 if idx > 0 {
25 output.push(',');
26 }
27 output.push_str(k);
28 output.push_str("=\"");
29 output.push_str(v);
30 output.push('"');
31 }
32}
33
34impl PrometheusExport for Counter {
35 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
36 output.push_str("# HELP ");
37 output.push_str(name);
38 output.push(' ');
39 output.push_str(help);
40 output.push_str("\n# TYPE ");
41 output.push_str(name);
42 output.push_str(" counter\n");
43 output.push_str(name);
44 output.push(' ');
45 push_display(output, self.sum());
46 output.push('\n');
47 }
48}
49
50impl PrometheusExport for Gauge {
51 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
52 output.push_str("# HELP ");
53 output.push_str(name);
54 output.push(' ');
55 output.push_str(help);
56 output.push_str("\n# TYPE ");
57 output.push_str(name);
58 output.push_str(" gauge\n");
59 output.push_str(name);
60 output.push(' ');
61 push_display(output, self.get());
62 output.push('\n');
63 }
64}
65
66impl PrometheusExport for GaugeF64 {
67 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
68 output.push_str("# HELP ");
69 output.push_str(name);
70 output.push(' ');
71 output.push_str(help);
72 output.push_str("\n# TYPE ");
73 output.push_str(name);
74 output.push_str(" gauge\n");
75 output.push_str(name);
76 output.push(' ');
77 push_display(output, self.get());
78 output.push('\n');
79 }
80}
81
82impl PrometheusExport for Histogram {
83 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
84 output.push_str("# HELP ");
85 output.push_str(name);
86 output.push(' ');
87 output.push_str(help);
88 output.push_str("\n# TYPE ");
89 output.push_str(name);
90 output.push_str(" histogram\n");
91
92 for (bound, count) in self.buckets_cumulative() {
93 output.push_str(name);
94 output.push_str("_bucket{le=\"");
95 if bound == u64::MAX {
96 output.push_str("+Inf");
97 } else {
98 push_display(output, bound);
99 }
100 output.push_str("\"} ");
101 push_display(output, count);
102 output.push('\n');
103 }
104
105 output.push_str(name);
106 output.push_str("_sum ");
107 push_display(output, self.sum());
108 output.push('\n');
109
110 output.push_str(name);
111 output.push_str("_count ");
112 push_display(output, self.count());
113 output.push('\n');
114 }
115}
116
117impl PrometheusExport for Distribution {
118 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(" summary\n");
127
128 output.push_str(name);
129 output.push_str("_sum ");
130 push_display(output, self.sum());
131 output.push('\n');
132
133 output.push_str(name);
134 output.push_str("_count ");
135 push_display(output, self.count());
136 output.push('\n');
137 }
138}
139
140impl<L: LabelEnum> PrometheusExport for LabeledCounter<L> {
141 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
142 output.push_str("# HELP ");
143 output.push_str(name);
144 output.push(' ');
145 output.push_str(help);
146 output.push_str("\n# TYPE ");
147 output.push_str(name);
148 output.push_str(" counter\n");
149
150 for (label, count) in self.iter() {
151 output.push_str(name);
152 output.push('{');
153 output.push_str(L::LABEL_NAME);
154 output.push_str("=\"");
155 output.push_str(label.variant_name());
156 output.push_str("\"} ");
157 push_display(output, count);
158 output.push('\n');
159 }
160 }
161}
162
163impl<L: LabelEnum> PrometheusExport for LabeledGauge<L> {
164 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
165 output.push_str("# HELP ");
166 output.push_str(name);
167 output.push(' ');
168 output.push_str(help);
169 output.push_str("\n# TYPE ");
170 output.push_str(name);
171 output.push_str(" gauge\n");
172
173 for (label, value) in self.iter() {
174 output.push_str(name);
175 output.push('{');
176 output.push_str(L::LABEL_NAME);
177 output.push_str("=\"");
178 output.push_str(label.variant_name());
179 output.push_str("\"} ");
180 push_display(output, value);
181 output.push('\n');
182 }
183 }
184}
185
186impl<L: LabelEnum> PrometheusExport for LabeledHistogram<L> {
187 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
188 output.push_str("# HELP ");
189 output.push_str(name);
190 output.push(' ');
191 output.push_str(help);
192 output.push_str("\n# TYPE ");
193 output.push_str(name);
194 output.push_str(" histogram\n");
195
196 for (label, buckets, sum, count) in self.iter() {
197 let variant = label.variant_name();
198
199 for (bound, bucket_count) in buckets {
200 output.push_str(name);
201 output.push_str("_bucket{");
202 output.push_str(L::LABEL_NAME);
203 output.push_str("=\"");
204 output.push_str(variant);
205 output.push_str("\",le=\"");
206 if bound == u64::MAX {
207 output.push_str("+Inf");
208 } else {
209 push_display(output, bound);
210 }
211 output.push_str("\"} ");
212 push_display(output, bucket_count);
213 output.push('\n');
214 }
215
216 output.push_str(name);
217 output.push_str("_sum{");
218 output.push_str(L::LABEL_NAME);
219 output.push_str("=\"");
220 output.push_str(variant);
221 output.push_str("\"} ");
222 push_display(output, sum);
223 output.push('\n');
224
225 output.push_str(name);
226 output.push_str("_count{");
227 output.push_str(L::LABEL_NAME);
228 output.push_str("=\"");
229 output.push_str(variant);
230 output.push_str("\"} ");
231 push_display(output, count);
232 output.push('\n');
233 }
234 }
235}
236
237impl PrometheusExport for DynamicCounter {
238 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
239 output.push_str("# HELP ");
240 output.push_str(name);
241 output.push(' ');
242 output.push_str(help);
243 output.push_str("\n# TYPE ");
244 output.push_str(name);
245 output.push_str(" counter\n");
246
247 self.visit_series(|labels, count| {
248 output.push_str(name);
249 output.push('{');
250 write_dynamic_labels(output, labels);
251 output.push_str("} ");
252 push_display(output, count);
253 output.push('\n');
254 });
255 }
256}
257
258impl PrometheusExport for DynamicGauge {
259 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
260 output.push_str("# HELP ");
261 output.push_str(name);
262 output.push(' ');
263 output.push_str(help);
264 output.push_str("\n# TYPE ");
265 output.push_str(name);
266 output.push_str(" gauge\n");
267
268 self.visit_series(|labels, value| {
269 output.push_str(name);
270 output.push('{');
271 write_dynamic_labels(output, labels);
272 output.push_str("} ");
273 push_display(output, value);
274 output.push('\n');
275 });
276 }
277}
278
279impl PrometheusExport for DynamicGaugeI64 {
280 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
281 output.push_str("# HELP ");
282 output.push_str(name);
283 output.push(' ');
284 output.push_str(help);
285 output.push_str("\n# TYPE ");
286 output.push_str(name);
287 output.push_str(" gauge\n");
288
289 self.visit_series(|labels, value| {
290 output.push_str(name);
291 output.push('{');
292 write_dynamic_labels(output, labels);
293 output.push_str("} ");
294 push_display(output, value);
295 output.push('\n');
296 });
297 }
298}
299
300impl PrometheusExport for DynamicHistogram {
301 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
302 output.push_str("# HELP ");
303 output.push_str(name);
304 output.push(' ');
305 output.push_str(help);
306 output.push_str("\n# TYPE ");
307 output.push_str(name);
308 output.push_str(" histogram\n");
309
310 self.visit_series(|labels, series| {
311 for (bound, bucket_count) in series.buckets_cumulative_iter() {
312 output.push_str(name);
313 output.push_str("_bucket{");
314 write_dynamic_labels(output, labels);
315 if !labels.is_empty() {
316 output.push(',');
317 }
318 output.push_str("le=\"");
319 if bound == u64::MAX {
320 output.push_str("+Inf");
321 } else {
322 push_display(output, bound);
323 }
324 output.push_str("\"} ");
325 push_display(output, bucket_count);
326 output.push('\n');
327 }
328
329 output.push_str(name);
330 output.push_str("_sum{");
331 write_dynamic_labels(output, labels);
332 output.push_str("} ");
333 push_display(output, series.sum());
334 output.push('\n');
335
336 output.push_str(name);
337 output.push_str("_count{");
338 write_dynamic_labels(output, labels);
339 output.push_str("} ");
340 push_display(output, series.count());
341 output.push('\n');
342 });
343 }
344}
345
346impl PrometheusExport for DynamicDistribution {
347 fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
349 output.push_str("# HELP ");
350 output.push_str(name);
351 output.push(' ');
352 output.push_str(help);
353 output.push_str("\n# TYPE ");
354 output.push_str(name);
355 output.push_str(" summary\n");
356
357 self.visit_series(|labels, count, sum, _snap| {
358 output.push_str(name);
359 output.push_str("_sum{");
360 write_dynamic_labels(output, labels);
361 output.push_str("} ");
362 push_display(output, sum);
363 output.push('\n');
364
365 output.push_str(name);
366 output.push_str("_count{");
367 write_dynamic_labels(output, labels);
368 output.push_str("} ");
369 push_display(output, count);
370 output.push('\n');
371 });
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::PrometheusExport;
378 use crate::{Counter, Distribution, DynamicCounter, DynamicHistogram, Gauge, Histogram};
379
380 #[test]
381 fn test_prometheus_counter() {
382 let counter = Counter::new(4);
383 counter.inc();
384 counter.inc();
385
386 let mut output = String::new();
387 counter.export_prometheus(&mut output, "test_counter", "A test counter");
388
389 assert!(output.contains("# HELP test_counter A test counter"));
390 assert!(output.contains("# TYPE test_counter counter"));
391 assert!(output.contains("test_counter 2"));
392 }
393
394 #[test]
395 fn test_prometheus_gauge() {
396 let gauge = Gauge::new();
397 gauge.set(42);
398
399 let mut output = String::new();
400 gauge.export_prometheus(&mut output, "test_gauge", "A test gauge");
401
402 assert!(output.contains("# HELP test_gauge A test gauge"));
403 assert!(output.contains("# TYPE test_gauge gauge"));
404 assert!(output.contains("test_gauge 42"));
405 }
406
407 #[test]
408 fn test_prometheus_histogram() {
409 let histogram = Histogram::new(&[10, 100], 4);
410 histogram.record(5);
411 histogram.record(50);
412 histogram.record(500);
413
414 let mut output = String::new();
415 histogram.export_prometheus(&mut output, "test_hist", "A test histogram");
416
417 assert!(output.contains("# HELP test_hist A test histogram"));
418 assert!(output.contains("# TYPE test_hist histogram"));
419 assert!(output.contains("test_hist_bucket{le=\"10\"} 1"));
420 assert!(output.contains("test_hist_bucket{le=\"100\"} 2"));
421 assert!(output.contains("test_hist_bucket{le=\"+Inf\"} 3"));
422 assert!(output.contains("test_hist_count 3"));
423 }
424
425 #[test]
426 fn test_prometheus_distribution() {
427 let dist = Distribution::new(4);
428 dist.record(100);
429 dist.record(200);
430 dist.record(300);
431
432 let mut output = String::new();
433 dist.export_prometheus(&mut output, "latency", "Request latency");
434
435 assert!(output.contains("# HELP latency Request latency"));
436 assert!(output.contains("# TYPE latency summary"));
437 assert!(output.contains("latency_sum 600"));
438 assert!(output.contains("latency_count 3"));
439 }
440
441 #[test]
442 fn test_prometheus_dynamic_counter() {
443 let counter = DynamicCounter::new(4);
444 counter.add(&[("endpoint", "ep1"), ("method", "GET")], 3);
445
446 let mut output = String::new();
447 counter.export_prometheus(&mut output, "requests", "Requests by endpoint");
448
449 assert!(output.contains("# HELP requests Requests by endpoint"));
450 assert!(output.contains("# TYPE requests counter"));
451 assert!(output.contains("requests{endpoint=\"ep1\",method=\"GET\"} 3"));
452 }
453
454 #[test]
455 fn test_prometheus_dynamic_histogram() {
456 let h = DynamicHistogram::new(&[100], 4);
457 h.record(&[("endpoint", "ep1")], 50);
458 h.record(&[("endpoint", "ep1")], 150);
459
460 let mut output = String::new();
461 h.export_prometheus(&mut output, "latency", "Latency by endpoint");
462
463 assert!(output.contains("# TYPE latency histogram"));
464 assert!(output.contains("latency_bucket{endpoint=\"ep1\",le=\"100\"} 1"));
465 assert!(output.contains("latency_bucket{endpoint=\"ep1\",le=\"+Inf\"} 2"));
466 assert!(output.contains("latency_sum{endpoint=\"ep1\"} 200"));
467 assert!(output.contains("latency_count{endpoint=\"ep1\"} 2"));
468 }
469}