nym_metrics/
lib.rs

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        // if the block needs to return something, we can return it
133        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    &REGISTRY
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    // The first character must be [a-zA-Z_:], and all subsequent characters must be [a-zA-Z0-9_:].
566    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    // Essentially, needs to match the regex pattern of [a-zA-Z_:].
582    !(c.is_ascii_alphabetic() || c == '_' || c == ':')
583}
584
585#[inline]
586fn invalid_metric_name_character(c: char) -> bool {
587    // Essentially, needs to match the regex pattern of [a-zA-Z0-9_:].
588    !(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}