1use 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
64const IDLE_EVICT_INTERVALS: u32 = 3;
72
73static RECEIVER_STATE: OnceLock<Arc<State>> = OnceLock::new();
74
75#[derive(Clone, Default)]
77pub struct MetricsSnapshot {
78 pub upserts: Vec<Event>,
80
81 pub evictions: Vec<Context>,
83}
84
85pub 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 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 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 fn load(&self) -> f64 {
167 f64::from_bits(self.inner.value.load(Ordering::Relaxed))
168 }
169
170 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
193struct 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 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#[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 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 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 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
292pub struct FilterHandle {
297 state: Arc<State>,
298}
299
300impl FilterHandle {
301 pub fn override_filter(&self, level: Level) {
303 *self.state.current_level.lock().unwrap() = level;
304 }
305
306 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
391pub 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
440pub struct MetricsStream {
445 inner: ReusableBoxFuture<
446 'static,
447 (
448 Result<SharedMetricsSnapshot, RecvError>,
449 Receiver<SharedMetricsSnapshot>,
450 ),
451 >,
452}
453
454impl MetricsStream {
455 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 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
531fn 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 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 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 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 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 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 if let Some(context) = self.key_context_cache.get(&key) {
686 return context.clone();
687 }
688
689 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 fn take(&mut self, key: &Key) -> Option<Context> {
709 self.key_context_cache.remove(key)
710 }
711}
712
713pub 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
736pub 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 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 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 flush_once(&state, &mut resolver, &mut flush_state);
942 assert!(counter_present(&state, "dropped_counter"));
943
944 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 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 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 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 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 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 flush_once(&state, &mut resolver, &mut flush_state);
1027
1028 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 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 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 flush_once(&state, &mut resolver, &mut flush_state);
1062
1063 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 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 flush_once(&state, &mut resolver, &mut flush_state);
1084 assert!(!gauge_present(&state, "final_gauge"));
1085 }
1086}
1087
1088#[cfg(all(test, feature = "loom"))]
1110mod loom_tests {
1111 use loom::sync::atomic::{fence, AtomicU64, AtomicUsize, Ordering};
1112 use loom::sync::Arc;
1113
1114 struct Shared {
1117 value: AtomicU64,
1118 strong: AtomicUsize,
1119 }
1120
1121 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); }
1126
1127 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 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 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 let (delta, evicted) = decision(&shared);
1162 caller.join().unwrap();
1163
1164 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 #[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 #[test]
1190 fn strong_count_first_ordering_preserves_a_final_increment() {
1191 model(flush_decision_strong_count_first);
1192 }
1193}