Skip to main content

saluki_core/observability/metrics/
mod.rs

1//! Internal metrics support.
2//!
3//! Includes the [`MetricsStream`] broadcast of internally emitted metrics events and the
4//! [`Reflector`]-based [`AggregatedMetricsState`] view that downstream callers query for
5//! Prometheus exposition.
6
7use std::{
8    num::NonZeroUsize,
9    pin::Pin,
10    sync::{
11        atomic::{AtomicBool, AtomicU32, Ordering},
12        Arc, LazyLock, Mutex, OnceLock,
13    },
14    task::{self, ready, Poll},
15    time::Duration,
16};
17
18use async_trait::async_trait;
19use futures::Stream;
20use metrics::{
21    atomics::AtomicU64, Counter, CounterFn, Gauge, GaugeFn, Histogram, HistogramFn, Key, KeyName, Level, Metadata,
22    Recorder, SetRecorderError, SharedString, Unit,
23};
24use metrics_util::storage::AtomicBucket;
25use saluki_common::{collections::FastHashMap, sync::shutdown::ShutdownHandle};
26use saluki_context::{
27    origin::RawOrigin,
28    tags::{Tag, TagSet},
29    Context, ContextResolver, ContextResolverBuilder,
30};
31use saluki_error::GenericError;
32use tokio::{
33    select,
34    sync::broadcast::{self, error::RecvError, Receiver},
35};
36use tokio_util::sync::ReusableBoxFuture;
37use tracing::debug;
38
39use crate::{
40    data_model::event::{metric::*, Event},
41    runtime::{InitializationError, Supervisable, SupervisorFuture},
42};
43
44mod aggregated;
45pub use self::aggregated::{
46    get_shared_metrics_state, AggregatedMetricValue, AggregatedMetricsProcessor, AggregatedMetricsState,
47};
48
49mod histogram;
50pub use self::histogram::AggregatedHistogram;
51
52mod processor;
53pub use self::processor::TelemetryProcessor;
54
55mod reflector;
56pub use self::reflector::{Processor, Reflector};
57
58mod remapper;
59pub use self::remapper::{RemappedMetric, RemapperRule};
60
61const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
62const INTERNAL_METRICS_INTERNER_SIZE: NonZeroUsize = NonZeroUsize::new(16_384).unwrap();
63
64/// Number of consecutive flush intervals that a metric must go without a live handle and without being re-registered
65/// before it is evicted from the registry.
66///
67/// This grace period prevents metrics that are emitted through `metrics` macros without caching the returned handle --
68/// and that therefore re-register on every call -- from being churned in and out of the registry while they are still
69/// being emitted. Naturally, this only applies to metrics which re-register within `IDLE_EVICT_INTERVALS *
70/// FLUSH_INTERVAL`, which is 3 seconds by default... but that's a tradeoff we're making here to avoid unbounded growth.
71const IDLE_EVICT_INTERVALS: u32 = 3;
72
73static RECEIVER_STATE: OnceLock<Arc<State>> = OnceLock::new();
74
75/// A batch of metric updates produced by a single flush.
76#[derive(Clone, Default)]
77pub struct MetricsSnapshot {
78    /// Updates to active metrics.
79    pub upserts: Vec<Event>,
80
81    /// Metrics which are no longer active and should be removed.
82    pub evictions: Vec<Context>,
83}
84
85/// A [`MetricsSnapshot`] that can be cheaply cloned and shared between multiple consumers.
86pub type SharedMetricsSnapshot = Arc<MetricsSnapshot>;
87
88struct Handle<T> {
89    inner: T,
90    level: Level,
91    idle: AtomicU32,
92}
93
94impl<T> Handle<T> {
95    const fn new(inner: T, level: Level) -> Self {
96        Self {
97            inner,
98            level,
99            idle: AtomicU32::new(0),
100        }
101    }
102
103    fn level(&self) -> Level {
104        self.level
105    }
106
107    fn reset_idle(&self) {
108        self.idle.store(0, Ordering::Relaxed);
109    }
110
111    fn bump_idle(&self) -> u32 {
112        self.idle.fetch_add(1, Ordering::Relaxed).saturating_add(1)
113    }
114}
115
116struct CounterInner {
117    value: AtomicU64,
118}
119
120impl Handle<CounterInner> {
121    const fn new_counter(level: Level) -> Self {
122        Self::new(
123            CounterInner {
124                value: AtomicU64::new(0),
125            },
126            level,
127        )
128    }
129
130    /// Consumes the accumulated delta since the last flush, resetting the counter to zero.
131    fn consume(&self) -> u64 {
132        self.inner.value.swap(0, Ordering::Relaxed)
133    }
134}
135
136impl CounterFn for Handle<CounterInner> {
137    fn increment(&self, value: u64) {
138        CounterFn::increment(&self.inner.value, value);
139    }
140
141    fn absolute(&self, value: u64) {
142        CounterFn::absolute(&self.inner.value, value);
143    }
144}
145
146struct GaugeInner {
147    value: AtomicU64,
148    // Tracks whether or not the gauge has been written since the last flush, since the gauge _could_
149    // be modified in a way where the value seen by two consecutive flushes is the same, even though
150    // it _was_ modified between the two and should be considered non-idle.
151    dirty: AtomicBool,
152}
153
154impl Handle<GaugeInner> {
155    const fn new_gauge(level: Level) -> Self {
156        Self::new(
157            GaugeInner {
158                value: AtomicU64::new(0),
159                dirty: AtomicBool::new(false),
160            },
161            level,
162        )
163    }
164
165    /// Reads the current gauge value.
166    fn load(&self) -> f64 {
167        f64::from_bits(self.inner.value.load(Ordering::Relaxed))
168    }
169
170    /// Returns whether the gauge was written since the last flush, clearing the flag.
171    fn take_dirty(&self) -> bool {
172        self.inner.dirty.swap(false, Ordering::Relaxed)
173    }
174}
175
176impl GaugeFn for Handle<GaugeInner> {
177    fn increment(&self, value: f64) {
178        GaugeFn::increment(&self.inner.value, value);
179        self.inner.dirty.store(true, Ordering::Relaxed);
180    }
181
182    fn decrement(&self, value: f64) {
183        GaugeFn::decrement(&self.inner.value, value);
184        self.inner.dirty.store(true, Ordering::Relaxed);
185    }
186
187    fn set(&self, value: f64) {
188        GaugeFn::set(&self.inner.value, value);
189        self.inner.dirty.store(true, Ordering::Relaxed);
190    }
191}
192
193/// Storage for a single histogram, shared between the registry and every caller-held [`Histogram`].
194struct HistogramInner {
195    value: AtomicBucket<f64>,
196}
197
198impl Handle<HistogramInner> {
199    fn new_histogram(level: Level) -> Self {
200        Self::new(
201            HistogramInner {
202                value: AtomicBucket::new(),
203            },
204            level,
205        )
206    }
207
208    /// Drains all recorded samples since the last flush into `out`, clearing the histogram.
209    fn drain_into(&self, out: &mut Vec<f64>) {
210        self.inner.value.clear_with(|samples| out.extend(samples));
211    }
212}
213
214impl HistogramFn for Handle<HistogramInner> {
215    fn record(&self, value: f64) {
216        self.inner.value.push(value);
217    }
218}
219
220/// A registry for all internal metrics.
221///
222/// Optimized for simplicity and the ability to efficiently track metric state and evict idle metrics. Not optimized for
223/// high concurrency with regards to registration, so metric handles should always be held for as long as possible, when
224/// possible.
225#[derive(Default)]
226struct MetricsRegistry {
227    maps: Mutex<RegistryMaps>,
228}
229
230#[derive(Default)]
231struct RegistryMaps {
232    counters: FastHashMap<Key, Arc<Handle<CounterInner>>>,
233    gauges: FastHashMap<Key, Arc<Handle<GaugeInner>>>,
234    histograms: FastHashMap<Key, Arc<Handle<HistogramInner>>>,
235}
236
237impl MetricsRegistry {
238    /// Returns a handle to the counter for `key`, creating it at `level` if it doesn't yet exist.
239    ///
240    /// The level is fixed when the metric is first created; re-registration only refreshes the idle
241    /// counter (the activity signal that keeps actively emitted metrics from being evicted).
242    fn get_or_create_counter(&self, key: &Key, level: Level) -> Counter {
243        let mut maps = self.maps.lock().unwrap();
244        let handle = if let Some(handle) = maps.counters.get(key) {
245            handle.reset_idle();
246            Arc::clone(handle)
247        } else {
248            let handle = Arc::new(Handle::new_counter(level));
249            maps.counters.insert(key.clone(), Arc::clone(&handle));
250            handle
251        };
252
253        Counter::from_arc(handle)
254    }
255
256    fn remove_counter(&self, key: &Key) -> Option<Arc<Handle<CounterInner>>> {
257        let mut maps = self.maps.lock().unwrap();
258        maps.counters.remove(key)
259    }
260
261    /// Returns a handle to the gauge for `key`, creating it at `level` if it doesn't yet exist.
262    fn get_or_create_gauge(&self, key: &Key, level: Level) -> Gauge {
263        let mut maps = self.maps.lock().unwrap();
264        let handle = if let Some(handle) = maps.gauges.get(key) {
265            handle.reset_idle();
266            Arc::clone(handle)
267        } else {
268            let handle = Arc::new(Handle::new_gauge(level));
269            maps.gauges.insert(key.clone(), Arc::clone(&handle));
270            handle
271        };
272
273        Gauge::from_arc(handle)
274    }
275
276    /// Returns a handle to the histogram for `key`, creating it at `level` if it doesn't yet exist.
277    fn get_or_create_histogram(&self, key: &Key, level: Level) -> Histogram {
278        let mut maps = self.maps.lock().unwrap();
279        let handle = if let Some(handle) = maps.histograms.get(key) {
280            handle.reset_idle();
281            Arc::clone(handle)
282        } else {
283            let handle = Arc::new(Handle::new_histogram(level));
284            maps.histograms.insert(key.clone(), Arc::clone(&handle));
285            handle
286        };
287
288        Histogram::from_arc(handle)
289    }
290}
291
292/// Handle to the metrics filter.
293///
294/// Allows for overriding the current metrics filter level, which influences which metrics are emitted to downstream
295/// receivers.
296pub struct FilterHandle {
297    state: Arc<State>,
298}
299
300impl FilterHandle {
301    /// Overrides the current metrics filter level.
302    pub fn override_filter(&self, level: Level) {
303        *self.state.current_level.lock().unwrap() = level;
304    }
305
306    /// Resets the metrics filter level to the default that was configured when the metrics subsystem was initialized.
307    pub fn reset_filter(&self) {
308        *self.state.current_level.lock().unwrap() = self.state.default_level;
309    }
310}
311
312struct State {
313    registry: MetricsRegistry,
314    flush_tx: broadcast::Sender<SharedMetricsSnapshot>,
315    metrics_prefix: String,
316    default_level: Level,
317    current_level: Mutex<Level>,
318    idle_evict_intervals: u32,
319    flush_interval: Duration,
320}
321
322struct MetricsRecorder {
323    state: Arc<State>,
324}
325
326impl MetricsRecorder {
327    fn new(metrics_prefix: String, default_level: Level) -> Self {
328        let (flush_tx, _) = broadcast::channel(2);
329        Self {
330            state: Arc::new(State {
331                registry: MetricsRegistry::default(),
332                flush_tx,
333                metrics_prefix,
334                default_level,
335                current_level: Mutex::new(default_level),
336                idle_evict_intervals: IDLE_EVICT_INTERVALS,
337                flush_interval: FLUSH_INTERVAL,
338            }),
339        }
340    }
341
342    fn filter_handle(&self) -> FilterHandle {
343        FilterHandle {
344            state: Arc::clone(&self.state),
345        }
346    }
347
348    fn install(self) -> Result<(), SetRecorderError<Self>> {
349        let state = Arc::clone(&self.state);
350        metrics::set_global_recorder(self)?;
351
352        if RECEIVER_STATE.set(state).is_err() {
353            panic!("metrics receiver should never be set prior to global recorder being installed");
354        }
355
356        Ok(())
357    }
358
359    fn prefix_key(&self, key: &Key) -> Key {
360        Key::from_parts(format!("{}.{}", self.state.metrics_prefix, key.name()), key.labels())
361    }
362}
363
364impl Recorder for MetricsRecorder {
365    fn describe_counter(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
366    fn describe_gauge(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
367    fn describe_histogram(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
368
369    fn register_counter(&self, key: &Key, metadata: &Metadata<'_>) -> Counter {
370        let prefixed_key = self.prefix_key(key);
371        self.state
372            .registry
373            .get_or_create_counter(&prefixed_key, *metadata.level())
374    }
375
376    fn register_gauge(&self, key: &Key, metadata: &Metadata<'_>) -> Gauge {
377        let prefixed_key = self.prefix_key(key);
378        self.state
379            .registry
380            .get_or_create_gauge(&prefixed_key, *metadata.level())
381    }
382
383    fn register_histogram(&self, key: &Key, metadata: &Metadata<'_>) -> Histogram {
384        let prefixed_key = self.prefix_key(key);
385        self.state
386            .registry
387            .get_or_create_histogram(&prefixed_key, *metadata.level())
388    }
389}
390
391/// Deletes an internal counter from the metrics registry.
392///
393/// If the counter exists and metrics snapshots are being consumed, this sends any pending counter
394/// delta before the eviction update so downstream consumers do not lose the final increment.
395pub fn delete_counter(key: Key) -> bool {
396    let Some(state) = RECEIVER_STATE.get() else {
397        return false;
398    };
399
400    let prefixed_key = Key::from_parts(format!("{}.{}", state.metrics_prefix, key.name()), key.labels());
401    let Some(snapshot) = delete_counter_from_state(state, &prefixed_key) else {
402        return false;
403    };
404
405    if state.flush_tx.receiver_count() > 0 {
406        let _ = state.flush_tx.send(Arc::new(snapshot));
407    }
408
409    true
410}
411
412fn delete_counter_from_state(state: &State, prefixed_key: &Key) -> Option<MetricsSnapshot> {
413    let handle = state.registry.remove_counter(prefixed_key)?;
414    let delta = handle.consume();
415    let context = context_from_key(prefixed_key);
416    let current_level = *state.current_level.lock().unwrap();
417
418    let mut snapshot = MetricsSnapshot {
419        upserts: Vec::new(),
420        evictions: vec![context.clone()],
421    };
422    if delta > 0 && handle.level() >= current_level {
423        snapshot
424            .upserts
425            .push(Event::Metric(Metric::counter(context, delta as f64)));
426    }
427
428    Some(snapshot)
429}
430
431fn context_from_key(key: &Key) -> Context {
432    let tags = key
433        .labels()
434        .map(|l| Tag::from(format!("{}:{}", l.key(), l.value())))
435        .collect::<TagSet>();
436
437    Context::from_parts(key.name().to_string(), tags)
438}
439
440/// Internal metrics stream
441///
442/// Used to receive periodic snapshots of the internal metrics registry, which contains all metrics that are currently
443/// active within the process.
444pub struct MetricsStream {
445    inner: ReusableBoxFuture<
446        'static,
447        (
448            Result<SharedMetricsSnapshot, RecvError>,
449            Receiver<SharedMetricsSnapshot>,
450        ),
451    >,
452}
453
454impl MetricsStream {
455    /// Creates a new `MetricsStream` that receives updates from the internal metrics registry.
456    pub fn register() -> Self {
457        let state = RECEIVER_STATE.get().expect("metrics receiver should be set");
458        Self {
459            inner: ReusableBoxFuture::new(make_rx_future(state.flush_tx.subscribe())),
460        }
461    }
462}
463
464impl Stream for MetricsStream {
465    type Item = SharedMetricsSnapshot;
466
467    fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
468        loop {
469            // Poll the receiver, and rearm the future once we actually resolve it.
470            let (result, rx) = ready!(self.inner.poll(cx));
471            self.inner.set(make_rx_future(rx));
472
473            match result {
474                Ok(item) => return Poll::Ready(Some(item)),
475                Err(RecvError::Closed) => return Poll::Ready(None),
476                Err(RecvError::Lagged(n)) => {
477                    debug!(
478                        missed_payloads = n,
479                        "Stream lagging behind internal metrics producer. Internal metrics may have been lost."
480                    );
481                    continue;
482                }
483            }
484        }
485    }
486}
487
488async fn make_rx_future(
489    mut rx: Receiver<SharedMetricsSnapshot>,
490) -> (
491    Result<SharedMetricsSnapshot, RecvError>,
492    Receiver<SharedMetricsSnapshot>,
493) {
494    let result = rx.recv().await;
495    (result, rx)
496}
497
498#[derive(Default)]
499struct FlushState {
500    counter_emits: Vec<(Key, f64)>,
501    gauge_emits: Vec<(Key, f64)>,
502    histogram_emits: Vec<(Key, Vec<f64>)>,
503    evicted_keys: Vec<Key>,
504}
505
506impl FlushState {
507    fn clear(&mut self) {
508        self.counter_emits.clear();
509        self.gauge_emits.clear();
510        self.histogram_emits.clear();
511        self.evicted_keys.clear();
512    }
513}
514
515async fn flush_metrics() {
516    let mut context_resolver = MetricsContextResolver::new(INTERNAL_METRICS_INTERNER_SIZE);
517
518    let state = RECEIVER_STATE.get().expect("metrics receiver should be set");
519
520    let mut flush_interval = tokio::time::interval(state.flush_interval);
521    flush_interval.tick().await;
522
523    let mut flush_state = FlushState::default();
524
525    loop {
526        flush_interval.tick().await;
527        flush_once(state, &mut context_resolver, &mut flush_state);
528    }
529}
530
531/// Performs a single flush pass over the registry: consumes each metric's value, evicts metrics that
532/// are no longer referenced and have gone idle, and broadcasts the resulting metric values and
533/// evictions to downstream consumers.
534fn flush_once(state: &State, context_resolver: &mut MetricsContextResolver, flush_state: &mut FlushState) {
535    let has_listeners = state.flush_tx.receiver_count() > 0;
536    let current_level = *state.current_level.lock().unwrap();
537    let idle_threshold = state.idle_evict_intervals;
538    let mut snapshot = MetricsSnapshot::default();
539
540    flush_state.clear();
541    let FlushState {
542        counter_emits,
543        gauge_emits,
544        histogram_emits,
545        evicted_keys,
546    } = flush_state;
547
548    {
549        let mut maps = state.registry.maps.lock().unwrap();
550
551        // Counters: a counter is never idle so long as it has a non-zero delta during a flush _or_ has
552        // an active reference (strong count > 1).
553        //
554        // The strong-count check precedes `consume()`, with an Acquire fence on the orphaned path:
555        // observing `strong_count == 1` synchronizes-with the dropping thread's release, which
556        // guarantees the following `consume()` observes that thread's final increment.
557        maps.counters.retain(|key, handle| {
558            let idle = handle.bump_idle();
559            let orphaned = Arc::strong_count(handle) == 1;
560            if orphaned {
561                std::sync::atomic::fence(Ordering::Acquire);
562            }
563            let delta = handle.consume();
564
565            if orphaned && delta == 0 && idle >= idle_threshold {
566                evicted_keys.push(key.clone());
567                return false;
568            }
569
570            if has_listeners && handle.level() >= current_level {
571                counter_emits.push((key.clone(), delta as f64));
572            }
573            true
574        });
575
576        // Gauges: a gauge is never idle so long as it was touched ("dirty") prior to a flush _or_ has
577        // an active reference (strong count > 1).
578        maps.gauges.retain(|key, handle| {
579            let idle = handle.bump_idle();
580            let orphaned = Arc::strong_count(handle) == 1;
581            if orphaned {
582                std::sync::atomic::fence(Ordering::Acquire);
583            }
584            let value = handle.load();
585            let written = handle.take_dirty();
586
587            if orphaned && !written && idle >= idle_threshold {
588                evicted_keys.push(key.clone());
589                return false;
590            }
591
592            if has_listeners && handle.level() >= current_level {
593                gauge_emits.push((key.clone(), value));
594            }
595            true
596        });
597
598        // Histograms: a histogram is never idle so long as it has recorded samples prior to a flush _or_
599        // has an active reference (strong count > 1).
600        //
601        // The strong-count check precedes the drain, with an Acquire fence on the orphaned path, so the
602        // drain observes a dropping thread's final `record()`.
603        maps.histograms.retain(|key, handle| {
604            let idle = handle.bump_idle();
605            let orphaned = Arc::strong_count(handle) == 1;
606            if orphaned {
607                std::sync::atomic::fence(Ordering::Acquire);
608            }
609            let mut histogram_samples = Vec::new();
610            handle.drain_into(&mut histogram_samples);
611            let had_samples = !histogram_samples.is_empty();
612
613            if orphaned && !had_samples && idle >= idle_threshold {
614                evicted_keys.push(key.clone());
615                return false;
616            }
617
618            if has_listeners && had_samples && handle.level() >= current_level {
619                histogram_emits.push((key.clone(), histogram_samples));
620            }
621            true
622        });
623    }
624
625    // For every evicted key we collected, take its cached context (if it exists) and add it to the
626    // snapshot's evictions list.
627    for key in evicted_keys {
628        if let Some(context) = context_resolver.take(key) {
629            if has_listeners {
630                snapshot.evictions.push(context);
631            }
632        }
633    }
634
635    if !has_listeners {
636        return;
637    }
638
639    for (key, value) in counter_emits.drain(..) {
640        let context = context_resolver.resolve_from_key(key);
641        snapshot.upserts.push(Event::Metric(Metric::counter(context, value)));
642    }
643    for (key, value) in gauge_emits.drain(..) {
644        let context = context_resolver.resolve_from_key(key);
645        snapshot.upserts.push(Event::Metric(Metric::gauge(context, value)));
646    }
647    for (key, samples) in histogram_emits.drain(..) {
648        let context = context_resolver.resolve_from_key(key);
649        snapshot
650            .upserts
651            .push(Event::Metric(Metric::histogram(context, &samples[..])));
652    }
653
654    if !snapshot.upserts.is_empty() || !snapshot.evictions.is_empty() {
655        let _ = state.flush_tx.send(Arc::new(snapshot));
656    }
657}
658
659struct MetricsContextResolver {
660    context_resolver: ContextResolver,
661    key_context_cache: FastHashMap<Key, Context>,
662}
663
664impl MetricsContextResolver {
665    fn new(resolver_interner_size_bytes: NonZeroUsize) -> Self {
666        Self {
667            // Set up our context resolver without caching, since we will be caching the contexts ourselves.
668            context_resolver: ContextResolverBuilder::from_name("core/internal_metrics")
669                .expect("resolver name is not empty")
670                .with_interner_capacity_bytes(resolver_interner_size_bytes)
671                .without_caching()
672                .build(),
673            key_context_cache: FastHashMap::default(),
674        }
675    }
676
677    fn resolve_from_key(&mut self, key: Key) -> Context {
678        static SELF_ORIGIN_INFO: LazyLock<RawOrigin<'static>> = LazyLock::new(|| {
679            let mut origin_info = RawOrigin::default();
680            origin_info.set_process_id(std::process::id());
681            origin_info
682        });
683
684        // Check the cache first.
685        if let Some(context) = self.key_context_cache.get(&key) {
686            return context.clone();
687        }
688
689        // We don't have the context cached, so we need to resolve it.
690        let tags = key
691            .labels()
692            .map(|l| Tag::from(format!("{}:{}", l.key(), l.value())))
693            .collect::<TagSet>();
694
695        let context = self
696            .context_resolver
697            .resolve(key.name(), &tags, Some(SELF_ORIGIN_INFO.clone()))
698            .expect("resolver should always allow falling back");
699
700        self.key_context_cache.insert(key, context.clone());
701        context
702    }
703
704    /// Removes a key's cached context, returning it if present.
705    ///
706    /// Returns `Some` only if the key had been resolved before (that is, the metric was emitted at
707    /// least once), which is what tells the flush loop whether a downstream eviction needs to be sent.
708    fn take(&mut self, key: &Key) -> Option<Context> {
709        self.key_context_cache.remove(key)
710    }
711}
712
713/// Initializes the metrics subsystem with the given metrics prefix and default filter level.
714///
715/// `default_level` sets the initial filter level for emitted metrics, and is also what the filter is restored to when
716/// [`FilterHandle::reset_filter`] is invoked. Metrics whose level is more verbose than this default are filtered out
717/// until a runtime override is applied via [`FilterHandle::override_filter`].
718///
719/// Returns a [`FilterHandle`] for adjusting the runtime metrics filter, plus a [`MetricsFlusherWorker`]
720/// that must be added to a [`Supervisor`][crate::runtime::Supervisor] in order to drive the periodic
721/// flush loop. Internal metrics aren't propagated to subscribers until the worker is running.
722///
723/// # Errors
724///
725/// If a global recorder was already installed, an error will be returned.
726pub async fn initialize_metrics(
727    metrics_prefix: String, default_level: Level,
728) -> Result<(FilterHandle, MetricsFlusherWorker), GenericError> {
729    let recorder = MetricsRecorder::new(metrics_prefix, default_level);
730    let filter_handle = recorder.filter_handle();
731    recorder.install()?;
732
733    Ok((filter_handle, MetricsFlusherWorker))
734}
735
736/// A worker that periodically flushes the internal metrics registry to broadcast subscribers.
737///
738/// Wraps the internal flush loop and runs it under a [`Supervisor`][crate::runtime::Supervisor]. Must
739/// only be added to a supervisor after [`initialize_metrics`] has been called -- the flush loop
740/// reads from the global recorder state set by that call.
741pub struct MetricsFlusherWorker;
742
743#[async_trait]
744impl Supervisable for MetricsFlusherWorker {
745    fn name(&self) -> &str {
746        "internal-telemetry-metrics-flusher"
747    }
748
749    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
750        Ok(Box::pin(async move {
751            select! {
752                _ = process_shutdown => {},
753                _ = flush_metrics() => {},
754            }
755
756            Ok(())
757        }))
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use metrics::Label;
764    use saluki_common::collections::FastHashSet;
765
766    use super::*;
767
768    const HIGH_CARDINALITY_COUNTER_NAME: &str = "high_cardinality_counter";
769    const HIGH_CARDINALITY_COUNTERS: usize = 64;
770
771    // We keep a live receiver in each test so `flush_once` observes a listener and emits updates.
772    fn make_state(idle_evict_intervals: u32) -> (Arc<State>, Receiver<SharedMetricsSnapshot>) {
773        let (flush_tx, rx) = broadcast::channel(64);
774        let state = Arc::new(State {
775            registry: MetricsRegistry::default(),
776            flush_tx,
777            metrics_prefix: "test".to_string(),
778            default_level: Level::TRACE,
779            current_level: Mutex::new(Level::TRACE),
780            idle_evict_intervals,
781            flush_interval: FLUSH_INTERVAL,
782        });
783        (state, rx)
784    }
785
786    fn resolver() -> MetricsContextResolver {
787        MetricsContextResolver::new(INTERNAL_METRICS_INTERNER_SIZE)
788    }
789
790    // Mimics `MetricsRecorder::register_counter` against a raw key (no prefixing needed in tests).
791    fn register_counter(state: &State, name: &'static str) -> Counter {
792        state
793            .registry
794            .get_or_create_counter(&Key::from_name(name), Level::TRACE)
795    }
796
797    fn register_counter_with_origin(state: &State, name: &'static str, origin: &str) -> Counter {
798        let key = Key::from_parts(name, vec![Label::new("origin", origin.to_string())]);
799        state.registry.get_or_create_counter(&key, Level::TRACE)
800    }
801
802    fn register_gauge(state: &State, name: &'static str) -> Gauge {
803        state.registry.get_or_create_gauge(&Key::from_name(name), Level::TRACE)
804    }
805
806    fn counter_present(state: &State, name: &'static str) -> bool {
807        state
808            .registry
809            .maps
810            .lock()
811            .unwrap()
812            .counters
813            .contains_key(&Key::from_name(name))
814    }
815
816    fn counter_count(state: &State) -> usize {
817        state.registry.maps.lock().unwrap().counters.len()
818    }
819
820    fn gauge_present(state: &State, name: &'static str) -> bool {
821        state
822            .registry
823            .maps
824            .lock()
825            .unwrap()
826            .gauges
827            .contains_key(&Key::from_name(name))
828    }
829
830    fn drain(rx: &mut Receiver<SharedMetricsSnapshot>) -> Vec<MetricsSnapshot> {
831        let mut out = Vec::new();
832        while let Ok(batch) = rx.try_recv() {
833            out.push((*batch).clone());
834        }
835        out
836    }
837
838    fn evicted_names(snapshots: &[MetricsSnapshot]) -> Vec<String> {
839        snapshots
840            .iter()
841            .flat_map(|snapshot| snapshot.evictions.iter().map(|ctx| ctx.name().to_string()))
842            .collect()
843    }
844
845    fn metric_count(state: &AggregatedMetricsState, name: &str) -> usize {
846        let mut count = 0;
847        state.visit_metrics(|context, _| {
848            if context.name() == name {
849                count += 1;
850            }
851        });
852        count
853    }
854
855    #[test]
856    fn delete_counter_from_state_emits_pending_delta_before_eviction() {
857        let (state, _rx) = make_state(IDLE_EVICT_INTERVALS);
858        let key = Key::from_parts(
859            "deleted_counter",
860            vec![Label::new("origin", "container_id://deleted".to_string())],
861        );
862
863        let counter = state.registry.get_or_create_counter(&key, Level::TRACE);
864        counter.increment(7);
865        drop(counter);
866
867        let snapshot = delete_counter_from_state(&state, &key).expect("counter should be deleted");
868
869        assert!(!state.registry.maps.lock().unwrap().counters.contains_key(&key));
870        assert_eq!(snapshot.evictions.len(), 1);
871        assert_eq!(snapshot.evictions[0].name(), "deleted_counter");
872        assert!(snapshot.evictions[0].tags().has_tag("origin:container_id://deleted"));
873
874        assert_eq!(snapshot.upserts.len(), 1);
875        let Event::Metric(metric) = &snapshot.upserts[0] else {
876            panic!("delete should emit the pending counter delta");
877        };
878        assert_eq!(metric.context(), &snapshot.evictions[0]);
879        let MetricValues::Counter(points) = metric.values() else {
880            panic!("delete should emit a counter metric");
881        };
882        assert_eq!(points.into_iter().map(|(_, value)| value).sum::<f64>(), 7.0);
883    }
884
885    #[tokio::test]
886    async fn evicts_high_cardinality_counters_from_registry_and_aggregated_state() {
887        let (state, mut rx) = make_state(IDLE_EVICT_INTERVALS);
888        let mut resolver = resolver();
889        let mut flush_state = FlushState::default();
890        let agg_processor = AggregatedMetricsProcessor;
891        let agg_state = agg_processor.build_initial_state();
892        let mut expected_eviction_tags = FastHashSet::default();
893        let mut observed_eviction_tags = FastHashSet::default();
894
895        for index in 0..HIGH_CARDINALITY_COUNTERS {
896            let origin = format!("entity://source-{index}");
897            expected_eviction_tags.insert(format!("origin:{origin}"));
898
899            let counter = register_counter_with_origin(&state, HIGH_CARDINALITY_COUNTER_NAME, &origin);
900            counter.increment(1);
901            drop(counter);
902        }
903
904        assert_eq!(counter_count(&state), HIGH_CARDINALITY_COUNTERS);
905
906        for _ in 0..IDLE_EVICT_INTERVALS {
907            flush_once(&state, &mut resolver, &mut flush_state);
908
909            for update in drain(&mut rx) {
910                for context in &update.evictions {
911                    if context.name() != HIGH_CARDINALITY_COUNTER_NAME {
912                        continue;
913                    }
914
915                    for tag in &expected_eviction_tags {
916                        if context.tags().has_tag(tag) {
917                            observed_eviction_tags.insert(tag.clone());
918                        }
919                    }
920                }
921
922                agg_processor.process(update, &agg_state);
923            }
924        }
925
926        assert_eq!(counter_count(&state), 0);
927        assert_eq!(metric_count(&agg_state, HIGH_CARDINALITY_COUNTER_NAME), 0);
928        assert_eq!(observed_eviction_tags, expected_eviction_tags);
929    }
930
931    #[tokio::test]
932    async fn evicts_counter_within_bounded_time_after_handle_dropped() {
933        let (state, mut rx) = make_state(3);
934        let mut resolver = resolver();
935        let mut flush_state = FlushState::default();
936
937        let counter = register_counter(&state, "dropped_counter");
938        counter.increment(5);
939
940        // While the handle is held, the metric is retained and its delta is emitted.
941        flush_once(&state, &mut resolver, &mut flush_state);
942        assert!(counter_present(&state, "dropped_counter"));
943
944        // Drop the last handle; the metric must be reclaimed within `idle_evict_intervals` flushes.
945        drop(counter);
946        let _ = drain(&mut rx);
947        for _ in 0..3 {
948            flush_once(&state, &mut resolver, &mut flush_state);
949        }
950        assert!(!counter_present(&state, "dropped_counter"));
951
952        // The eviction is propagated downstream so the aggregated view drops it too.
953        let updates = drain(&mut rx);
954        assert!(evicted_names(&updates).iter().any(|n| n == "dropped_counter"));
955    }
956
957    #[tokio::test]
958    async fn does_not_evict_while_handle_is_held() {
959        let (state, _rx) = make_state(3);
960        let mut resolver = resolver();
961        let mut flush_state = FlushState::default();
962
963        let counter = register_counter(&state, "held_counter");
964
965        // Idle well past the eviction threshold without dropping the handle.
966        for _ in 0..8 {
967            flush_once(&state, &mut resolver, &mut flush_state);
968        }
969
970        assert!(
971            counter_present(&state, "held_counter"),
972            "a held metric must never be evicted"
973        );
974        drop(counter);
975    }
976
977    #[tokio::test]
978    async fn reregistration_keeps_uncached_metric_alive() {
979        // Models the uncached-macro pattern: a fresh handle is registered (and dropped) every interval.
980        // Re-registration resets the idle counter, so the metric is never churned out while in use.
981        let (state, _rx) = make_state(3);
982        let mut resolver = resolver();
983        let mut flush_state = FlushState::default();
984
985        for _ in 0..8 {
986            let counter = register_counter(&state, "uncached_counter");
987            counter.increment(1);
988            drop(counter);
989            flush_once(&state, &mut resolver, &mut flush_state);
990        }
991        assert!(counter_present(&state, "uncached_counter"));
992
993        // Once it stops being re-registered, it is reclaimed within the idle threshold.
994        for _ in 0..3 {
995            flush_once(&state, &mut resolver, &mut flush_state);
996        }
997        assert!(!counter_present(&state, "uncached_counter"));
998    }
999
1000    #[tokio::test]
1001    async fn gauge_reset_to_same_value_is_not_evicted() {
1002        // A gauge re-set to the same value every interval (e.g. a backoff gauge that sits at 0.0) must
1003        // stay alive. Activity is driven by re-registration, not value comparison.
1004        let (state, _rx) = make_state(3);
1005        let mut resolver = resolver();
1006        let mut flush_state = FlushState::default();
1007
1008        for _ in 0..8 {
1009            let gauge = register_gauge(&state, "steady_gauge");
1010            gauge.set(0.0);
1011            drop(gauge);
1012            flush_once(&state, &mut resolver, &mut flush_state);
1013        }
1014        assert!(gauge_present(&state, "steady_gauge"));
1015    }
1016
1017    #[tokio::test]
1018    async fn final_counter_delta_reaches_downstream_before_eviction() {
1019        let (state, mut rx) = make_state(1);
1020        let mut resolver = resolver();
1021        let mut flush_state = FlushState::default();
1022
1023        let counter = register_counter(&state, "final_counter");
1024
1025        // Arm eviction by letting the idle counter reach the threshold while the handle is held.
1026        flush_once(&state, &mut resolver, &mut flush_state);
1027
1028        // Increment and drop the handle in the same inter-flush window. The next flush sees a non-zero
1029        // delta, so it must emit it and defer eviction by one interval (so the delta isn't lost).
1030        counter.increment(7);
1031        drop(counter);
1032        flush_once(&state, &mut resolver, &mut flush_state);
1033        assert!(
1034            counter_present(&state, "final_counter"),
1035            "must not evict in an interval that produced a delta"
1036        );
1037
1038        // The emitted delta must reach the downstream aggregated state.
1039        let agg_processor = AggregatedMetricsProcessor;
1040        let agg_state = agg_processor.build_initial_state();
1041        for update in drain(&mut rx) {
1042            agg_processor.process(update, &agg_state);
1043        }
1044        assert_eq!(agg_state.get_aggregated_with_tags("final_counter", &[]), 7.0);
1045
1046        // The following interval (no delta) finally evicts it.
1047        flush_once(&state, &mut resolver, &mut flush_state);
1048        assert!(!counter_present(&state, "final_counter"));
1049    }
1050
1051    #[tokio::test]
1052    async fn gauge_final_value_reaches_downstream_before_eviction() {
1053        let (state, mut rx) = make_state(1);
1054        let mut resolver = resolver();
1055        let mut flush_state = FlushState::default();
1056
1057        let gauge = register_gauge(&state, "final_gauge");
1058        gauge.set(1.0);
1059
1060        // Arm eviction by letting the idle counter reach the threshold while the handle is held.
1061        flush_once(&state, &mut resolver, &mut flush_state);
1062
1063        // Update to a NEW value and drop the handle in the same inter-flush window. Even though the
1064        // gauge is now orphaned and idle, the write must not be lost: the next flush sees the dirty flag,
1065        // emits the value, and defers eviction by one interval.
1066        gauge.set(42.0);
1067        drop(gauge);
1068        flush_once(&state, &mut resolver, &mut flush_state);
1069        assert!(
1070            gauge_present(&state, "final_gauge"),
1071            "must not evict in an interval that wrote a new value"
1072        );
1073
1074        // The final value must reach the downstream aggregated state.
1075        let agg_processor = AggregatedMetricsProcessor;
1076        let agg_state = agg_processor.build_initial_state();
1077        for update in drain(&mut rx) {
1078            agg_processor.process(update, &agg_state);
1079        }
1080        assert_eq!(agg_state.find_single_with_tags("final_gauge", &[]), Some(42.0));
1081
1082        // The following interval (no write) finally evicts it.
1083        flush_once(&state, &mut resolver, &mut flush_state);
1084        assert!(!gauge_present(&state, "final_gauge"));
1085    }
1086}
1087
1088// Loom model of the counter eviction path in `flush_once`.
1089//
1090// In `flush_once`, the registry `maps` lock is held during the drain, but the increment/drop path
1091// does not take that lock: `Counter::increment` writes straight through the `Arc<Handle>`, and
1092// dropping a caller's `Counter` only decrements the Arc strong count. A component holding a cached
1093// handle can therefore land a final increment and drop the handle while a flush is mid-pass over it.
1094//
1095// The model transcribes the counter branch with loom primitives instead of exercising the real types,
1096// which loom cannot instrument here:
1097//
1098// - The strong count is an explicit `AtomicUsize`. loom's `Arc::strong_count` does not reflect a
1099//   concurrent decrement from another thread (it models Arc's drop synchronization, not the observable
1100//   count value), so a flush reading it never sees the orphaned (`== 1`) state mid-race. The explicit
1101//   atomic matches `std`'s `Arc`: clone increments (Relaxed), drop decrements with `Release`,
1102//   observation is a `Relaxed` load -- the same shape as how `flush_once` reads `Arc::strong_count`.
1103// - The real `Handle` is wrapped by the `metrics` crate's `Counter`/`CounterFn`, which construct
1104//   `std::sync::Arc` internally. loom only instruments atomics and `Arc`s swapped to `loom::sync::*`
1105//   behind the `loom` cfg, not those inside a third-party crate.
1106//
1107// `flush_decision_consume_first` and `flush_decision_strong_count_first` mirror the two possible
1108// orderings of `flush_once`'s counter branch and must stay in lockstep with it.
1109#[cfg(all(test, feature = "loom"))]
1110mod loom_tests {
1111    use loom::sync::atomic::{fence, AtomicU64, AtomicUsize, Ordering};
1112    use loom::sync::Arc;
1113
1114    /// The shared state behind a counter handle: the accumulator the flush loop drains, and the Arc
1115    /// strong count it consults to decide whether the handle is orphaned.
1116    struct Shared {
1117        value: AtomicU64,
1118        strong: AtomicUsize,
1119    }
1120
1121    /// A component holding a cached handle lands one final increment, then drops the handle.
1122    fn caller_increment_then_drop(shared: &Arc<Shared>, amount: u64) {
1123        shared.value.fetch_add(amount, Ordering::Relaxed);
1124        shared.strong.fetch_sub(1, Ordering::Release); // Arc clone drop
1125    }
1126
1127    /// Consumes the value, then checks the strong count (value read before the liveness check).
1128    fn flush_decision_consume_first(shared: &Arc<Shared>) -> (u64, bool) {
1129        let delta = shared.value.swap(0, Ordering::Relaxed);
1130        let orphaned = shared.strong.load(Ordering::Relaxed) == 1;
1131        (delta, orphaned && delta == 0)
1132    }
1133
1134    /// Checks the strong count first; if orphaned, Acquire-fences to synchronize with the dropping
1135    /// thread's `Release`, then consumes the value.
1136    fn flush_decision_strong_count_first(shared: &Arc<Shared>) -> (u64, bool) {
1137        let orphaned = shared.strong.load(Ordering::Relaxed) == 1;
1138        if orphaned {
1139            fence(Ordering::Acquire);
1140        }
1141        let delta = shared.value.swap(0, Ordering::Relaxed);
1142        (delta, orphaned && delta == 0)
1143    }
1144
1145    fn model(decision: fn(&Arc<Shared>) -> (u64, bool)) {
1146        loom::model(move || {
1147            const FINAL: u64 = 7;
1148
1149            // strong count starts at 2: one ref in the registry map, one held by the caller.
1150            let shared = Arc::new(Shared {
1151                value: AtomicU64::new(0),
1152                strong: AtomicUsize::new(2),
1153            });
1154            let caller_shared = Arc::clone(&shared);
1155
1156            let caller = loom::thread::spawn(move || {
1157                caller_increment_then_drop(&caller_shared, FINAL);
1158            });
1159
1160            // The flush task evaluates this counter while the component races.
1161            let (delta, evicted) = decision(&shared);
1162            caller.join().unwrap();
1163
1164            // Invariant: a counter may only be evicted once everything it accumulated has been
1165            // emitted. If the flush evicts it, the delta it emitted downstream must already include
1166            // the final increment -- otherwise those counts are silently discarded with the handle.
1167            if evicted {
1168                assert_eq!(
1169                    delta,
1170                    FINAL,
1171                    "counter evicted while {} counts were never emitted -> permanently lost",
1172                    FINAL - delta
1173                );
1174            }
1175        });
1176    }
1177
1178    // Consuming the value before checking the strong count is lossy: loom finds an interleaving where
1179    // the handle is evicted while a final increment goes unemitted. `#[should_panic]` asserts loom
1180    // reaches that interleaving.
1181    #[test]
1182    #[should_panic(expected = "permanently lost")]
1183    fn consume_first_ordering_loses_a_final_increment() {
1184        model(flush_decision_consume_first);
1185    }
1186
1187    // Checking the strong count first (Acquire-fencing when orphaned) before consuming preserves every
1188    // increment across all interleavings loom explores.
1189    #[test]
1190    fn strong_count_first_ordering_preserves_a_final_increment() {
1191        model(flush_decision_strong_count_first);
1192    }
1193}