1use prometheus::{
2 Gauge, GaugeVec, Histogram, HistogramOpts, HistogramTimer, HistogramVec, IntCounter, IntCounterVec, Opts,
3 TextEncoder, core::Collector,
4};
5
6pub fn gather_all_metrics() -> prometheus::Result<String> {
8 let families = prometheus::gather();
9
10 let encoder = TextEncoder::new();
11 encoder.encode_to_string(&families)
12}
13
14fn register_metric<M, C>(name: &str, desc: &str, creator: C) -> prometheus::Result<M>
15where
16 M: Clone + Collector + 'static,
17 C: Fn(Opts) -> prometheus::Result<M>,
18{
19 let metric = creator(Opts::new(name, desc))?;
20
21 prometheus::register(Box::new(metric.clone()))?;
22
23 Ok(metric)
24}
25
26fn register_metric_vec<M, C>(name: &str, desc: &str, labels: &[&str], creator: C) -> prometheus::Result<M>
27where
28 M: Clone + Collector + 'static,
29 C: Fn(Opts, &[&str]) -> prometheus::Result<M>,
30{
31 if labels.is_empty() {
32 return Err(prometheus::Error::Msg(
33 "at least a single label must be specified".into(),
34 ));
35 }
36
37 let metric = creator(Opts::new(name, desc), labels)?;
38
39 prometheus::register(Box::new(metric.clone()))?;
40
41 Ok(metric)
42}
43
44pub struct SimpleCounter {
47 name: String,
48 ctr: IntCounter,
49}
50
51impl SimpleCounter {
52 pub fn get(&self) -> u64 {
54 self.ctr.get()
55 }
56
57 pub fn increment_by(&self, by: u64) {
59 self.ctr.inc_by(by)
60 }
61
62 pub fn reset(&self) {
64 self.ctr.reset()
65 }
66
67 pub fn increment(&self) {
69 self.increment_by(1)
70 }
71
72 pub fn name(&self) -> String {
74 self.name.clone()
75 }
76}
77
78impl SimpleCounter {
79 pub fn new(name: &str, description: &str) -> prometheus::Result<Self> {
81 register_metric(name, description, IntCounter::with_opts).map(|m| Self {
82 name: name.to_string(),
83 ctr: m,
84 })
85 }
86}
87
88pub struct MultiCounter {
91 name: String,
92 labels: Vec<String>,
93 ctr: IntCounterVec,
94}
95
96impl MultiCounter {
97 pub fn name(&self) -> String {
99 self.name.clone()
100 }
101}
102
103impl MultiCounter {
104 pub fn new(name: &str, description: &str, labels: &[&str]) -> prometheus::Result<Self> {
106 register_metric_vec(name, description, labels, IntCounterVec::new).map(|m| Self {
107 name: name.to_string(),
108 labels: Vec::from(labels).iter().map(|s| String::from(*s)).collect(),
109 ctr: m,
110 })
111 }
112
113 pub fn increment_by(&self, label_values: &[&str], by: u64) {
115 if let Ok(c) = self.ctr.get_metric_with_label_values(label_values) {
116 c.inc_by(by)
117 }
118 }
119
120 pub fn increment(&self, label_values: &[&str]) {
122 self.increment_by(label_values, 1)
123 }
124
125 pub fn reset(&self) {
127 self.ctr.reset();
128 }
129
130 pub fn get(&self, label_values: &[&str]) -> Option<u64> {
132 self.ctr
133 .get_metric_with_label_values(label_values)
134 .map(|c| c.get())
135 .ok()
136 }
137
138 pub fn labels(&self) -> Vec<&str> {
140 self.labels.iter().map(String::as_str).collect()
141 }
142}
143
144#[derive(Debug)]
147pub struct SimpleGauge {
148 name: String,
149 gg: Gauge,
150}
151
152impl SimpleGauge {
153 pub fn increment(&self, by: f64) {
155 self.gg.add(by)
156 }
157
158 pub fn decrement(&self, by: f64) {
160 self.gg.sub(by)
161 }
162
163 pub fn set(&self, value: f64) {
165 self.gg.set(value)
166 }
167
168 pub fn get(&self) -> f64 {
170 self.gg.get()
171 }
172
173 pub fn name(&self) -> String {
175 self.name.clone()
176 }
177}
178
179impl SimpleGauge {
180 pub fn new(name: &str, description: &str) -> prometheus::Result<Self> {
182 let m = register_metric(name, description, Gauge::with_opts).map(|m| Self {
183 name: name.to_string(),
184 gg: m,
185 })?;
186 m.set(0_f64);
187 Ok(m)
188 }
189}
190
191#[derive(Debug)]
194pub struct MultiGauge {
195 name: String,
196 labels: Vec<String>,
197 ctr: GaugeVec,
198}
199
200impl MultiGauge {
201 pub fn name(&self) -> String {
203 self.name.clone()
204 }
205}
206
207impl MultiGauge {
208 pub fn new(name: &str, description: &str, labels: &[&str]) -> prometheus::Result<Self> {
210 register_metric_vec(name, description, labels, GaugeVec::new).map(|m| Self {
211 name: name.to_string(),
212 labels: Vec::from(labels).iter().map(|s| String::from(*s)).collect(),
213 ctr: m,
214 })
215 }
216
217 pub fn increment(&self, label_values: &[&str], by: f64) {
219 if let Ok(c) = self.ctr.get_metric_with_label_values(label_values) {
220 c.add(by)
221 }
222 }
223
224 pub fn decrement(&self, label_values: &[&str], by: f64) {
226 if let Ok(c) = self.ctr.get_metric_with_label_values(label_values) {
227 c.sub(by)
228 }
229 }
230
231 pub fn set(&self, label_values: &[&str], value: f64) {
233 if let Ok(c) = self.ctr.get_metric_with_label_values(label_values) {
234 c.set(value)
235 }
236 }
237
238 pub fn get(&self, label_values: &[&str]) -> Option<f64> {
240 self.ctr
241 .get_metric_with_label_values(label_values)
242 .map(|c| c.get())
243 .ok()
244 }
245
246 pub fn labels(&self) -> Vec<&str> {
248 self.labels.iter().map(String::as_str).collect()
249 }
250}
251
252#[cfg(any(not(feature = "js"), test))]
253#[macro_export]
254macro_rules! histogram_start_measure {
255 ($v:ident) => {
257 $v.start_measure()
258 };
259 ($v:ident, $l:expr) => {
261 $v.start_measure($l)
262 };
263}
264
265enum TimerVariant {
266 Native(HistogramTimer),
267 #[cfg(feature = "js")]
268 Wasm {
269 start_ts: f64,
270 new_ts: fn() -> f64,
271 labels: Vec<String>,
272 },
273}
274
275pub struct SimpleTimer {
277 inner: TimerVariant,
278}
279
280pub struct SimpleHistogram {
283 name: String,
284 hh: Histogram,
285}
286
287impl SimpleHistogram {
288 pub fn observe(&self, value: f64) {
290 self.hh.observe(value)
291 }
292
293 pub fn record_measure(&self, timer: SimpleTimer) {
295 match timer.inner {
296 TimerVariant::Native(timer) => timer.observe_duration(),
297 #[cfg(feature = "js")]
298 TimerVariant::Wasm { start_ts, new_ts, .. } => self.hh.observe(new_ts() - start_ts),
299 }
300 }
301
302 pub fn cancel_measure(&self, timer: SimpleTimer) -> f64 {
304 match timer.inner {
305 TimerVariant::Native(timer) => timer.stop_and_discard(),
306 #[cfg(feature = "js")]
307 TimerVariant::Wasm { start_ts, new_ts, .. } => new_ts() - start_ts,
308 }
309 }
310
311 pub fn get_sample_count(&self) -> u64 {
313 self.hh.get_sample_count()
314 }
315
316 pub fn get_sample_sum(&self) -> f64 {
318 self.hh.get_sample_sum()
319 }
320
321 pub fn name(&self) -> String {
323 self.name.clone()
324 }
325}
326
327impl SimpleHistogram {
328 pub fn new(name: &str, description: &str, buckets: Vec<f64>) -> prometheus::Result<Self> {
332 let mut opts = HistogramOpts::new(name, description);
333 if !buckets.is_empty() {
334 opts = opts.buckets(buckets);
335 }
336
337 let metric = Histogram::with_opts(opts)?;
338
339 prometheus::register(Box::new(metric.clone()))?;
340
341 Ok(Self {
342 name: name.to_string(),
343 hh: metric,
344 })
345 }
346
347 pub fn start_measure(&self) -> SimpleTimer {
349 SimpleTimer {
350 inner: TimerVariant::Native(self.hh.start_timer()),
351 }
352 }
353}
354
355pub struct MultiHistogram {
358 name: String,
359 labels: Vec<String>,
360 hh: HistogramVec,
361}
362
363impl MultiHistogram {
364 pub fn record_measure(&self, timer: SimpleTimer) {
366 match timer.inner {
367 TimerVariant::Native(timer) => timer.observe_duration(),
368 #[cfg(feature = "js")]
369 TimerVariant::Wasm {
370 start_ts,
371 new_ts,
372 labels,
373 } => {
374 if let Ok(h) = self
375 .hh
376 .get_metric_with_label_values(&labels.iter().map(String::as_str).collect::<Vec<&str>>())
377 {
378 h.observe(new_ts() - start_ts)
379 }
380 }
381 }
382 }
383
384 pub fn cancel_measure(&self, timer: SimpleTimer) -> f64 {
386 match timer.inner {
387 TimerVariant::Native(timer) => timer.stop_and_discard(),
388 #[cfg(feature = "js")]
389 TimerVariant::Wasm { start_ts, new_ts, .. } => new_ts() - start_ts,
390 }
391 }
392
393 pub fn name(&self) -> String {
395 self.name.clone()
396 }
397}
398
399impl MultiHistogram {
400 pub fn new(name: &str, description: &str, buckets: Vec<f64>, labels: &[&str]) -> prometheus::Result<Self> {
404 let mut opts = HistogramOpts::new(name, description);
405 if !buckets.is_empty() {
406 opts = opts.buckets(buckets);
407 }
408
409 let metric = HistogramVec::new(opts, labels)?;
410
411 prometheus::register(Box::new(metric.clone()))?;
412
413 Ok(Self {
414 name: name.to_string(),
415 labels: Vec::from(labels).iter().map(|s| String::from(*s)).collect(),
416 hh: metric,
417 })
418 }
419
420 pub fn start_measure(&self, label_values: &[&str]) -> prometheus::Result<SimpleTimer> {
422 self.hh.get_metric_with_label_values(label_values).map(|h| SimpleTimer {
423 inner: TimerVariant::Native(h.start_timer()),
424 })
425 }
426
427 pub fn observe(&self, label_values: &[&str], value: f64) {
429 if let Ok(c) = self.hh.get_metric_with_label_values(label_values) {
430 c.observe(value)
431 }
432 }
433
434 pub fn get_sample_count(&self, label_values: &[&str]) -> Option<u64> {
436 self.hh
437 .get_metric_with_label_values(label_values)
438 .map(|c| c.get_sample_count())
439 .ok()
440 }
441
442 pub fn get_sample_sum(&self, label_values: &[&str]) -> Option<f64> {
444 self.hh
445 .get_metric_with_label_values(label_values)
446 .map(|c| c.get_sample_sum())
447 .ok()
448 }
449
450 pub fn labels(&self) -> Vec<&str> {
452 self.labels.iter().map(String::as_str).collect()
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use anyhow::Context;
459
460 use super::*;
461
462 #[test]
463 fn test_counter() -> anyhow::Result<()> {
464 let counter = SimpleCounter::new("my_ctr", "test counter")?;
465
466 assert_eq!("my_ctr", counter.name());
467
468 counter.increment();
469
470 assert_eq!(1, counter.get());
471
472 let metrics = gather_all_metrics()?;
473 assert!(metrics.contains("my_ctr 1"));
474
475 Ok(())
476 }
477
478 #[test]
479 fn test_multi_counter() -> anyhow::Result<()> {
480 let counter = MultiCounter::new("my_mctr", "test multicounter", &["version"])?;
481
482 assert_eq!("my_mctr", counter.name());
483 assert!(counter.labels().contains(&"version"));
484
485 counter.increment_by(&["1.90.1"], 10);
486 counter.increment_by(&["1.89.20"], 1);
487 counter.increment_by(&["1.90.1"], 15);
488
489 assert_eq!(25, counter.get(&["1.90.1"]).context("should be present")?);
490 assert_eq!(1, counter.get(&["1.89.20"]).context("should be present")?);
491
492 let metrics = gather_all_metrics()?;
493 assert!(metrics.contains("my_mctr{version=\"1.90.1\"} 25"));
494 assert!(metrics.contains("my_mctr{version=\"1.89.20\"} 1"));
495
496 Ok(())
497 }
498
499 #[test]
500 fn test_gauge() -> anyhow::Result<()> {
501 let gauge = SimpleGauge::new("my_gauge", "test gauge")?;
502
503 assert_eq!("my_gauge", gauge.name());
504
505 gauge.increment(10.0);
506
507 assert_eq!(10.0, gauge.get());
508
509 let metrics = gather_all_metrics()?;
510 assert!(metrics.contains("my_gauge 10"));
511
512 gauge.decrement(5.1);
513
514 assert_eq!(4.9, gauge.get());
515
516 let metrics2 = gather_all_metrics()?;
517 assert!(metrics2.contains("my_gauge 4.9"));
518
519 Ok(())
520 }
521
522 #[test]
523 fn test_multi_gauge() -> anyhow::Result<()> {
524 let gauge = MultiGauge::new("my_mgauge", "test multicounter", &["version"])?;
525
526 assert_eq!("my_mgauge", gauge.name());
527 assert!(gauge.labels().contains(&"version"));
528
529 gauge.increment(&["1.90.1"], 10.0);
530 gauge.increment(&["1.89.20"], 5.0);
531 gauge.increment(&["1.90.1"], 15.0);
532 gauge.decrement(&["1.89.20"], 2.0);
533
534 assert_eq!(25.0, gauge.get(&["1.90.1"]).context("should be present")?);
535 assert_eq!(3.0, gauge.get(&["1.89.20"]).context("should be present")?);
536
537 let metrics = gather_all_metrics()?;
538 assert!(metrics.contains("my_mgauge{version=\"1.90.1\"} 25"));
539 assert!(metrics.contains("my_mgauge{version=\"1.89.20\"} 3"));
540
541 Ok(())
542 }
543
544 #[test]
545 fn test_histogram() -> anyhow::Result<()> {
546 let histogram = SimpleHistogram::new("my_histogram", "test histogram", vec![1.0, 2.0, 3.0, 4.0, 5.0])?;
547
548 assert_eq!("my_histogram", histogram.name());
549
550 histogram.observe(2.0);
551 histogram.observe(2.0);
552 histogram.observe(1.0);
553 histogram.observe(5.0);
554
555 assert_eq!(4, histogram.get_sample_count());
556 assert_eq!(10.0, histogram.get_sample_sum());
557
558 let metrics = gather_all_metrics()?;
559 assert!(metrics.contains("my_histogram_bucket{le=\"1\"} 1"));
560 assert!(metrics.contains("my_histogram_bucket{le=\"2\"} 3"));
561 assert!(metrics.contains("my_histogram_bucket{le=\"3\"} 3"));
562 assert!(metrics.contains("my_histogram_bucket{le=\"4\"} 3"));
563 assert!(metrics.contains("my_histogram_bucket{le=\"5\"} 4"));
564
565 let timer = histogram_start_measure!(histogram);
566 histogram.cancel_measure(timer);
567
568 Ok(())
569 }
570
571 #[test]
572 fn test_multi_histogram() -> anyhow::Result<()> {
573 let histogram = MultiHistogram::new(
574 "my_mhistogram",
575 "test histogram",
576 vec![1.0, 2.0, 3.0, 4.0, 5.0],
577 &["version"],
578 )?;
579
580 assert_eq!("my_mhistogram", histogram.name());
581 assert!(histogram.labels().contains(&"version"));
582
583 histogram.observe(&["1.90.0"], 2.0);
584 histogram.observe(&["1.90.0"], 2.0);
585 histogram.observe(&["1.90.0"], 1.0);
586 histogram.observe(&["1.90.0"], 5.0);
587 histogram.observe(&["1.89.20"], 10.0);
588
589 assert_eq!(
590 1,
591 histogram.get_sample_count(&["1.89.20"]).context("should be present")?
592 );
593 assert_eq!(
594 10.0,
595 histogram.get_sample_sum(&["1.89.20"]).context("should be present")?
596 );
597
598 assert_eq!(4, histogram.get_sample_count(&["1.90.0"]).context("should be present")?);
599 assert_eq!(
600 10.0,
601 histogram.get_sample_sum(&["1.90.0"]).context("should be present")?
602 );
603
604 let metrics = gather_all_metrics()?;
605 assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"1\"} 1"));
606 assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"2\"} 3"));
607 assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"3\"} 3"));
608 assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"4\"} 3"));
609 assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"5\"} 4"));
610
611 assert!(metrics.contains("my_mhistogram_bucket{version=\"1.89.20\",le=\"+Inf\"} 1"));
612
613 let timer = histogram_start_measure!(histogram, &["1.90.0"])?;
614 histogram.cancel_measure(timer);
615
616 Ok(())
617 }
618}