Skip to main content

commonware_runtime/telemetry/metrics/
mod.rs

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