fastmetrics/registry/
mod.rs

1//! Registry module provides functionality for metric collection and organization.
2//!
3//! The registry is the central component that holds all metrics in an application.
4//! It supports organizing metrics hierarchically using namespaces and subsystems,
5//! and allows attaching constant labels to groups of metrics.
6//!
7//! See [`Registry`] for more details.
8
9mod errors;
10mod global;
11mod register;
12
13use std::{
14    borrow::Cow,
15    collections::hash_map::{self, HashMap},
16};
17
18pub use self::{errors::*, global::*, register::*};
19pub use crate::raw::Unit;
20use crate::{
21    encoder::EncodeMetric,
22    raw::{Metadata, MetricType},
23};
24
25/// A registry for collecting and organizing metrics.
26///
27/// The Registry type serves as a container for metrics and provides functionality to:
28/// - Organize metrics using namespaces and subsystems
29/// - Attach constant labels to groups of metrics
30/// - Create hierarchical metric structures
31/// - Register various types of metrics with optional units
32///
33/// # Namespaces and Subsystems
34///
35/// Metrics can be organized using:
36/// - A namespace: top-level prefix for all metrics in the registry
37/// - Subsystems: nested prefixes that create logical groupings
38///
39/// The final metric names will follow the pattern: `namespace_subsystem1_subsystem2_metric_name`.
40///
41/// # Example
42///
43/// ```rust
44/// # use fastmetrics::{
45/// #    metrics::{counter::Counter, gauge::Gauge},
46/// #    registry::{Registry, RegistryError},
47/// # };
48/// #
49/// # fn main() -> Result<(), RegistryError> {
50/// // Create a registry with a `myapp` namespace
51/// let mut registry = Registry::builder()
52///     .with_namespace("myapp")
53///     .with_const_labels([("env", "prod")])
54///     .build();
55/// assert_eq!(registry.namespace(), Some("myapp"));
56/// assert_eq!(registry.constant_labels(), [("env".into(), "prod".into())]);
57///
58/// // Register metrics into the registry
59/// let uptime_seconds = <Gauge>::default();
60/// registry.register("uptime_seconds", "Application uptime", uptime_seconds.clone())?;
61///
62/// // Create a subsystem for database metrics
63/// let db = registry.subsystem("database");
64/// assert_eq!(db.namespace(), Some("myapp_database"));
65/// assert_eq!(db.constant_labels(), [("env".into(), "prod".into())]);
66///
67/// // Register metrics into the database subsystem
68/// let db_connections = <Gauge>::default();
69/// db.register("connections", "Active database connections", db_connections.clone())?;
70///
71/// // Create a nested subsystem with additional constant labels
72/// let mysql = db.subsystem_builder("mysql").with_const_labels([("engine", "innodb")]).build();
73/// assert_eq!(mysql.namespace(), Some("myapp_database_mysql"));
74/// assert_eq!(
75///     mysql.constant_labels(),
76///     [("env".into(), "prod".into()), ("engine".into(), "innodb".into())],
77/// );
78///
79/// // Register metrics into the mysql subsystem
80/// let mysql_queries = <Counter>::default();
81/// mysql.register("queries", "Total MySQL queries", mysql_queries.clone())?;
82/// # Ok(())
83/// # }
84/// ```
85#[derive(Default)]
86pub struct Registry {
87    namespace: Option<Cow<'static, str>>,
88    const_labels: Vec<(Cow<'static, str>, Cow<'static, str>)>,
89    pub(crate) metrics: HashMap<Metadata, Box<dyn EncodeMetric + 'static>>,
90    pub(crate) subsystems: HashMap<Cow<'static, str>, Registry>,
91}
92
93/// A builder for constructing [`Registry`] instances with custom configuration.
94#[derive(Default)]
95pub struct RegistryBuilder {
96    namespace: Option<Cow<'static, str>>,
97    const_labels: Vec<(Cow<'static, str>, Cow<'static, str>)>,
98}
99
100impl RegistryBuilder {
101    /// Sets a `namespace` prefix for all metrics in the [`Registry`].
102    ///
103    /// # Note
104    ///
105    /// The namespace cannot be an empty string and must be in `snake_case` format,
106    /// otherwise it will throw a panic.
107    pub fn with_namespace(mut self, namespace: impl Into<Cow<'static, str>>) -> Self {
108        let namespace = namespace.into();
109        assert!(!namespace.is_empty(), "namespace cannot be an empty string");
110        assert!(is_snake_case(&namespace), "namespace must be in snake_case format");
111        self.namespace = Some(namespace);
112        self
113    }
114
115    /// Sets the `constant labels` that apply to all metrics in the [`Registry`].
116    ///
117    /// **NOTE**: constant labels are rarely used.
118    pub fn with_const_labels<N, V>(mut self, labels: impl IntoIterator<Item = (N, V)>) -> Self
119    where
120        N: Into<Cow<'static, str>>,
121        V: Into<Cow<'static, str>>,
122    {
123        self.const_labels = labels
124            .into_iter()
125            .map(|(name, value)| (name.into(), value.into()))
126            .collect::<Vec<_>>();
127        self
128    }
129
130    /// Builds a [`Registry`] instance.
131    pub fn build(self) -> Registry {
132        Registry {
133            namespace: self.namespace,
134            const_labels: self.const_labels,
135            metrics: HashMap::default(),
136            subsystems: HashMap::default(),
137        }
138    }
139}
140
141impl Registry {
142    /// Creates a [`RegistryBuilder`] to build [`Registry`] instance.
143    pub fn builder() -> RegistryBuilder {
144        RegistryBuilder::default()
145    }
146
147    /// Returns the current `namespace` of [`Registry`].
148    pub fn namespace(&self) -> Option<&str> {
149        self.namespace.as_deref()
150    }
151
152    /// Returns the `constant labels` of [`Registry`].
153    pub fn constant_labels(&self) -> &[(Cow<'static, str>, Cow<'static, str>)] {
154        &self.const_labels
155    }
156}
157
158// register
159impl Registry {
160    /// Registers a metric without a unit into [`Registry`].
161    ///
162    /// # Example
163    ///
164    /// ```rust
165    /// # use fastmetrics::{
166    /// #    metrics::counter::Counter,
167    /// #    registry::{Registry, RegistryError},
168    /// # };
169    /// #
170    /// # fn main() -> Result<(), RegistryError> {
171    /// let mut registry = Registry::default();
172    ///
173    /// let http_request_total = <Counter>::default();
174    /// registry.register(
175    ///     "http_request",
176    ///     "Total number of HTTP requests",
177    ///     http_request_total.clone()
178    /// )?;
179    /// // update the metric
180    /// // ...
181    /// # Ok(())
182    /// # }
183    /// ```
184    pub fn register(
185        &mut self,
186        name: impl Into<Cow<'static, str>>,
187        help: impl Into<Cow<'static, str>>,
188        metric: impl EncodeMetric + 'static,
189    ) -> Result<&mut Self, RegistryError> {
190        self.register_metric(name, help, None::<Unit>, metric)
191    }
192
193    /// Registers a metric with the specified unit into [`Registry`].
194    ///
195    /// # Example
196    ///
197    /// ```rust
198    /// # use fastmetrics::{
199    /// #     metrics::histogram::Histogram,
200    /// #     raw::metadata::Unit,
201    /// #     registry::{Registry, RegistryError},
202    /// # };
203    /// # fn main() -> Result<(), RegistryError> {
204    /// let mut registry = Registry::default();
205    ///
206    /// let http_request_duration_seconds = Histogram::default();
207    /// registry.register_with_unit(
208    ///     "http_request_duration",
209    ///     "Histogram of time spent during HTTP requests",
210    ///     Unit::Seconds,
211    ///     http_request_duration_seconds.clone()
212    /// )?;
213    /// // update the metric
214    /// // ...
215    /// # Ok(())
216    /// # }
217    /// ```
218    pub fn register_with_unit(
219        &mut self,
220        name: impl Into<Cow<'static, str>>,
221        help: impl Into<Cow<'static, str>>,
222        unit: impl Into<Unit>,
223        metric: impl EncodeMetric + 'static,
224    ) -> Result<&mut Self, RegistryError> {
225        self.register_metric(name, help, Some(unit), metric)
226    }
227
228    /// Registers a metric with an optional unit into [`Registry`].
229    ///
230    /// This is the most flexible registration method that allows specifying an optional unit.
231    /// Use [`Registry::register`] for metrics without units or [`Registry::register_with_unit`]
232    /// for metrics with units unless you need the flexibility of optional units.
233    ///
234    /// # Example
235    ///
236    /// ```rust
237    /// # use fastmetrics::{
238    /// #     metrics::{counter::Counter, histogram::Histogram},
239    /// #     registry::{Registry, RegistryError, Unit},
240    /// # };
241    /// # fn main() -> Result<(), RegistryError> {
242    /// let mut registry = Registry::default();
243    ///
244    /// // Register without a unit
245    /// let counter = <Counter>::default();
246    /// registry.register_metric("requests", "Total requests", None::<Unit>, counter)?;
247    ///
248    /// // Register with the unit
249    /// let histogram = Histogram::default();
250    /// registry.register_metric("duration", "Request duration", Some(Unit::Seconds), histogram)?;
251    /// # Ok(())
252    /// # }
253    /// ```
254    pub fn register_metric(
255        &mut self,
256        name: impl Into<Cow<'static, str>>,
257        help: impl Into<Cow<'static, str>>,
258        unit: Option<impl Into<Unit>>,
259        metric: impl EncodeMetric + 'static,
260    ) -> Result<&mut Self, RegistryError> {
261        let name = name.into();
262        if !is_snake_case(&name) {
263            return Err(RegistryError::InvalidNameFormat { name: name.clone() });
264        }
265
266        let unit = unit.map(Into::into);
267
268        // Check if metric type requires empty unit
269        match metric.metric_type() {
270            MetricType::StateSet | MetricType::Info | MetricType::Unknown => {
271                if unit.is_some() {
272                    return Err(RegistryError::MustHaveAnEmptyUnitString { name: name.clone() });
273                }
274            },
275            _ => {},
276        }
277
278        // Check the unit format
279        match unit {
280            Some(Unit::Other(unit)) if !is_lowercase(unit.as_ref()) => {
281                return Err(RegistryError::OtherUnitFormatMustBeLowercase { unit: unit.clone() });
282            },
283            _ => {},
284        }
285
286        let metadata = Metadata::new(name.clone(), help, metric.metric_type(), unit);
287        match self.metrics.entry(metadata) {
288            hash_map::Entry::Vacant(entry) => {
289                entry.insert(Box::new(metric));
290                Ok(self)
291            },
292            hash_map::Entry::Occupied(_) => Err(RegistryError::AlreadyExists { name }),
293        }
294    }
295}
296
297// subsystem
298impl Registry {
299    /// Creates a subsystem to register metrics with a subsystem `name` (as a part of prefix).
300    /// If the subsystem `name` already exists, the previous created subsystem will be returned.
301    ///
302    /// # Note
303    ///
304    /// The name of subsystem cannot be an empty string and must be in `snake_case` format,
305    /// otherwise it will throw a panic.
306    ///
307    /// # Example
308    ///
309    /// ```rust
310    /// # use fastmetrics::registry::Registry;
311    /// let mut registry = Registry::builder()
312    ///     .with_namespace("myapp")
313    ///     .with_const_labels([("env", "prod")])
314    ///     .build();
315    /// assert_eq!(registry.namespace(), Some("myapp"));
316    /// assert_eq!(registry.constant_labels(), [("env".into(), "prod".into())]);
317    ///
318    /// let subsystem1 = registry.subsystem("subsystem1");
319    /// assert_eq!(subsystem1.namespace(), Some("myapp_subsystem1"));
320    /// assert_eq!(subsystem1.constant_labels(), [("env".into(), "prod".into())]);
321    ///
322    /// let subsystem2 = registry.subsystem("subsystem2");
323    /// assert_eq!(subsystem2.namespace(), Some("myapp_subsystem2"));
324    /// assert_eq!(subsystem2.constant_labels(), [("env".into(), "prod".into())]);
325    ///
326    /// let nested_subsystem = registry.subsystem("subsystem1").subsystem("subsystem2");
327    /// assert_eq!(nested_subsystem.namespace(), Some("myapp_subsystem1_subsystem2"));
328    /// assert_eq!(nested_subsystem.constant_labels(), [("env".into(), "prod".into())]);
329    /// ```
330    pub fn subsystem(&mut self, name: impl Into<Cow<'static, str>>) -> &mut Registry {
331        self.subsystem_builder(name).build()
332    }
333
334    /// Creates a builder for constructing a subsystem with custom configuration.
335    ///
336    /// This method provides more flexibility than [`subsystem`](Registry::subsystem) by allowing
337    /// you to configure additional properties like constant labels specific to the subsystem.
338    ///
339    /// # Note
340    ///
341    /// The name of subsystem cannot be an empty string and must be in `snake_case` format,
342    /// otherwise it will throw a panic.
343    ///
344    /// # Example
345    ///
346    /// ```rust
347    /// # use fastmetrics::registry::Registry;
348    /// let mut registry = Registry::builder()
349    ///     .with_namespace("myapp")
350    ///     .with_const_labels([("env", "prod")])
351    ///     .build();
352    ///
353    /// let db = registry.subsystem("database");
354    ///
355    /// let mysql = db
356    ///     .subsystem_builder("mysql")
357    ///     .with_const_labels([("engine", "innodb")])
358    ///     .build();
359    ///
360    /// assert_eq!(mysql.namespace(), Some("myapp_database_mysql"));
361    /// assert_eq!(
362    ///     mysql.constant_labels(),
363    ///     [("env".into(), "prod".into()), ("engine".into(), "innodb".into())]
364    /// );
365    /// ```
366    pub fn subsystem_builder(
367        &mut self,
368        name: impl Into<Cow<'static, str>>,
369    ) -> RegistrySubsystemBuilder<'_> {
370        let name = name.into();
371        assert!(!name.is_empty(), "subsystem name cannot be an empty string");
372        assert!(is_snake_case(&name), "subsystem name must be in snake_case format");
373        RegistrySubsystemBuilder::new(self, name)
374    }
375}
376
377/// A builder for constructing subsystems with custom configuration.
378///
379/// This builder allows you to create subsystems with additional constant labels
380/// beyond those inherited from the parent registry. The subsystem will inherit
381/// the parent's namespace and constant labels, with any additional labels specified
382/// through this builder being merged in.
383pub struct RegistrySubsystemBuilder<'a> {
384    parent: &'a mut Registry,
385    name: Cow<'static, str>,
386    const_labels: Option<Vec<(Cow<'static, str>, Cow<'static, str>)>>,
387}
388
389impl<'a> RegistrySubsystemBuilder<'a> {
390    fn new(parent: &'a mut Registry, name: Cow<'static, str>) -> RegistrySubsystemBuilder<'a> {
391        Self { parent, name, const_labels: None }
392    }
393
394    /// Sets additional constant labels for the subsystem.
395    ///
396    /// These labels will be merged with the parent registry's constant labels.
397    /// If there are any label key conflicts, the subsystem's labels will take precedence.
398    ///
399    /// # Example
400    ///
401    /// ```rust
402    /// # use fastmetrics::registry::Registry;
403    /// let mut registry = Registry::builder()
404    ///     .with_namespace("myapp")
405    ///     .with_const_labels([("env", "prod")])
406    ///     .build();
407    ///
408    /// let subsystem = registry
409    ///     .subsystem_builder("database")
410    ///     .with_const_labels([("engine", "innodb"), ("instance", "primary")])
411    ///     .build();
412    /// ```
413    pub fn with_const_labels<N, V>(mut self, labels: impl IntoIterator<Item = (N, V)>) -> Self
414    where
415        N: Into<Cow<'static, str>>,
416        V: Into<Cow<'static, str>>,
417    {
418        let labels = labels
419            .into_iter()
420            .map(|(name, value)| (name.into(), value.into()))
421            .collect::<Vec<_>>();
422        self.const_labels = Some(labels);
423        self
424    }
425
426    /// Builds and returns a mutable reference to the subsystem.
427    ///
428    /// If a subsystem with the same name already exists, this will return a reference
429    /// to the existing subsystem. Otherwise, it creates a new subsystem with the
430    /// configured properties.
431    ///
432    /// The resulting subsystem will have:
433    /// - A namespace combining the parent's namespace with the subsystem name
434    /// - Constant labels merged from parent and subsystem-specific labels
435    pub fn build(self) -> &'a mut Registry {
436        let const_labels = match self.const_labels {
437            Some(subsystem_const_labels) => {
438                let mut merged = self.parent.const_labels.clone();
439
440                for (new_key, new_value) in subsystem_const_labels {
441                    if let Some(pos) = merged.iter().position(|(key, _)| key == &new_key) {
442                        merged[pos] = (new_key, new_value);
443                    } else {
444                        merged.push((new_key, new_value));
445                    }
446                }
447
448                merged
449            },
450            None => self.parent.const_labels.clone(),
451        };
452
453        self.parent.subsystems.entry(self.name.clone()).or_insert_with(|| {
454            let namespace = match &self.parent.namespace {
455                Some(namespace) => Cow::Owned(format!("{}_{}", namespace, self.name)),
456                None => self.name,
457            };
458            Registry::builder()
459                .with_namespace(namespace)
460                .with_const_labels(const_labels)
461                .build()
462        })
463    }
464}
465
466fn is_snake_case(name: &str) -> bool {
467    if name.is_empty() {
468        return false;
469    }
470
471    match name.chars().next() {
472        // first char shouldn't be ascii digit or '_'
473        Some(first) if first.is_ascii_digit() || first == '_' => return false,
474        _ => {},
475    }
476
477    // name shouldn't contain "__" and the suffix of name shouldn't be '_'
478    if name.contains("__") || name.ends_with('_') {
479        return false;
480    }
481
482    // all chars of name should match 'a'..='z' | '0'..='9' | '_'
483    name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
484}
485
486fn is_lowercase(name: &str) -> bool {
487    if name.is_empty() {
488        return false;
489    }
490
491    // all chars of name should match 'a'..='z' | '0'..='9'
492    name.chars().all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
493}
494
495#[cfg(test)]
496mod tests {
497    use std::{fmt, time::Duration};
498
499    use super::*;
500    use crate::encoder::MetricEncoder;
501
502    #[test]
503    fn test_registry_subsystem() {
504        let mut registry = Registry::builder()
505            .with_namespace("myapp")
506            .with_const_labels([("env", "prod")])
507            .build();
508        assert_eq!(registry.namespace(), Some("myapp"));
509        assert_eq!(registry.constant_labels(), [("env".into(), "prod".into())]);
510
511        let subsystem1 = registry.subsystem("subsystem1");
512        assert_eq!(subsystem1.namespace(), Some("myapp_subsystem1"));
513        assert_eq!(subsystem1.constant_labels(), [("env".into(), "prod".into())]);
514
515        let subsystem2 = registry.subsystem("subsystem2");
516        assert_eq!(subsystem2.namespace(), Some("myapp_subsystem2"));
517        assert_eq!(subsystem2.constant_labels(), [("env".into(), "prod".into())]);
518
519        let nested_subsystem = registry.subsystem("subsystem1").subsystem("subsystem2");
520        assert_eq!(nested_subsystem.namespace(), Some("myapp_subsystem1_subsystem2"));
521        assert_eq!(nested_subsystem.constant_labels(), [("env".into(), "prod".into())]);
522    }
523
524    #[test]
525    fn test_registry_subsystem_with_const_labels() {
526        let mut registry = Registry::builder()
527            .with_namespace("myapp")
528            .with_const_labels([("env", "prod")])
529            .build();
530        assert_eq!(registry.namespace(), Some("myapp"));
531        assert_eq!(registry.constant_labels(), [("env".into(), "prod".into())]);
532
533        let subsystem1 = registry
534            .subsystem_builder("subsystem1")
535            .with_const_labels([("name", "value")])
536            .build();
537        assert_eq!(subsystem1.namespace(), Some("myapp_subsystem1"));
538        assert_eq!(
539            subsystem1.constant_labels(),
540            [("env".into(), "prod".into()), ("name".into(), "value".into())]
541        );
542    }
543
544    #[test]
545    fn test_subsystem_const_labels_override() {
546        let mut registry = Registry::builder()
547            .with_namespace("myapp")
548            .with_const_labels([("env", "dev"), ("region", "us-west")])
549            .build();
550
551        let subsystem = registry
552            .subsystem_builder("cache")
553            .with_const_labels([("env", "prod"), ("type", "redis")])
554            .build();
555
556        let labels = subsystem.constant_labels();
557
558        assert_eq!(labels.iter().filter(|(k, _)| k == "env").count(), 1);
559        assert_eq!(labels.len(), 3);
560
561        assert!(labels.iter().any(|(k, v)| k == "env" && v == "prod"));
562        assert!(labels.iter().any(|(k, v)| k == "region" && v == "us-west"));
563        assert!(labels.iter().any(|(k, v)| k == "type" && v == "redis"));
564    }
565
566    pub(crate) struct DummyCounter;
567    impl EncodeMetric for DummyCounter {
568        fn encode(&self, _encoder: &mut dyn MetricEncoder) -> fmt::Result {
569            Ok(())
570        }
571
572        fn metric_type(&self) -> MetricType {
573            MetricType::Counter
574        }
575
576        fn timestamp(&self) -> Option<Duration> {
577            None
578        }
579    }
580
581    #[test]
582    fn test_register_same_metric() {
583        let mut registry = Registry::default();
584
585        // Register first counter
586        registry.register("my_dummy_counter", "", DummyCounter).unwrap();
587
588        // Try to register another counter with the same name and type - this will fail
589        let result = registry.register("my_dummy_counter", "Another dummy counter", DummyCounter);
590        assert!(matches!(result, Err(RegistryError::AlreadyExists { .. })));
591    }
592
593    #[test]
594    fn test_is_snake_case() {
595        let cases = vec!["name1", "name_1", "name_1_2"];
596        for case in cases {
597            assert!(is_snake_case(case));
598        }
599
600        let invalid_cases = vec!["_", "1name", "name__1", "name_", "name!"];
601        for invalid_case in invalid_cases {
602            assert!(!is_snake_case(invalid_case));
603        }
604    }
605
606    #[test]
607    fn test_is_lowercase() {
608        let cases = vec!["name1", "1name", "na1me"];
609        for case in cases {
610            assert!(is_lowercase(case));
611        }
612
613        let invalid_cases = vec!["_", "name_", "name!"];
614        for invalid_case in invalid_cases {
615            assert!(!is_lowercase(invalid_case));
616        }
617    }
618}