1use dashmap::DashMap;
2use std::fmt;
3use tracing::{debug, error, warn};
4
5use prometheus::{
6 Encoder as _, Gauge, Histogram, HistogramOpts, IntCounter, IntGauge, Registry, TextEncoder,
7 core::Collector,
8};
9
10pub use prometheus::HistogramTimer;
11pub use std::time::Instant;
12
13#[macro_export]
14macro_rules! prepend_package_name {
15 ($name: tt) => {
16 &format!(
17 "{}_{}",
18 std::module_path!()
19 .split("::")
20 .next()
21 .unwrap_or("x")
22 .to_string(),
23 $name
24 )
25 };
26}
27
28#[macro_export]
29macro_rules! inc_by {
30 ($name:literal, $x:expr, $help: expr) => {
31 $crate::REGISTRY.maybe_register_and_inc_by(
32 $crate::prepend_package_name!($name),
33 $x as i64,
34 $help,
35 );
36 };
37 ($name:literal, $x:expr) => {
38 $crate::REGISTRY.maybe_register_and_inc_by(
39 $crate::prepend_package_name!($name),
40 $x as i64,
41 None,
42 );
43 };
44}
45
46#[macro_export]
47macro_rules! inc {
48 ($name:literal, $help: expr) => {
49 $crate::REGISTRY.maybe_register_and_inc($crate::prepend_package_name!($name), $help);
50 };
51 ($name:literal) => {
52 $crate::REGISTRY.maybe_register_and_inc($crate::prepend_package_name!($name), None);
53 };
54}
55
56#[macro_export]
57macro_rules! metrics {
58 () => {
59 $crate::REGISTRY.to_string();
60 };
61}
62
63#[macro_export]
64macro_rules! set_metric {
65 ($name:literal, $x:expr, $help: expr) => {
66 $crate::REGISTRY.maybe_register_and_set(
67 $crate::prepend_package_name!($name),
68 $x as i64,
69 $help,
70 );
71 };
72 ($name:literal, $x:expr) => {
73 $crate::REGISTRY.maybe_register_and_set(
74 $crate::prepend_package_name!($name),
75 $x as i64,
76 None,
77 );
78 };
79}
80
81#[macro_export]
82macro_rules! set_metric_float {
83 ($name:literal, $x:expr, $help: expr) => {
84 $crate::REGISTRY.maybe_register_and_set_float(
85 $crate::prepend_package_name!($name),
86 $x as f64,
87 $help,
88 );
89 };
90 ($name:literal, $x:expr) => {
91 $crate::REGISTRY.maybe_register_and_set_float(
92 $crate::prepend_package_name!($name),
93 $x as f64,
94 None,
95 );
96 };
97}
98
99#[macro_export]
100macro_rules! add_histogram_obs {
101 ($name:expr, $x:expr, $b:expr, $help:expr) => {
102 $crate::REGISTRY.maybe_register_and_add_to_histogram(
103 $crate::prepend_package_name!($name),
104 $x as f64,
105 Some($b),
106 $help,
107 );
108 };
109
110 ($name:expr, $x:expr, $b:expr) => {
111 $crate::REGISTRY.maybe_register_and_add_to_histogram(
112 $crate::prepend_package_name!($name),
113 $x as f64,
114 Some($b),
115 None,
116 );
117 };
118 ($name:expr, $x:expr) => {
119 $crate::REGISTRY.maybe_register_and_add_to_histogram(
120 $crate::prepend_package_name!($name),
121 $x as f64,
122 None,
123 None,
124 );
125 };
126}
127
128#[macro_export]
129macro_rules! nanos {
130 ( $name:literal, $x:expr ) => {{
131 let start = $crate::Instant::now();
132 let r = $x;
134 let duration = start.elapsed().as_nanos() as i64;
135 let name = $crate::prepend_package_name!($name);
136 $crate::REGISTRY.maybe_register_and_inc_by(&format!("{}_nanos", $name), duration, None);
137 r
138 }};
139}
140
141lazy_static::lazy_static! {
142 pub static ref REGISTRY: MetricsController = MetricsController::default();
143}
144
145pub fn metrics_registry() -> &'static MetricsController {
146 ®ISTRY
147}
148
149#[derive(Default)]
150pub struct MetricsController {
151 registry: Registry,
152 registry_index: DashMap<String, Metric>,
153}
154
155pub enum Metric {
156 IntCounter(Box<IntCounter>),
157 IntGauge(Box<IntGauge>),
158 FloatGauge(Box<Gauge>),
159 Histogram(Box<Histogram>),
160}
161
162impl Metric {
163 pub fn new_int_counter(name: &str, help: &str) -> Option<Self> {
164 match IntCounter::new(sanitize_metric_name(name), help) {
165 Ok(c) => Some(c.into()),
166 Err(err) => {
167 error!("Failed to create counter {name:?}: {err}");
168 None
169 }
170 }
171 }
172
173 pub fn new_int_gauge(name: &str, help: &str) -> Option<Self> {
174 match IntGauge::new(sanitize_metric_name(name), help) {
175 Ok(g) => Some(g.into()),
176 Err(err) => {
177 error!("Failed to create gauge {name:?}: {err}");
178 None
179 }
180 }
181 }
182
183 pub fn new_float_gauge(name: &str, help: &str) -> Option<Self> {
184 match Gauge::new(sanitize_metric_name(name), help) {
185 Ok(g) => Some(g.into()),
186 Err(err) => {
187 error!("Failed to create gauge {name:?}: {err}");
188 None
189 }
190 }
191 }
192
193 pub fn new_histogram(name: &str, help: &str, buckets: Option<&[f64]>) -> Option<Self> {
194 let mut opts = HistogramOpts::new(sanitize_metric_name(name), help);
195 if let Some(buckets) = buckets {
196 opts = opts.buckets(buckets.to_vec())
197 }
198 match Histogram::with_opts(opts) {
199 Ok(h) => Some(Metric::Histogram(Box::new(h))),
200 Err(err) => {
201 error!("failed to create histogram {name:?}: {err}");
202 None
203 }
204 }
205 }
206
207 fn as_collector(&self) -> Box<dyn Collector> {
208 match self {
209 Metric::IntCounter(c) => c.clone(),
210 Metric::IntGauge(g) => g.clone(),
211 Metric::FloatGauge(g) => g.clone(),
212 Metric::Histogram(h) => h.clone(),
213 }
214 }
215}
216
217impl From<IntCounter> for Metric {
218 fn from(v: IntCounter) -> Self {
219 Metric::IntCounter(Box::new(v))
220 }
221}
222
223impl From<IntGauge> for Metric {
224 fn from(v: IntGauge) -> Self {
225 Metric::IntGauge(Box::new(v))
226 }
227}
228
229impl From<Gauge> for Metric {
230 fn from(v: Gauge) -> Self {
231 Metric::FloatGauge(Box::new(v))
232 }
233}
234
235impl From<Histogram> for Metric {
236 fn from(v: Histogram) -> Self {
237 Metric::Histogram(Box::new(v))
238 }
239}
240
241fn fq_name(c: &dyn Collector) -> String {
242 c.desc()
243 .first()
244 .map(|d| d.fq_name.clone())
245 .unwrap_or_default()
246}
247
248impl Metric {
249 #[inline(always)]
250 fn fq_name(&self) -> String {
251 match self {
252 Metric::IntCounter(c) => fq_name(c.as_ref()),
253 Metric::IntGauge(g) => fq_name(g.as_ref()),
254 Metric::FloatGauge(g) => fq_name(g.as_ref()),
255 Metric::Histogram(h) => fq_name(h.as_ref()),
256 }
257 }
258
259 #[inline(always)]
260 fn inc(&self) {
261 match self {
262 Metric::IntCounter(c) => c.inc(),
263 Metric::IntGauge(g) => g.inc(),
264 Metric::FloatGauge(g) => g.inc(),
265 Metric::Histogram(_) => {
266 warn!("invalid operation: attempted to call increment on a histogram")
267 }
268 }
269 }
270
271 #[inline(always)]
272 fn inc_by(&self, value: i64) {
273 match self {
274 Metric::IntCounter(c) => c.inc_by(value as u64),
275 Metric::IntGauge(g) => g.add(value),
276 Metric::FloatGauge(g) => {
277 warn!(
278 "attempted to increment a float gauge ('{}') by an integer - this is most likely a bug",
279 self.fq_name()
280 );
281 g.add(value as f64)
282 }
283 Metric::Histogram(_) => {
284 warn!("invalid operation: attempted to call increment on a histogram")
285 }
286 }
287 }
288
289 #[inline(always)]
290 fn set(&self, value: i64) {
291 match self {
292 Metric::IntCounter(_c) => {
293 warn!("Cannot set value for counter {:?}", self.fq_name());
294 }
295 Metric::IntGauge(g) => g.set(value),
296 Metric::FloatGauge(g) => {
297 warn!(
298 "attempted to set a float gauge ('{}') to an integer value - this is most likely a bug",
299 self.fq_name()
300 );
301 g.set(value as f64)
302 }
303 Metric::Histogram(_) => {
304 warn!("invalid operation: attempted to call set on a histogram")
305 }
306 }
307 }
308
309 #[inline(always)]
310 fn set_float(&self, value: f64) {
311 match self {
312 Metric::IntCounter(_c) => {
313 warn!("Cannot set value for counter {:?}", self.fq_name());
314 }
315 Metric::IntGauge(g) => {
316 warn!(
317 "attempted to set a integer gauge ('{}') to a float value - this is most likely a bug",
318 self.fq_name()
319 );
320 g.set(value as i64)
321 }
322 Metric::FloatGauge(g) => g.set(value),
323 Metric::Histogram(_) => {
324 warn!("invalid operation: attempted to call increment on a histogram")
325 }
326 }
327 }
328
329 #[inline(always)]
330 fn add_histogram_observation(&self, value: f64) {
331 match self {
332 Metric::Histogram(h) => {
333 h.observe(value);
334 }
335 _ => warn!("attempted to add histogram observation on a non-histogram metric"),
336 }
337 }
338
339 #[inline(always)]
340 fn start_timer(&self) -> Option<HistogramTimer> {
341 match self {
342 Metric::Histogram(h) => Some(h.start_timer()),
343 _ => {
344 warn!("attempted to start histogram observation on a non-histogram metric");
345 None
346 }
347 }
348 }
349}
350
351impl fmt::Display for MetricsController {
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 let metrics = self.gather();
354 let output = match String::from_utf8(metrics) {
355 Ok(output) => output,
356 Err(e) => return write!(f, "Error decoding metrics to String: {e}"),
357 };
358 write!(f, "{output}")
359 }
360}
361
362impl MetricsController {
363 #[inline(always)]
364 pub fn gather(&self) -> Vec<u8> {
365 let mut buffer = vec![];
366 let encoder = TextEncoder::new();
367 let metrics = self.registry.gather();
368 match encoder.encode(&metrics, &mut buffer) {
369 Ok(_) => {}
370 Err(e) => error!("Error encoding metrics to buffer: {}", e),
371 }
372 buffer
373 }
374
375 #[inline(always)]
376 pub fn to_writer(&self, writer: &mut dyn std::io::Write) {
377 let metrics = self.gather();
378 match writer.write_all(&metrics) {
379 Ok(_) => {}
380 Err(e) => error!("Error writing metrics to writer: {}", e),
381 }
382 }
383
384 #[inline(always)]
385 pub fn register_int_gauge<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
386 let Some(metric) = Metric::new_int_gauge(name, help.into().unwrap_or(name)) else {
387 return;
388 };
389 self.register_metric(metric);
390 }
391
392 #[inline(always)]
393 pub fn register_float_gauge<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
394 let Some(metric) = Metric::new_float_gauge(name, help.into().unwrap_or(name)) else {
395 return;
396 };
397 self.register_metric(metric);
398 }
399
400 #[inline(always)]
401 pub fn register_int_counter<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
402 let Some(metric) = Metric::new_int_counter(name, help.into().unwrap_or(name)) else {
403 return;
404 };
405 self.register_metric(metric);
406 }
407
408 #[inline(always)]
409 pub fn register_histogram<'a>(
410 &self,
411 name: &str,
412 help: impl Into<Option<&'a str>>,
413 buckets: Option<&[f64]>,
414 ) {
415 let Some(metric) = Metric::new_histogram(name, help.into().unwrap_or(name), buckets) else {
416 return;
417 };
418 self.register_metric(metric);
419 }
420
421 #[inline(always)]
422 pub fn set(&self, name: &str, value: i64) -> bool {
423 if let Some(metric) = self.registry_index.get(name) {
424 metric.set(value);
425 true
426 } else {
427 false
428 }
429 }
430
431 #[inline(always)]
432 pub fn set_float(&self, name: &str, value: f64) -> bool {
433 if let Some(metric) = self.registry_index.get(name) {
434 metric.set_float(value);
435 true
436 } else {
437 false
438 }
439 }
440
441 #[inline(always)]
442 pub fn add_to_histogram(&self, name: &str, value: f64) -> bool {
443 if let Some(metric) = self.registry_index.get(name) {
444 metric.add_histogram_observation(value);
445 true
446 } else {
447 false
448 }
449 }
450
451 #[inline(always)]
452 pub fn start_timer(&self, name: &str) -> Option<HistogramTimer> {
453 self.registry_index
454 .get(name)
455 .and_then(|metric| metric.start_timer())
456 }
457
458 #[inline(always)]
459 pub fn inc(&self, name: &str) -> bool {
460 if let Some(metric) = self.registry_index.get(name) {
461 metric.inc();
462 true
463 } else {
464 false
465 }
466 }
467
468 #[inline(always)]
469 pub fn inc_by(&self, name: &str, value: i64) -> bool {
470 if let Some(metric) = self.registry_index.get(name) {
471 metric.inc_by(value);
472 true
473 } else {
474 false
475 }
476 }
477
478 #[inline(always)]
479 pub fn maybe_register_and_set<'a>(
480 &self,
481 name: &str,
482 value: i64,
483 help: impl Into<Option<&'a str>>,
484 ) {
485 if !self.set(name, value) {
486 let help = help.into();
487 self.register_int_gauge(name, help);
488 self.set(name, value);
489 }
490 }
491
492 #[inline(always)]
493 pub fn maybe_register_and_set_float<'a>(
494 &self,
495 name: &str,
496 value: f64,
497 help: impl Into<Option<&'a str>>,
498 ) {
499 if !self.set_float(name, value) {
500 let help = help.into();
501 self.register_float_gauge(name, help);
502 self.set_float(name, value);
503 }
504 }
505
506 #[inline(always)]
507 pub fn maybe_register_and_add_to_histogram<'a>(
508 &self,
509 name: &str,
510 value: f64,
511 buckets: Option<&[f64]>,
512 help: impl Into<Option<&'a str>>,
513 ) {
514 if !self.add_to_histogram(name, value) {
515 let help = help.into();
516 self.register_histogram(name, help, buckets);
517 self.add_to_histogram(name, value);
518 }
519 }
520
521 #[inline(always)]
522 pub fn maybe_register_and_inc<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
523 if !self.inc(name) {
524 let help = help.into();
525 self.register_int_counter(name, help);
526 self.inc(name);
527 }
528 }
529
530 #[inline(always)]
531 pub fn maybe_register_and_inc_by<'a>(
532 &self,
533 name: &str,
534 value: i64,
535 help: impl Into<Option<&'a str>>,
536 ) {
537 if !self.inc_by(name, value) {
538 let help = help.into();
539 self.register_int_counter(name, help);
540 self.inc_by(name, value);
541 }
542 }
543
544 #[inline(always)]
545 pub fn register_metric(&self, metric: impl Into<Metric>) {
546 let m = metric.into();
547 let fq_name = m.fq_name();
548
549 if self.registry_index.contains_key(&fq_name) {
550 return;
551 }
552
553 match self.registry.register(m.as_collector()) {
554 Ok(_) => {
555 self.registry_index.insert(fq_name, m);
556 }
557 Err(err) => {
558 debug!("Failed to register '{fq_name}': {err}")
559 }
560 }
561 }
562}
563
564fn sanitize_metric_name(name: &str) -> String {
565 let mut out = String::with_capacity(name.len());
567 let mut is_invalid: fn(char) -> bool = invalid_metric_name_start_character;
568 for c in name.chars() {
569 if is_invalid(c) {
570 out.push('_');
571 } else {
572 out.push(c);
573 }
574 is_invalid = invalid_metric_name_character;
575 }
576 out
577}
578
579#[inline]
580fn invalid_metric_name_start_character(c: char) -> bool {
581 !(c.is_ascii_alphabetic() || c == '_' || c == ':')
583}
584
585#[inline]
586fn invalid_metric_name_character(c: char) -> bool {
587 !(c.is_ascii_alphanumeric() || c == '_' || c == ':')
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 #[test]
596 fn test_sanitization() {
597 assert_eq!(
598 sanitize_metric_name("packets_sent_34.242.65.133:1789"),
599 "packets_sent_34_242_65_133:1789"
600 )
601 }
602
603 #[test]
604 fn prepend_package_name() {
605 let literal = prepend_package_name!("foo");
606 assert_eq!(literal, "nym_metrics_foo");
607
608 let bar = "bar";
609 let format = format!("foomp_{bar}");
610 let formatted = prepend_package_name!(format);
611 assert_eq!(formatted, "nym_metrics_foomp_bar");
612 }
613}