Skip to main content

commonware_runtime/telemetry/metrics/
mod.rs

1//! Utility functions for metrics
2
3mod batched;
4pub mod histogram;
5mod registration;
6pub mod status;
7pub(crate) mod task;
8
9/// Prefix for runtime metrics.
10pub(crate) const METRICS_PREFIX: &str = "runtime";
11
12pub use commonware_runtime_macros::{EncodeLabelSet, EncodeLabelValue, EncodeStruct};
13pub use prometheus_client::{
14    collector, encoding,
15    encoding::{
16        CounterValueEncoder, DescriptorEncoder, EncodeCounterValue, EncodeExemplarTime,
17        EncodeExemplarValue, EncodeGaugeValue, EncodeLabel, EncodeLabelKey,
18        EncodeLabelSet as EncodeLabelSetTrait, EncodeLabelValue as EncodeLabelValueTrait,
19        EncodeMetric, ExemplarValueEncoder, GaugeValueEncoder, LabelEncoder, LabelKeyEncoder,
20        LabelSetEncoder, LabelValueEncoder, MetricEncoder, NoLabelSet,
21    },
22    metrics::{MetricType, TypedMetric},
23    registry,
24    registry::Metric,
25};
26
27/// Underlying Prometheus metric types. Used when constructing a metric
28/// to pass to [`crate::Metrics::register`].
29pub mod raw {
30    pub use prometheus_client::metrics::{
31        counter::Counter,
32        family::{self, Family},
33        gauge::Gauge,
34        histogram::Histogram,
35    };
36}
37
38pub use batched::{Batchable, Batched};
39use commonware_utils::sync::Mutex;
40use prometheus_client::encoding::{
41    MetricEncoder as PromMetricEncoder,
42    text::{encode, encode_eof},
43};
44pub use registration::Registration;
45use std::{
46    any::Any,
47    borrow::Cow,
48    collections::{BTreeMap, HashMap},
49    ops::Deref,
50    sync::{Arc, Weak, atomic::Ordering},
51};
52
53/// Native integer width used by [`raw::Gauge`] on this target.
54///
55/// `i64` on platforms with 64-bit atomics, `i32` otherwise. Matches
56/// `prometheus_client::metrics::gauge::Gauge`'s backing type.
57#[cfg(target_has_atomic = "64")]
58pub type GaugeValue = i64;
59#[cfg(not(target_has_atomic = "64"))]
60pub type GaugeValue = i32;
61
62/// A registered counter metric.
63pub type Counter = Registered<raw::Counter>;
64/// A registered gauge metric.
65pub type Gauge = Registered<raw::Gauge>;
66/// A registered histogram metric.
67pub type Histogram = Registered<raw::Histogram>;
68/// A registered family of counters keyed by `L`.
69pub type CounterFamily<L> = Registered<raw::Family<L, raw::Counter>>;
70/// A registered family of gauges keyed by `L`.
71pub type GaugeFamily<L> = Registered<raw::Family<L, raw::Gauge>>;
72
73/// Convenience methods for Prometheus gauges.
74pub trait GaugeExt {
75    /// Set a gauge from a lossless integer conversion.
76    fn try_set<T: TryInto<GaugeValue>>(&self, value: T) -> Result<GaugeValue, T::Error>;
77
78    /// Atomically raise a gauge to at least the provided value.
79    fn try_set_max<T: TryInto<GaugeValue>>(&self, value: T) -> Result<GaugeValue, T::Error>;
80}
81
82impl GaugeExt for raw::Gauge {
83    fn try_set<T: TryInto<GaugeValue>>(&self, value: T) -> Result<GaugeValue, T::Error> {
84        let value = value.try_into()?;
85        Ok(self.set(value))
86    }
87
88    fn try_set_max<T: TryInto<GaugeValue>>(&self, value: T) -> Result<GaugeValue, T::Error> {
89        let value = value.try_into()?;
90        Ok(self.inner().fetch_max(value, Ordering::Relaxed))
91    }
92}
93
94pub use histogram::HistogramExt;
95
96/// One-line constructors for the common metric types.
97pub trait MetricsExt: crate::Metrics {
98    /// Register a counter with the runtime.
99    fn counter<N: Into<String>, H: Into<String>>(&self, name: N, help: H) -> Counter {
100        self.register(name, help, raw::Counter::default())
101    }
102
103    /// Register a gauge with the runtime.
104    fn gauge<N: Into<String>, H: Into<String>>(&self, name: N, help: H) -> Gauge {
105        self.register(name, help, raw::Gauge::default())
106    }
107
108    /// Register a histogram with the runtime.
109    fn histogram<N: Into<String>, H: Into<String>, I>(
110        &self,
111        name: N,
112        help: H,
113        buckets: I,
114    ) -> Histogram
115    where
116        I: IntoIterator<Item = f64>,
117    {
118        self.register(name, help, raw::Histogram::new(buckets))
119    }
120
121    /// Register a metric family with the runtime.
122    fn family<N, H, S, M>(&self, name: N, help: H) -> Registered<raw::Family<S, M>>
123    where
124        N: Into<String>,
125        H: Into<String>,
126        S: Clone + std::hash::Hash + Eq,
127        M: Default,
128        raw::Family<S, M>: Metric,
129    {
130        self.register(name, help, raw::Family::<S, M>::default())
131    }
132}
133
134impl<T: crate::Metrics> MetricsExt for T {}
135
136/// Validates that a label matches Prometheus metric name format: `[a-zA-Z][a-zA-Z0-9_]*`.
137///
138/// # Panics
139///
140/// Panics if the label is empty, starts with a non-alphabetic character,
141/// or contains characters other than `[a-zA-Z0-9_]`.
142pub fn validate_label(label: &str) {
143    let mut chars = label.chars();
144    assert!(
145        chars.next().is_some_and(|c| c.is_ascii_alphabetic()),
146        "label must start with [a-zA-Z]: {label}"
147    );
148    assert!(
149        chars.all(|c| c.is_ascii_alphanumeric() || c == '_'),
150        "label must only contain [a-zA-Z0-9_]: {label}"
151    );
152}
153
154/// Add an attribute to a sorted attribute list, maintaining sorted order via binary search.
155///
156/// A duplicate key has its value overwritten.
157pub fn add_attribute(
158    attributes: &mut Vec<(String, String)>,
159    key: &str,
160    value: impl std::fmt::Display,
161) {
162    let key_string = key.to_string();
163    let value_string = value.to_string();
164
165    match attributes.binary_search_by(|(k, _)| k.cmp(&key_string)) {
166        Ok(pos) => attributes[pos].1 = value_string,
167        Err(pos) => attributes.insert(pos, (key_string, value_string)),
168    }
169}
170
171#[cfg(any(test, feature = "test-utils"))]
172fn matches_metric_name(full: &str, name: &str) -> bool {
173    full == name
174        || full
175            .strip_suffix(name)
176            .is_some_and(|prefix| prefix.ends_with('_'))
177}
178
179/// Return `true` if encoded Prometheus metrics contain a sample with `name` and `value`.
180///
181/// `name` may be either the full encoded metric name or its unprefixed suffix.
182/// Labels attached to the sample are ignored.
183#[cfg(any(test, feature = "test-utils"))]
184#[must_use]
185pub fn has_metric_value(metrics: &str, name: &str, value: impl std::fmt::Display) -> bool {
186    let value = value.to_string();
187    metric_samples(metrics, name).any(|(_, sample_value)| sample_value == value)
188}
189
190/// Iterate over the label set and value of encoded Prometheus samples matching `name`.
191///
192/// `name` may be either the full encoded metric name or its unprefixed suffix. Label sets are
193/// returned without their surrounding braces and are empty for unlabeled samples.
194#[cfg(any(test, feature = "test-utils"))]
195pub fn metric_samples<'a>(
196    metrics: &'a str,
197    name: &'a str,
198) -> impl Iterator<Item = (&'a str, &'a str)> + 'a {
199    metrics.lines().filter_map(move |line| {
200        let line = line.trim();
201        if line.starts_with('#') {
202            return None;
203        }
204
205        let sample_end = line.find(|c: char| c == '{' || c.is_whitespace())?;
206        let sample_name = &line[..sample_end];
207        if !matches_metric_name(sample_name, name) {
208            return None;
209        }
210
211        let mut rest = &line[sample_end..];
212        let labels = if let Some(labeled) = rest.strip_prefix('{') {
213            // Label values may contain braces and escaped quotes. Only an unquoted brace ends
214            // the label set.
215            let mut quoted = false;
216            let mut escaped = false;
217            let labels_end = labeled.bytes().position(|byte| {
218                if quoted {
219                    if escaped {
220                        escaped = false;
221                    } else if byte == b'\\' {
222                        escaped = true;
223                    } else if byte == b'"' {
224                        quoted = false;
225                    }
226                    false
227                } else if byte == b'"' {
228                    quoted = true;
229                    false
230                } else {
231                    byte == b'}'
232                }
233            })?;
234            rest = &labeled[labels_end + 1..];
235            &labeled[..labels_end]
236        } else {
237            ""
238        };
239        if !rest.chars().next().is_some_and(char::is_whitespace) {
240            return None;
241        }
242
243        Some((labels, rest.split_whitespace().next()?))
244    })
245}
246
247/// Count the number of running tasks whose name starts with the given prefix.
248///
249/// This function encodes metrics and counts tasks that are currently running
250/// (have a value of 1) and whose name starts with the specified prefix.
251///
252/// This is useful for verifying that all child tasks under a given label hierarchy
253/// have been properly shut down.
254///
255/// # Example
256///
257/// ```rust
258/// use commonware_runtime::{
259///     deterministic, telemetry::metrics::count_running_tasks, Clock, Metrics, Runner, Spawner,
260///     Supervisor,
261/// };
262/// use std::time::Duration;
263///
264/// let executor = deterministic::Runner::default();
265/// executor.start(|context| async move {
266///     // Spawn a task under a labeled context
267///     let handle = context.child("worker").spawn(|ctx| async move {
268///         ctx.sleep(Duration::from_secs(100)).await;
269///     });
270///
271///     // Allow the task to start
272///     context.sleep(Duration::from_millis(10)).await;
273///
274///     // Count running tasks with "worker" prefix
275///     let count = count_running_tasks(&context, "worker");
276///     assert!(count > 0, "worker task should be running");
277///
278///     // Abort the task
279///     handle.abort();
280///     let _ = handle.await;
281///     context.sleep(Duration::from_millis(10)).await;
282///
283///     // Verify task is stopped
284///     let count = count_running_tasks(&context, "worker");
285///     assert_eq!(count, 0, "worker task should be stopped");
286/// });
287/// ```
288#[cfg(any(test, feature = "test-utils"))]
289pub fn count_running_tasks(metrics: &impl crate::Metrics, prefix: &str) -> usize {
290    let encoded = metrics.encode();
291    encoded
292        .lines()
293        .filter_map(|line| {
294            if !line.starts_with("runtime_tasks_running{") || !line.contains("kind=\"Task\"") {
295                return None;
296            }
297            let name = line.split("name=\"").nth(1)?.split('"').next()?;
298            if !name.starts_with(prefix) {
299                return None;
300            }
301            line.trim_end().rsplit(' ').next()?.parse::<usize>().ok()
302        })
303        .sum()
304}
305
306// Adaptation of client_rust's internal descriptor encoder.
307//
308// Source:
309// https://github.com/prometheus/client_rust/blob/4a6d40a55443d5b18f5be311d246c03e56f417d6/src/encoding/text.rs#L218-L275
310fn encode_descriptor<W>(
311    writer: &mut W,
312    name: &str,
313    help: &str,
314    metric_type: MetricType,
315) -> Result<(), std::fmt::Error>
316where
317    W: std::fmt::Write,
318{
319    writer.write_str("# HELP ")?;
320    writer.write_str(name)?;
321    writer.write_str(" ")?;
322    writer.write_str(help)?;
323    writer.write_str("\n# TYPE ")?;
324    writer.write_str(name)?;
325    writer.write_str(" ")?;
326    writer.write_str(metric_type.as_str())?;
327    writer.write_str("\n")?;
328    Ok(())
329}
330
331/// Join a metric or label prefix with a child name using Prometheus' `_` separator.
332pub(crate) fn prefixed_name(prefix: &str, name: &str) -> String {
333    if prefix.is_empty() {
334        name.to_string()
335    } else {
336        format!("{prefix}_{name}")
337    }
338}
339
340/// Build a child context label by appending `label` to `prefix`, asserting that
341/// `label` is valid and does not shadow the reserved runtime metric prefix.
342pub(crate) fn child_label(prefix: &str, label: &str) -> String {
343    validate_label(label);
344    let name = prefixed_name(prefix, label);
345    assert!(
346        !name.starts_with(METRICS_PREFIX),
347        "using runtime label is not allowed"
348    );
349    name
350}
351
352struct RegistryGuard {
353    id: usize,
354    registry: Weak<Mutex<RegistryInner>>,
355}
356
357impl Drop for RegistryGuard {
358    fn drop(&mut self) {
359        let Some(registry) = self.registry.upgrade() else {
360            return;
361        };
362        registry.lock().release_registration(self.id);
363    }
364}
365
366/// A metric handle whose lifetime controls registry exposure and attached cleanup.
367#[must_use = "registered metrics are removed when the returned handle is dropped"]
368pub struct Registered<M> {
369    metric: Arc<M>,
370    registration: Registration,
371}
372
373impl<M> Clone for Registered<M> {
374    fn clone(&self) -> Self {
375        Self {
376            metric: self.metric.clone(),
377            registration: self.registration.clone(),
378        }
379    }
380}
381
382impl<M> Registered<M> {
383    /// Create a metric handle with an explicit lifecycle registration.
384    ///
385    /// The provided [`Registration`] controls what happens when the last clone
386    /// of this handle is dropped. Use [`Self::detached`] when the handle needs no
387    /// registry entry or cleanup.
388    pub fn with_registration(metric: M, registration: Registration) -> Self {
389        Self {
390            metric: Arc::new(metric),
391            registration,
392        }
393    }
394
395    /// Create a metric handle with no registry entry and no cleanup on drop.
396    pub fn detached(metric: M) -> Self {
397        Self::with_registration(metric, Registration::from(()))
398    }
399
400    pub fn metric(&self) -> &M {
401        self.metric.as_ref()
402    }
403}
404
405impl<S, M, C> Registered<raw::Family<S, M, C>>
406where
407    S: Clone + std::hash::Hash + Eq,
408    C: raw::family::MetricConstructor<M>,
409{
410    pub fn get_by<Q>(
411        &self,
412        label_set: &Q,
413    ) -> Option<impl Deref<Target = M> + '_ + use<'_, Q, S, M, C>>
414    where
415        for<'a> S: From<&'a Q>,
416    {
417        let label_set = S::from(label_set);
418        self.get(&label_set)
419    }
420
421    pub fn get_or_create_by<Q>(
422        &self,
423        label_set: &Q,
424    ) -> impl Deref<Target = M> + '_ + use<'_, Q, S, M, C>
425    where
426        for<'a> S: From<&'a Q>,
427    {
428        let label_set = S::from(label_set);
429        self.get_or_create(&label_set)
430    }
431
432    pub fn remove_by<Q>(&self, label_set: &Q) -> bool
433    where
434        for<'a> S: From<&'a Q>,
435    {
436        let label_set = S::from(label_set);
437        self.remove(&label_set)
438    }
439}
440
441impl<M> Deref for Registered<M> {
442    type Target = M;
443
444    fn deref(&self) -> &Self::Target {
445        self.metric()
446    }
447}
448
449impl<M: std::fmt::Debug> std::fmt::Debug for Registered<M> {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        f.debug_struct("Registered")
452            .field("metric", self.metric())
453            .finish_non_exhaustive()
454    }
455}
456
457type MetricAttributes = Vec<(Cow<'static, str>, Cow<'static, str>)>;
458type MetricKey = (String, MetricAttributes);
459type SampleEncoder = dyn Fn(&mut String) -> Result<(), std::fmt::Error> + Send + Sync;
460
461struct PendingMetricEntry {
462    family_name: String,
463    attributes: MetricAttributes,
464    encode_samples: Box<SampleEncoder>,
465    metric_any: Arc<dyn Any + Send + Sync>,
466}
467
468pub(crate) struct SharedMetric<M>(pub(crate) Arc<M>);
469
470impl<M: std::fmt::Debug> std::fmt::Debug for SharedMetric<M> {
471    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472        self.0.fmt(f)
473    }
474}
475
476impl<M: EncodeMetric> EncodeMetric for SharedMetric<M> {
477    fn encode(&self, encoder: PromMetricEncoder<'_>) -> Result<(), std::fmt::Error> {
478        self.0.encode(encoder)
479    }
480
481    fn metric_type(&self) -> MetricType {
482        self.0.metric_type()
483    }
484}
485
486fn create_sample_encoder<M>(
487    name: String,
488    labels: MetricAttributes,
489    metric: Arc<M>,
490) -> Box<SampleEncoder>
491where
492    M: Metric,
493{
494    // TODO (#3659): Avoid allocating an upstream registry per metric once
495    // `prometheus-client` exposes a public sample-only `MetricEncoder` path
496    // for encoding one metric with const labels.
497    let mut registry = registry::Registry::with_labels(labels.into_iter());
498    registry.register(name, "", SharedMetric(metric));
499
500    Box::new(move |samples| {
501        let mut encoded = String::new();
502        encode(&mut encoded, &registry).expect("encoding temporary metric registry failed");
503        for line in encoded.lines() {
504            if line.starts_with('#') {
505                continue;
506            }
507            samples.push_str(line);
508            samples.push('\n');
509        }
510        Ok(())
511    })
512}
513
514fn owned_attributes(attributes: Vec<(String, String)>) -> MetricAttributes {
515    attributes
516        .into_iter()
517        .map(|(k, v)| (Cow::Owned(k), Cow::Owned(v)))
518        .collect()
519}
520
521// Match upstream prometheus-client's `Descriptor::new` normalization.
522//
523// Source:
524// https://github.com/prometheus/client_rust/blob/4a6d40a55443d5b18f5be311d246c03e56f417d6/src/registry.rs#L340-L348
525fn normalize_help(help: String) -> String {
526    help + "."
527}
528
529struct MetricEntry {
530    family_name: String,
531    attributes: MetricAttributes,
532    encode_samples: Box<SampleEncoder>,
533    metric_any: Arc<dyn Any + Send + Sync>,
534    claims: usize,
535    family_index: usize,
536}
537
538#[derive(Debug)]
539struct MetricFamily {
540    help: String,
541    metric_type: MetricType,
542    descriptor: String,
543    metric_ids: Vec<usize>,
544}
545
546/// Manages metrics with explicit lifetimes.
547#[derive(Clone)]
548pub struct Registry {
549    inner: Arc<Mutex<RegistryInner>>,
550}
551
552struct RegistryInner {
553    /// Dense metric storage indexed by stable metric id.
554    metrics: Vec<Option<MetricEntry>>,
555    /// Metric ids that can be reused after a metric is fully unregistered.
556    free_metric_ids: Vec<usize>,
557    /// Metric families keyed by family name, kept sorted for deterministic encoding.
558    families: BTreeMap<String, MetricFamily>,
559    /// Exact metric keys for duplicate registration detection.
560    keys: HashMap<MetricKey, usize>,
561    /// Monotonic id source used when there is no reusable metric slot.
562    next_metric_id: usize,
563}
564
565impl Default for Registry {
566    fn default() -> Self {
567        Self::new()
568    }
569}
570
571impl Registry {
572    pub fn new() -> Self {
573        Self {
574            inner: Arc::new(Mutex::new(RegistryInner::new())),
575        }
576    }
577
578    pub(crate) fn register<M>(
579        &self,
580        name: String,
581        help: String,
582        attributes: Vec<(String, String)>,
583        metric: Arc<M>,
584    ) -> Registered<M>
585    where
586        M: Metric,
587    {
588        let mut inner = self.inner.lock();
589        inner.register(Arc::downgrade(&self.inner), name, help, attributes, metric)
590    }
591
592    pub fn encode(&self) -> String {
593        self.inner.lock().encode()
594    }
595}
596
597impl RegistryInner {
598    fn new() -> Self {
599        Self {
600            metrics: Vec::new(),
601            free_metric_ids: Vec::new(),
602            families: BTreeMap::new(),
603            keys: HashMap::new(),
604            next_metric_id: 0,
605        }
606    }
607
608    fn register<M>(
609        &mut self,
610        registry: Weak<Mutex<Self>>,
611        name: String,
612        help: String,
613        attributes: Vec<(String, String)>,
614        metric: Arc<M>,
615    ) -> Registered<M>
616    where
617        M: Metric,
618    {
619        let attributes = owned_attributes(attributes);
620        let help = normalize_help(help);
621        let metric_type = metric.metric_type();
622        let encode_samples =
623            create_sample_encoder(name.clone(), attributes.clone(), metric.clone());
624        let key = (name.clone(), attributes.clone());
625        if let Some(existing_id) = self.keys.get(&key).copied() {
626            let entry = self.metric_ref(existing_id);
627            if let Some(family) = self.families.get(&name) {
628                assert_eq!(
629                    family.help, help,
630                    "metric family `{}` registered with inconsistent help text",
631                    name
632                );
633            }
634            let existing_metric = Arc::clone(&entry.metric_any)
635                .downcast::<M>()
636                .unwrap_or_else(|_| {
637                    panic!(
638                        "duplicate metric `{}` with attributes {:?} registered with different type",
639                        key.0, key.1
640                    )
641                });
642            self.claim_registration(existing_id);
643            return Registered {
644                metric: existing_metric,
645                registration: Registration::from(RegistryGuard {
646                    id: existing_id,
647                    registry,
648                }),
649            };
650        }
651        self.assert_family_matches(&name, &help, metric_type);
652
653        let id = self.allocate_metric_id();
654        let registration = Registration::from(RegistryGuard { id, registry });
655        let metric_any: Arc<dyn Any + Send + Sync> = metric.clone();
656        self.insert_metric_entry(
657            id,
658            help,
659            metric_type,
660            PendingMetricEntry {
661                family_name: name,
662                attributes,
663                encode_samples,
664                metric_any,
665            },
666        );
667        Registered {
668            metric,
669            registration,
670        }
671    }
672
673    fn metric_slot_mut(&mut self, id: usize) -> &mut Option<MetricEntry> {
674        if id == self.metrics.len() {
675            self.metrics.push(None);
676        }
677        &mut self.metrics[id]
678    }
679
680    fn metric_ref(&self, id: usize) -> &MetricEntry {
681        self.metrics
682            .get(id)
683            .and_then(Option::as_ref)
684            .expect("metric id missing from registry")
685    }
686
687    fn metric_mut(&mut self, id: usize) -> &mut MetricEntry {
688        self.metrics
689            .get_mut(id)
690            .and_then(Option::as_mut)
691            .expect("metric id missing from registry")
692    }
693
694    fn allocate_metric_id(&mut self) -> usize {
695        if let Some(id) = self.free_metric_ids.pop() {
696            return id;
697        }
698        let id = self.next_metric_id;
699        self.next_metric_id = self
700            .next_metric_id
701            .checked_add(1)
702            .expect("metric id overflow");
703        id
704    }
705
706    fn assert_family_matches(&self, name: &str, help: &str, metric_type: MetricType) {
707        if let Some(family) = self.families.get(name) {
708            assert_eq!(
709                family.help, help,
710                "metric family `{}` registered with inconsistent help text",
711                name
712            );
713            assert_eq!(
714                family.metric_type.as_str(),
715                metric_type.as_str(),
716                "metric family `{}` registered with inconsistent metric type",
717                name
718            );
719        }
720    }
721
722    fn insert_metric_entry(
723        &mut self,
724        id: usize,
725        help: String,
726        metric_type: MetricType,
727        entry: PendingMetricEntry,
728    ) {
729        let PendingMetricEntry {
730            family_name,
731            attributes,
732            encode_samples,
733            metric_any,
734        } = entry;
735        self.keys
736            .insert((family_name.clone(), attributes.clone()), id);
737        let family = match self.families.entry(family_name.clone()) {
738            std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(),
739            std::collections::btree_map::Entry::Vacant(entry) => {
740                let mut descriptor = String::new();
741                encode_descriptor(&mut descriptor, &family_name, &help, metric_type)
742                    .expect("encoding cached descriptor failed");
743                entry.insert(MetricFamily {
744                    help,
745                    metric_type,
746                    descriptor,
747                    metric_ids: Vec::new(),
748                })
749            }
750        };
751        let family_index = family.metric_ids.len();
752        family.metric_ids.push(id);
753        self.metric_slot_mut(id).replace(MetricEntry {
754            family_name,
755            attributes,
756            encode_samples,
757            metric_any,
758            claims: 1,
759            family_index,
760        });
761    }
762
763    fn claim_registration(&mut self, id: usize) {
764        let entry = self.metric_mut(id);
765        entry.claims = entry
766            .claims
767            .checked_add(1)
768            .expect("registration claims overflow");
769    }
770
771    fn release_registration(&mut self, id: usize) {
772        let entry = self.metric_mut(id);
773        entry.claims = entry
774            .claims
775            .checked_sub(1)
776            .expect("registration claim count underflow");
777        if entry.claims > 0 {
778            return;
779        }
780        self.drop_metric_entry(id);
781    }
782
783    fn drop_metric_entry(&mut self, id: usize) {
784        let metric = self
785            .metrics
786            .get_mut(id)
787            .and_then(Option::take)
788            .expect("metric id missing from registry");
789        let MetricEntry {
790            family_name,
791            attributes,
792            family_index,
793            ..
794        } = metric;
795        let key = (family_name, attributes);
796        if self.keys.get(&key).copied() == Some(id) {
797            self.keys.remove(&key);
798        }
799        let (family_name, _) = key;
800        let (swapped_metric_id, remove_family) = {
801            let family = self
802                .families
803                .get_mut(&family_name)
804                .expect("family missing during unregister");
805            let removed = family.metric_ids.swap_remove(family_index);
806            assert_eq!(removed, id, "family index mismatch during unregister");
807            let swapped = family.metric_ids.get(family_index).copied();
808            (swapped, family.metric_ids.is_empty())
809        };
810        if let Some(swapped_metric_id) = swapped_metric_id {
811            self.metric_mut(swapped_metric_id).family_index = family_index;
812        }
813        if remove_family {
814            self.families.remove(&family_name);
815        }
816        self.free_metric_ids.push(id);
817    }
818
819    pub fn encode(&self) -> String {
820        let mut output = String::new();
821        let mut samples = String::new();
822        for family in self.families.values() {
823            samples.clear();
824            for metric_id in &family.metric_ids {
825                let metric = self.metric_ref(*metric_id);
826                (metric.encode_samples)(&mut samples).expect("encoding live metric samples failed");
827            }
828            // Suppress the HELP/TYPE descriptor when the family produced no
829            // samples (e.g. a `Family<S, M>` with no child entries). Matches
830            // upstream prometheus-client's empty-metric filtering.
831            if samples.is_empty() {
832                continue;
833            }
834            output.push_str(&family.descriptor);
835            output.push_str(&samples);
836        }
837
838        encode_eof(&mut output).expect("encoding EOF failed");
839        output
840    }
841}
842
843pub(crate) struct Scope {
844    registry: Registry,
845    prefix: String,
846}
847
848pub(crate) trait Register {
849    /// Register a metric under this scope's prefix.
850    fn register<M: Metric>(&mut self, name: &str, help: &str, metric: M) -> Registered<M>;
851
852    /// Create a child scope by appending `prefix` to the current prefix.
853    fn sub_registry(&mut self, prefix: &str) -> Scope;
854}
855
856impl Register for Registry {
857    fn register<M: Metric>(&mut self, name: &str, help: &str, metric: M) -> Registered<M> {
858        validate_label(name);
859        Self::register(
860            self,
861            name.to_string(),
862            help.to_string(),
863            Vec::new(),
864            Arc::new(metric),
865        )
866    }
867
868    fn sub_registry(&mut self, prefix: &str) -> Scope {
869        validate_label(prefix);
870        Scope {
871            registry: self.clone(),
872            prefix: prefix.to_string(),
873        }
874    }
875}
876
877impl Register for Scope {
878    fn register<M: Metric>(&mut self, name: &str, help: &str, metric: M) -> Registered<M> {
879        validate_label(name);
880        let name = prefixed_name(&self.prefix, name);
881        let help = help.to_string();
882        let metric = Arc::new(metric);
883        Registry::register(&self.registry, name, help, Vec::new(), metric)
884    }
885
886    fn sub_registry(&mut self, prefix: &str) -> Scope {
887        validate_label(prefix);
888        Self {
889            registry: self.registry.clone(),
890            prefix: prefixed_name(&self.prefix, prefix),
891        }
892    }
893}
894
895#[cfg(test)]
896mod tests {
897    use super::*;
898    use crate::{Metrics as _, Runner, Spawner, Supervisor as _, deterministic};
899    use commonware_macros::test_traced;
900    use futures::future;
901    use std::sync::mpsc::{self, TryRecvError};
902
903    #[test]
904    fn test_has_metric_value_unlabeled() {
905        let metrics = "# HELP storage_items_tracked items\nstorage_items_tracked 2\n";
906        assert!(has_metric_value(metrics, "items_tracked", 2));
907        assert!(has_metric_value(metrics, "storage_items_tracked", 2));
908        assert!(!has_metric_value(metrics, "items_tracked_extra", 2));
909        assert!(!has_metric_value(metrics, "items_tracked", 3));
910    }
911
912    #[test]
913    fn test_has_metric_value_labeled() {
914        let metrics = r#"storage_init_items_tracked{index="2"} 2"#;
915        assert!(has_metric_value(metrics, "items_tracked", 2));
916        assert!(has_metric_value(metrics, "storage_init_items_tracked", 2));
917    }
918
919    #[test]
920    fn test_metric_samples() {
921        let metrics = r#"
922            # HELP storage_items_tracked items
923            storage_items_tracked 2
924            storage_init_items_tracked{index="2",path="a}b",quote="a\"b"} 3
925            storage_other_items_tracked{index="4"} 5 123
926            otheritems_tracked 6
927            storage_items_tracked_extra 6
928            storage_items_tracked{broken="label} 7
929            storage_items_tracked{index="8"}
930        "#;
931
932        assert_eq!(
933            metric_samples(metrics, "items_tracked").collect::<Vec<_>>(),
934            vec![
935                ("", "2"),
936                (r#"index="2",path="a}b",quote="a\"b""#, "3"),
937                (r#"index="4""#, "5"),
938            ]
939        );
940        assert_eq!(
941            metric_samples(metrics, "storage_init_items_tracked").collect::<Vec<_>>(),
942            vec![(r#"index="2",path="a}b",quote="a\"b""#, "3")]
943        );
944    }
945
946    #[test_traced]
947    fn test_count_running_tasks() {
948        let executor = deterministic::Runner::default();
949        executor.start(|context| async move {
950            // Initially no tasks with "worker" prefix
951            assert_eq!(
952                count_running_tasks(&context, "worker"),
953                0,
954                "no worker tasks initially"
955            );
956
957            // Spawn a task under a labeled context that stays running
958            let handle1 = context.child("worker").spawn(|_| async move {
959                future::pending::<()>().await;
960            });
961
962            // Count running tasks with "worker" prefix
963            let count = count_running_tasks(&context, "worker");
964            assert_eq!(count, 1, "worker task should be running");
965
966            // Non-matching prefix should return 0
967            assert_eq!(
968                count_running_tasks(&context, "other"),
969                0,
970                "no tasks with 'other' prefix"
971            );
972
973            // Spawn a nested task (worker_child)
974            let handle2 = context
975                .child("worker")
976                .child("child")
977                .spawn(|_| async move {
978                    future::pending::<()>().await;
979                });
980
981            // Count should include both parent and nested tasks
982            let count = count_running_tasks(&context, "worker");
983            assert_eq!(count, 2, "both worker and worker_child should be counted");
984
985            // Abort parent task
986            handle1.abort();
987            let _ = handle1.await;
988
989            // Only nested task remains
990            let count = count_running_tasks(&context, "worker");
991            assert_eq!(count, 1, "only worker_child should remain");
992
993            // Abort nested task
994            handle2.abort();
995            let _ = handle2.await;
996
997            // All tasks stopped
998            assert_eq!(
999                count_running_tasks(&context, "worker"),
1000                0,
1001                "all worker tasks should be stopped"
1002            );
1003        });
1004    }
1005
1006    #[test_traced]
1007    fn test_no_duplicate_metrics() {
1008        let executor = deterministic::Runner::default();
1009        executor.start(|context| async move {
1010            // Register metrics under different labels (no duplicates)
1011            let c1 = raw::Counter::<u64>::default();
1012            let _metric_a = context.child("a").register("test", "help", c1);
1013            let c2 = raw::Counter::<u64>::default();
1014            let _metric_b = context.child("b").register("test", "help", c2);
1015        });
1016        // Test passes if runtime doesn't panic on shutdown
1017    }
1018
1019    #[test_traced]
1020    fn test_duplicate_metrics_reuse_existing_handle() {
1021        let executor = deterministic::Runner::default();
1022        executor.start(|context| async move {
1023            let c1 = raw::Counter::<u64>::default();
1024            let metric_a = context.child("a").register("test", "help", c1);
1025            let c2 = raw::Counter::<u64>::default();
1026            let metric_b = context.child("a").register("test", "help", c2);
1027
1028            assert!(std::ptr::eq(metric_a.metric(), metric_b.metric()));
1029
1030            metric_a.inc();
1031            metric_b.inc_by(2);
1032            let encoded = context.encode();
1033            assert!(encoded.contains("a_test_total 3"));
1034        });
1035    }
1036
1037    #[test]
1038    fn test_claims_track_register_calls_not_handle_clones() {
1039        let registry = Registry::new();
1040        let key: MetricKey = ("votes".to_string(), Vec::new());
1041
1042        let first = registry.register(
1043            key.0.clone(),
1044            "vote count".to_string(),
1045            Vec::new(),
1046            Arc::new(raw::Counter::<u64>::default()),
1047        );
1048        let first_clone = first.clone();
1049        let id = {
1050            let registry = registry.inner.lock();
1051            let id = *registry.keys.get(&key).expect("metric key missing");
1052            assert_eq!(registry.metric_ref(id).claims, 1);
1053            id
1054        };
1055
1056        let second = registry.register(
1057            key.0,
1058            "vote count".to_string(),
1059            Vec::new(),
1060            Arc::new(raw::Counter::<u64>::default()),
1061        );
1062        let second_clone = second.clone();
1063        {
1064            let registry = registry.inner.lock();
1065            assert_eq!(registry.metric_ref(id).claims, 2);
1066        }
1067
1068        drop(first);
1069        drop(second);
1070        {
1071            let registry = registry.inner.lock();
1072            assert_eq!(registry.metric_ref(id).claims, 2);
1073        }
1074
1075        drop(second_clone);
1076        {
1077            let registry = registry.inner.lock();
1078            assert_eq!(registry.metric_ref(id).claims, 1);
1079        }
1080
1081        drop(first_clone);
1082        let registry = registry.inner.lock();
1083        assert!(
1084            registry.keys.is_empty(),
1085            "keys left behind: {:?}",
1086            registry.keys
1087        );
1088        assert!(
1089            registry.families.is_empty(),
1090            "families left behind: {:?}",
1091            registry.families
1092        );
1093    }
1094
1095    #[test]
1096    #[should_panic(expected = "registered with different type")]
1097    fn test_duplicate_metrics_different_type_panics() {
1098        let executor = deterministic::Runner::default();
1099        executor.start(|context| async move {
1100            let counter = raw::Counter::<u64>::default();
1101            let _metric_a = context.child("a").register("test", "help", counter);
1102            let gauge = raw::Gauge::<i64>::default();
1103            let _metric_b = context.child("a").register("test", "help", gauge);
1104        });
1105    }
1106
1107    #[test]
1108    fn test_duplicate_register_acquires_during_last_drop_window() {
1109        let registry = Registry::new();
1110        let key: MetricKey = ("votes".to_string(), Vec::new());
1111
1112        let original = registry.register(
1113            key.0.clone(),
1114            "vote count".to_string(),
1115            Vec::new(),
1116            Arc::new(raw::Counter::<u64>::default()),
1117        );
1118        let original_metric = Arc::clone(&original.metric);
1119        let _original = std::mem::ManuallyDrop::new(original);
1120        let original_id = {
1121            let registry = registry.inner.lock();
1122            *registry.keys.get(&key).expect("metric key missing")
1123        };
1124        // Simulate the final drop after it has decided to clean up but before
1125        // it obtains the registry lock. The dropped claim is still counted in
1126        // this window.
1127        let duplicate = registry.register(
1128            key.0,
1129            "vote count".to_string(),
1130            Vec::new(),
1131            Arc::new(raw::Counter::<u64>::default()),
1132        );
1133        assert!(Arc::ptr_eq(&original_metric, &duplicate.metric));
1134
1135        registry.inner.lock().release_registration(original_id);
1136
1137        duplicate.inc_by(7);
1138        let encoded = registry.encode();
1139        assert!(
1140            encoded.contains("votes_total 7"),
1141            "last drop removed duplicate registration: {encoded}"
1142        );
1143
1144        drop(duplicate);
1145        let registry = registry.inner.lock();
1146        assert!(
1147            registry.keys.is_empty(),
1148            "keys left behind: {:?}",
1149            registry.keys
1150        );
1151        assert!(
1152            registry.families.is_empty(),
1153            "families left behind: {:?}",
1154            registry.families
1155        );
1156    }
1157
1158    #[test]
1159    fn test_registered_with_registration_notifies_on_last_drop() {
1160        struct NotifyOnDrop(mpsc::Sender<&'static str>);
1161
1162        impl Drop for NotifyOnDrop {
1163            fn drop(&mut self) {
1164                let _ = self.0.send("dropped");
1165            }
1166        }
1167
1168        let (tx, rx) = mpsc::channel();
1169        let registered = Registered::with_registration(
1170            raw::Counter::<u64>::default(),
1171            Registration::from(NotifyOnDrop(tx)),
1172        );
1173        let clone = registered.clone();
1174
1175        drop(registered);
1176        assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
1177
1178        drop(clone);
1179        assert_eq!(rx.recv().unwrap(), "dropped");
1180        assert!(matches!(rx.try_recv(), Err(TryRecvError::Disconnected)));
1181    }
1182
1183    fn register_counter(registry: &Registry, name: &str, help: &str, value: u64) -> Counter {
1184        let counter = raw::Counter::<u64>::default();
1185        counter.inc_by(value);
1186        registry.register(
1187            name.to_string(),
1188            help.to_string(),
1189            Vec::new(),
1190            Arc::new(counter),
1191        )
1192    }
1193
1194    #[test]
1195    fn test_encode_is_deterministic() {
1196        let registry = Registry::default();
1197        let _beta = register_counter(&registry, "beta", "beta counter", 2);
1198        let _alpha = register_counter(&registry, "alpha", "alpha counter", 1);
1199        let first = registry.encode();
1200        let second = registry.encode();
1201        assert_eq!(first, second);
1202        let alpha = first
1203            .find("# TYPE alpha")
1204            .expect("alpha family header present");
1205        let beta = first
1206            .find("# TYPE beta")
1207            .expect("beta family header present");
1208        assert!(alpha < beta, "families emitted in sorted order: {first}");
1209    }
1210
1211    #[test]
1212    fn test_encode_emits_single_eof() {
1213        let registry = Registry::default();
1214        let _a = register_counter(&registry, "a", "help", 1);
1215        let _b = register_counter(&registry, "b", "help", 2);
1216        let encoded = registry.encode();
1217        assert_eq!(encoded.matches("# EOF").count(), 1);
1218        assert!(
1219            encoded.ends_with("# EOF\n"),
1220            "must terminate with EOF: {encoded}"
1221        );
1222    }
1223
1224    #[test]
1225    fn test_encode_type_aware_suffixes() {
1226        let registry = Registry::default();
1227        let _requests = register_counter(&registry, "requests", "request count", 3);
1228        let histogram = raw::Histogram::new([0.1, 1.0, 10.0]);
1229        histogram.observe(0.5);
1230        let _histogram = registry.register(
1231            "latency".to_string(),
1232            "latency seconds".to_string(),
1233            Vec::new(),
1234            Arc::new(histogram),
1235        );
1236        let encoded = registry.encode();
1237        assert!(
1238            encoded.contains("requests_total 3"),
1239            "counter _total suffix: {encoded}"
1240        );
1241        assert!(
1242            encoded.contains("latency_bucket"),
1243            "histogram _bucket suffix: {encoded}"
1244        );
1245        assert!(
1246            encoded.contains("latency_sum"),
1247            "histogram _sum suffix: {encoded}"
1248        );
1249        assert!(
1250            encoded.contains("latency_count"),
1251            "histogram _count suffix: {encoded}"
1252        );
1253    }
1254
1255    #[test]
1256    fn test_encode_shares_family_header_across_attributes() {
1257        let registry = Registry::default();
1258        let c1 = raw::Counter::<u64>::default();
1259        c1.inc();
1260        let _c1 = registry.register(
1261            "votes".to_string(),
1262            "vote count".to_string(),
1263            vec![("epoch".to_string(), "1".to_string())],
1264            Arc::new(c1),
1265        );
1266        let c2 = raw::Counter::<u64>::default();
1267        c2.inc_by(2);
1268        let _c2 = registry.register(
1269            "votes".to_string(),
1270            "vote count".to_string(),
1271            vec![("epoch".to_string(), "2".to_string())],
1272            Arc::new(c2),
1273        );
1274        let encoded = registry.encode();
1275        assert_eq!(
1276            encoded.matches("# HELP votes").count(),
1277            1,
1278            "single HELP: {encoded}"
1279        );
1280        assert_eq!(
1281            encoded.matches("# TYPE votes").count(),
1282            1,
1283            "single TYPE: {encoded}"
1284        );
1285        assert!(encoded.contains("votes_total{epoch=\"1\"} 1"));
1286        assert!(encoded.contains("votes_total{epoch=\"2\"} 2"));
1287    }
1288
1289    #[test]
1290    fn test_encode_registers_without_prefix() {
1291        let registry = Registry::default();
1292        let _registered = register_counter(&registry, "votes", "vote count", 1);
1293        let encoded = registry.encode();
1294        assert!(
1295            encoded.contains("votes_total 1"),
1296            "no prefix applied: {encoded}"
1297        );
1298        assert!(
1299            encoded.starts_with("# HELP votes"),
1300            "family header at start: {encoded}"
1301        );
1302    }
1303
1304    #[test]
1305    fn test_encode_suppresses_empty_family() {
1306        // A Family registered with no child entries should not emit its HELP/TYPE
1307        // descriptor on scrape. This matches upstream prometheus-client's
1308        // `encode_omit_empty` behavior.
1309        let registry = Registry::default();
1310        let empty_family = raw::Family::<Vec<(String, String)>, raw::Counter>::default();
1311        let _empty_family = registry.register(
1312            "votes".to_string(),
1313            "vote count".to_string(),
1314            Vec::new(),
1315            Arc::new(empty_family),
1316        );
1317        let _ticks = register_counter(&registry, "ticks", "tick count", 1);
1318        let encoded = registry.encode();
1319        assert!(!encoded.contains("votes"), "empty family leaked: {encoded}");
1320        assert!(
1321            encoded.contains("ticks_total 1"),
1322            "populated metric missing: {encoded}"
1323        );
1324        assert_eq!(encoded.matches("# EOF").count(), 1);
1325    }
1326
1327    #[test]
1328    fn test_encode_matches_upstream_registry() {
1329        // Byte-for-byte parity between our `Registry::encode` and upstream
1330        // prometheus-client's `registry::Registry::encode` on an equivalent
1331        // metric set. Covers HELP normalization (trailing `.`), TYPE lines,
1332        // counter `_total` suffix, histogram `_bucket`/`_sum`/`_count`, and
1333        // the single final `# EOF`. Our registry emits families in sorted
1334        // order (see `test_encode_is_deterministic`); upstream preserves
1335        // registration order. Register here in sorted order so the parity
1336        // assertion only flags real format divergences.
1337        let counter = raw::Counter::<u64>::default();
1338        counter.inc_by(7);
1339        let gauge = raw::Gauge::<i64>::default();
1340        gauge.set(-3);
1341        let histogram = raw::Histogram::new([0.1, 1.0]);
1342        histogram.observe(0.5);
1343
1344        let ours = Registry::default();
1345        let _latency = ours.register(
1346            "latency".to_string(),
1347            "request latency seconds".to_string(),
1348            Vec::new(),
1349            Arc::new(histogram.clone()),
1350        );
1351        let _level = ours.register(
1352            "level".to_string(),
1353            "current level".to_string(),
1354            Vec::new(),
1355            Arc::new(gauge.clone()),
1356        );
1357        let _votes = ours.register(
1358            "votes".to_string(),
1359            "number of votes".to_string(),
1360            Vec::new(),
1361            Arc::new(counter.clone()),
1362        );
1363        let ours_encoded = ours.encode();
1364
1365        let mut theirs = registry::Registry::default();
1366        theirs.register("latency", "request latency seconds", histogram);
1367        theirs.register("level", "current level", gauge);
1368        theirs.register("votes", "number of votes", counter);
1369        let mut theirs_encoded = String::new();
1370        encode(&mut theirs_encoded, &theirs).expect("upstream encode failed");
1371
1372        assert_eq!(
1373            ours_encoded, theirs_encoded,
1374            "output diverged from upstream prometheus-client registry"
1375        );
1376    }
1377
1378    #[test]
1379    fn test_shuffled_duplicate_drops_do_not_leave_registry_entries() {
1380        let registry = Registry::new();
1381        let mut handles = Vec::new();
1382
1383        for _ in 0..8 {
1384            handles.push(registry.register(
1385                "votes".to_string(),
1386                "vote count".to_string(),
1387                Vec::new(),
1388                Arc::new(raw::Counter::<u64>::default()),
1389            ));
1390        }
1391
1392        for index in [3, 0, 6, 1] {
1393            let _ = handles.swap_remove(index);
1394            handles.push(registry.register(
1395                "votes".to_string(),
1396                "vote count".to_string(),
1397                Vec::new(),
1398                Arc::new(raw::Counter::<u64>::default()),
1399            ));
1400        }
1401
1402        drop(handles);
1403        let registry = registry.inner.lock();
1404        assert!(
1405            registry.keys.is_empty(),
1406            "keys left behind: {:?}",
1407            registry.keys
1408        );
1409        assert!(
1410            registry.families.is_empty(),
1411            "families left behind: {:?}",
1412            registry.families
1413        );
1414    }
1415}