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}