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_error::GenericError;
27use tokio::{
28 select,
29 sync::broadcast::{self, error::RecvError, Receiver},
30};
31use tokio_util::sync::ReusableBoxFuture;
32use tracing::debug;
33
34use crate::{
35 data_model::{
36 event::{
37 metric::{
38 context::{Context, ContextResolver, ContextResolverBuilder},
39 *,
40 },
41 Event,
42 },
43 origin::RawOrigin,
44 tags::{Tag, TagSet},
45 },
46 runtime::{InitializationError, Supervisable, SupervisorFuture},
47};
48
49mod aggregated;
50pub use self::aggregated::{
51 get_shared_metrics_state, initialize_shared_metrics_state, AggregatedMetricValue, AggregatedMetricsProcessor,
52 AggregatedMetricsState, SharedMetricsWorker,
53};
54
55mod api;
56pub use self::api::InternalTelemetryAPIWorker;
57
58mod histogram;
59pub use self::histogram::AggregatedHistogram;
60
61mod processor;
62pub use self::processor::TelemetryProcessor;
63
64mod reflector;
65pub use self::reflector::{Processor, Reflector, ReflectorWorker};
66
67mod remapper;
68pub use self::remapper::{RemappedMetric, RemapperRule};
69
70const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
71const INTERNAL_METRICS_INTERNER_SIZE: NonZeroUsize = NonZeroUsize::new(16_384).unwrap();
72
73const IDLE_EVICT_INTERVALS: u32 = 3;
81
82static RECEIVER_STATE: OnceLock<Arc<State>> = OnceLock::new();
83
84#[derive(Clone, Default)]
86pub struct MetricsSnapshot {
87 pub upserts: Vec<Event>,
89
90 pub evictions: Vec<Context>,
92}
93
94pub type SharedMetricsSnapshot = Arc<MetricsSnapshot>;
96
97struct Handle<T> {
98 inner: T,
99 level: Level,
100 idle: AtomicU32,
101}
102
103impl<T> Handle<T> {
104 const fn new(inner: T, level: Level) -> Self {
105 Self {
106 inner,
107 level,
108 idle: AtomicU32::new(0),
109 }
110 }
111
112 fn level(&self) -> Level {
113 self.level
114 }
115
116 fn reset_idle(&self) {
117 self.idle.store(0, Ordering::Relaxed);
118 }
119
120 fn bump_idle(&self) -> u32 {
121 self.idle.fetch_add(1, Ordering::Relaxed).saturating_add(1)
122 }
123}
124
125struct CounterInner {
126 value: AtomicU64,
127}
128
129impl Handle<CounterInner> {
130 const fn new_counter(level: Level) -> Self {
131 Self::new(
132 CounterInner {
133 value: AtomicU64::new(0),
134 },
135 level,
136 )
137 }
138
139 fn consume(&self) -> u64 {
141 self.inner.value.swap(0, Ordering::Relaxed)
142 }
143}
144
145impl CounterFn for Handle<CounterInner> {
146 fn increment(&self, value: u64) {
147 CounterFn::increment(&self.inner.value, value);
148 }
149
150 fn absolute(&self, value: u64) {
151 CounterFn::absolute(&self.inner.value, value);
152 }
153}
154
155struct GaugeInner {
156 value: AtomicU64,
157 dirty: AtomicBool,
161}
162
163impl Handle<GaugeInner> {
164 const fn new_gauge(level: Level) -> Self {
165 Self::new(
166 GaugeInner {
167 value: AtomicU64::new(0),
168 dirty: AtomicBool::new(false),
169 },
170 level,
171 )
172 }
173
174 fn load(&self) -> f64 {
176 f64::from_bits(self.inner.value.load(Ordering::Relaxed))
177 }
178
179 fn take_dirty(&self) -> bool {
181 self.inner.dirty.swap(false, Ordering::Relaxed)
182 }
183}
184
185impl GaugeFn for Handle<GaugeInner> {
186 fn increment(&self, value: f64) {
187 GaugeFn::increment(&self.inner.value, value);
188 self.inner.dirty.store(true, Ordering::Relaxed);
189 }
190
191 fn decrement(&self, value: f64) {
192 GaugeFn::decrement(&self.inner.value, value);
193 self.inner.dirty.store(true, Ordering::Relaxed);
194 }
195
196 fn set(&self, value: f64) {
197 GaugeFn::set(&self.inner.value, value);
198 self.inner.dirty.store(true, Ordering::Relaxed);
199 }
200}
201
202struct HistogramInner {
204 value: AtomicBucket<f64>,
205}
206
207impl Handle<HistogramInner> {
208 fn new_histogram(level: Level) -> Self {
209 Self::new(
210 HistogramInner {
211 value: AtomicBucket::new(),
212 },
213 level,
214 )
215 }
216
217 fn drain_into(&self, out: &mut Vec<f64>) {
219 self.inner.value.clear_with(|samples| out.extend(samples));
220 }
221}
222
223impl HistogramFn for Handle<HistogramInner> {
224 fn record(&self, value: f64) {
225 self.inner.value.push(value);
226 }
227}
228
229#[derive(Default)]
235struct MetricsRegistry {
236 maps: Mutex<RegistryMaps>,
237}
238
239#[derive(Default)]
240struct RegistryMaps {
241 counters: FastHashMap<Key, Arc<Handle<CounterInner>>>,
242 gauges: FastHashMap<Key, Arc<Handle<GaugeInner>>>,
243 histograms: FastHashMap<Key, Arc<Handle<HistogramInner>>>,
244}
245
246impl MetricsRegistry {
247 fn get_or_create_counter(&self, key: &Key, level: Level) -> Counter {
252 let mut maps = self.maps.lock().unwrap();
253 let handle = if let Some(handle) = maps.counters.get(key) {
254 handle.reset_idle();
255 Arc::clone(handle)
256 } else {
257 let handle = Arc::new(Handle::new_counter(level));
258 maps.counters.insert(key.clone(), Arc::clone(&handle));
259 handle
260 };
261
262 Counter::from_arc(handle)
263 }
264
265 fn remove_counter(&self, key: &Key) -> Option<Arc<Handle<CounterInner>>> {
266 let mut maps = self.maps.lock().unwrap();
267 maps.counters.remove(key)
268 }
269
270 fn get_or_create_gauge(&self, key: &Key, level: Level) -> Gauge {
272 let mut maps = self.maps.lock().unwrap();
273 let handle = if let Some(handle) = maps.gauges.get(key) {
274 handle.reset_idle();
275 Arc::clone(handle)
276 } else {
277 let handle = Arc::new(Handle::new_gauge(level));
278 maps.gauges.insert(key.clone(), Arc::clone(&handle));
279 handle
280 };
281
282 Gauge::from_arc(handle)
283 }
284
285 fn get_or_create_histogram(&self, key: &Key, level: Level) -> Histogram {
287 let mut maps = self.maps.lock().unwrap();
288 let handle = if let Some(handle) = maps.histograms.get(key) {
289 handle.reset_idle();
290 Arc::clone(handle)
291 } else {
292 let handle = Arc::new(Handle::new_histogram(level));
293 maps.histograms.insert(key.clone(), Arc::clone(&handle));
294 handle
295 };
296
297 Histogram::from_arc(handle)
298 }
299}
300
301pub struct FilterHandle {
306 state: Arc<State>,
307}
308
309impl FilterHandle {
310 pub fn override_filter(&self, level: Level) {
312 *self.state.current_level.lock().unwrap() = level;
313 }
314
315 pub fn reset_filter(&self) {
317 *self.state.current_level.lock().unwrap() = self.state.default_level;
318 }
319}
320
321struct State {
322 registry: MetricsRegistry,
323 flush_tx: broadcast::Sender<SharedMetricsSnapshot>,
324 metrics_prefix: String,
325 default_level: Level,
326 current_level: Mutex<Level>,
327 idle_evict_intervals: u32,
328 flush_interval: Duration,
329}
330
331struct MetricsRecorder {
332 state: Arc<State>,
333}
334
335impl MetricsRecorder {
336 fn new(metrics_prefix: String, default_level: Level) -> Self {
337 let (flush_tx, _) = broadcast::channel(2);
338 Self {
339 state: Arc::new(State {
340 registry: MetricsRegistry::default(),
341 flush_tx,
342 metrics_prefix,
343 default_level,
344 current_level: Mutex::new(default_level),
345 idle_evict_intervals: IDLE_EVICT_INTERVALS,
346 flush_interval: FLUSH_INTERVAL,
347 }),
348 }
349 }
350
351 fn filter_handle(&self) -> FilterHandle {
352 FilterHandle {
353 state: Arc::clone(&self.state),
354 }
355 }
356
357 fn install(self) -> Result<(), SetRecorderError<Self>> {
358 let state = Arc::clone(&self.state);
359 metrics::set_global_recorder(self)?;
360
361 if RECEIVER_STATE.set(state).is_err() {
362 panic!("metrics receiver should never be set prior to global recorder being installed");
363 }
364
365 Ok(())
366 }
367
368 fn prefix_key(&self, key: &Key) -> Key {
369 Key::from_parts(format!("{}.{}", self.state.metrics_prefix, key.name()), key.labels())
370 }
371}
372
373impl Recorder for MetricsRecorder {
374 fn describe_counter(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
375 fn describe_gauge(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
376 fn describe_histogram(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
377
378 fn register_counter(&self, key: &Key, metadata: &Metadata<'_>) -> Counter {
379 let prefixed_key = self.prefix_key(key);
380 self.state
381 .registry
382 .get_or_create_counter(&prefixed_key, *metadata.level())
383 }
384
385 fn register_gauge(&self, key: &Key, metadata: &Metadata<'_>) -> Gauge {
386 let prefixed_key = self.prefix_key(key);
387 self.state
388 .registry
389 .get_or_create_gauge(&prefixed_key, *metadata.level())
390 }
391
392 fn register_histogram(&self, key: &Key, metadata: &Metadata<'_>) -> Histogram {
393 let prefixed_key = self.prefix_key(key);
394 self.state
395 .registry
396 .get_or_create_histogram(&prefixed_key, *metadata.level())
397 }
398}
399
400pub fn delete_counter(key: Key) -> bool {
405 let Some(state) = RECEIVER_STATE.get() else {
406 return false;
407 };
408
409 let prefixed_key = Key::from_parts(format!("{}.{}", state.metrics_prefix, key.name()), key.labels());
410 let Some(snapshot) = delete_counter_from_state(state, &prefixed_key) else {
411 return false;
412 };
413
414 if state.flush_tx.receiver_count() > 0 {
415 let _ = state.flush_tx.send(Arc::new(snapshot));
416 }
417
418 true
419}
420
421fn delete_counter_from_state(state: &State, prefixed_key: &Key) -> Option<MetricsSnapshot> {
422 let handle = state.registry.remove_counter(prefixed_key)?;
423 let delta = handle.consume();
424 let context = context_from_key(prefixed_key);
425 let current_level = *state.current_level.lock().unwrap();
426
427 let mut snapshot = MetricsSnapshot {
428 upserts: Vec::new(),
429 evictions: vec![context.clone()],
430 };
431 if delta > 0 && handle.level() >= current_level {
432 snapshot
433 .upserts
434 .push(Event::Metric(Metric::counter(context, delta as f64)));
435 }
436
437 Some(snapshot)
438}
439
440fn context_from_key(key: &Key) -> Context {
441 let tags = key
442 .labels()
443 .map(|l| Tag::from(format!("{}:{}", l.key(), l.value())))
444 .collect::<TagSet>();
445
446 Context::from_parts(key.name().to_string(), tags)
447}
448
449pub struct MetricsStream {
454 inner: ReusableBoxFuture<
455 'static,
456 (
457 Result<SharedMetricsSnapshot, RecvError>,
458 Receiver<SharedMetricsSnapshot>,
459 ),
460 >,
461}
462
463impl MetricsStream {
464 pub fn register() -> Self {
466 let state = RECEIVER_STATE.get().expect("metrics receiver should be set");
467 Self {
468 inner: ReusableBoxFuture::new(make_rx_future(state.flush_tx.subscribe())),
469 }
470 }
471}
472
473impl Stream for MetricsStream {
474 type Item = SharedMetricsSnapshot;
475
476 fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
477 loop {
478 let (result, rx) = ready!(self.inner.poll(cx));
480 self.inner.set(make_rx_future(rx));
481
482 match result {
483 Ok(item) => return Poll::Ready(Some(item)),
484 Err(RecvError::Closed) => return Poll::Ready(None),
485 Err(RecvError::Lagged(n)) => {
486 debug!(
487 missed_payloads = n,
488 "Stream lagging behind internal metrics producer. Internal metrics may have been lost."
489 );
490 continue;
491 }
492 }
493 }
494 }
495}
496
497async fn make_rx_future(
498 mut rx: Receiver<SharedMetricsSnapshot>,
499) -> (
500 Result<SharedMetricsSnapshot, RecvError>,
501 Receiver<SharedMetricsSnapshot>,
502) {
503 let result = rx.recv().await;
504 (result, rx)
505}
506
507#[derive(Default)]
508struct FlushState {
509 counter_emits: Vec<(Key, f64)>,
510 gauge_emits: Vec<(Key, f64)>,
511 histogram_emits: Vec<(Key, Vec<f64>)>,
512 evicted_keys: Vec<Key>,
513}
514
515impl FlushState {
516 fn clear(&mut self) {
517 self.counter_emits.clear();
518 self.gauge_emits.clear();
519 self.histogram_emits.clear();
520 self.evicted_keys.clear();
521 }
522}
523
524async fn flush_metrics() {
525 let mut context_resolver = MetricsContextResolver::new(INTERNAL_METRICS_INTERNER_SIZE);
526
527 let state = RECEIVER_STATE.get().expect("metrics receiver should be set");
528
529 let mut flush_interval = tokio::time::interval(state.flush_interval);
530 flush_interval.tick().await;
531
532 let mut flush_state = FlushState::default();
533
534 loop {
535 flush_interval.tick().await;
536 flush_once(state, &mut context_resolver, &mut flush_state);
537 }
538}
539
540fn flush_once(state: &State, context_resolver: &mut MetricsContextResolver, flush_state: &mut FlushState) {
544 let has_listeners = state.flush_tx.receiver_count() > 0;
545 let current_level = *state.current_level.lock().unwrap();
546 let idle_threshold = state.idle_evict_intervals;
547 let mut snapshot = MetricsSnapshot::default();
548
549 flush_state.clear();
550 let FlushState {
551 counter_emits,
552 gauge_emits,
553 histogram_emits,
554 evicted_keys,
555 } = flush_state;
556
557 {
558 let mut maps = state.registry.maps.lock().unwrap();
559
560 maps.counters.retain(|key, handle| {
567 let idle = handle.bump_idle();
568 let orphaned = Arc::strong_count(handle) == 1;
569 if orphaned {
570 std::sync::atomic::fence(Ordering::Acquire);
571 }
572 let delta = handle.consume();
573
574 if orphaned && delta == 0 && idle >= idle_threshold {
575 evicted_keys.push(key.clone());
576 return false;
577 }
578
579 if has_listeners && handle.level() >= current_level {
580 counter_emits.push((key.clone(), delta as f64));
581 }
582 true
583 });
584
585 maps.gauges.retain(|key, handle| {
588 let idle = handle.bump_idle();
589 let orphaned = Arc::strong_count(handle) == 1;
590 if orphaned {
591 std::sync::atomic::fence(Ordering::Acquire);
592 }
593 let value = handle.load();
594 let written = handle.take_dirty();
595
596 if orphaned && !written && idle >= idle_threshold {
597 evicted_keys.push(key.clone());
598 return false;
599 }
600
601 if has_listeners && handle.level() >= current_level {
602 gauge_emits.push((key.clone(), value));
603 }
604 true
605 });
606
607 maps.histograms.retain(|key, handle| {
613 let idle = handle.bump_idle();
614 let orphaned = Arc::strong_count(handle) == 1;
615 if orphaned {
616 std::sync::atomic::fence(Ordering::Acquire);
617 }
618 let mut histogram_samples = Vec::new();
619 handle.drain_into(&mut histogram_samples);
620 let had_samples = !histogram_samples.is_empty();
621
622 if orphaned && !had_samples && idle >= idle_threshold {
623 evicted_keys.push(key.clone());
624 return false;
625 }
626
627 if has_listeners && had_samples && handle.level() >= current_level {
628 histogram_emits.push((key.clone(), histogram_samples));
629 }
630 true
631 });
632 }
633
634 for key in evicted_keys {
637 if let Some(context) = context_resolver.take(key) {
638 if has_listeners {
639 snapshot.evictions.push(context);
640 }
641 }
642 }
643
644 if !has_listeners {
645 return;
646 }
647
648 for (key, value) in counter_emits.drain(..) {
649 let context = context_resolver.resolve_from_key(key);
650 snapshot.upserts.push(Event::Metric(Metric::counter(context, value)));
651 }
652 for (key, value) in gauge_emits.drain(..) {
653 let context = context_resolver.resolve_from_key(key);
654 snapshot.upserts.push(Event::Metric(Metric::gauge(context, value)));
655 }
656 for (key, samples) in histogram_emits.drain(..) {
657 let context = context_resolver.resolve_from_key(key);
658 snapshot
659 .upserts
660 .push(Event::Metric(Metric::histogram(context, &samples[..])));
661 }
662
663 if !snapshot.upserts.is_empty() || !snapshot.evictions.is_empty() {
664 let _ = state.flush_tx.send(Arc::new(snapshot));
665 }
666}
667
668struct MetricsContextResolver {
669 context_resolver: ContextResolver,
670 key_context_cache: FastHashMap<Key, Context>,
671}
672
673impl MetricsContextResolver {
674 fn new(resolver_interner_size_bytes: NonZeroUsize) -> Self {
675 Self {
676 context_resolver: ContextResolverBuilder::from_name("core/internal_metrics")
678 .expect("resolver name is not empty")
679 .with_interner_capacity_bytes(resolver_interner_size_bytes)
680 .without_caching()
681 .build(),
682 key_context_cache: FastHashMap::default(),
683 }
684 }
685
686 fn resolve_from_key(&mut self, key: Key) -> Context {
687 static SELF_ORIGIN_INFO: LazyLock<RawOrigin<'static>> = LazyLock::new(|| {
688 let mut origin_info = RawOrigin::default();
689 origin_info.set_process_id(std::process::id());
690 origin_info
691 });
692
693 if let Some(context) = self.key_context_cache.get(&key) {
695 return context.clone();
696 }
697
698 let tags = key
700 .labels()
701 .map(|l| Tag::from(format!("{}:{}", l.key(), l.value())))
702 .collect::<TagSet>();
703
704 let context = self
705 .context_resolver
706 .resolve(key.name(), &tags, Some(SELF_ORIGIN_INFO.clone()))
707 .expect("resolver should always allow falling back");
708
709 self.key_context_cache.insert(key, context.clone());
710 context
711 }
712
713 fn take(&mut self, key: &Key) -> Option<Context> {
718 self.key_context_cache.remove(key)
719 }
720}
721
722pub async fn initialize_metrics(
736 metrics_prefix: String, default_level: Level,
737) -> Result<(FilterHandle, MetricsFlusherWorker), GenericError> {
738 let recorder = MetricsRecorder::new(metrics_prefix, default_level);
739 let filter_handle = recorder.filter_handle();
740 recorder.install()?;
741
742 Ok((filter_handle, MetricsFlusherWorker))
743}
744
745pub struct MetricsFlusherWorker;
751
752#[async_trait]
753impl Supervisable for MetricsFlusherWorker {
754 fn name(&self) -> &str {
755 "flusher"
756 }
757
758 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
759 Ok(Box::pin(async move {
760 select! {
761 _ = process_shutdown => {},
762 _ = flush_metrics() => {},
763 }
764
765 Ok(())
766 }))
767 }
768}
769
770#[cfg(test)]
773pub(crate) fn aggregate_upserts(upserts: Vec<Event>) -> AggregatedMetricsState {
774 let processor = AggregatedMetricsProcessor;
775 let state = processor.build_initial_state();
776 processor.process(
777 MetricsSnapshot {
778 upserts,
779 evictions: Vec::new(),
780 },
781 &state,
782 );
783 state
784}
785
786#[cfg(test)]
787mod tests {
788 use metrics::Label;
789 use saluki_common::collections::FastHashSet;
790
791 use super::*;
792
793 const HIGH_CARDINALITY_COUNTER_NAME: &str = "high_cardinality_counter";
794 const HIGH_CARDINALITY_COUNTERS: usize = 64;
795
796 fn make_state(idle_evict_intervals: u32) -> (Arc<State>, Receiver<SharedMetricsSnapshot>) {
798 let (flush_tx, rx) = broadcast::channel(64);
799 let state = Arc::new(State {
800 registry: MetricsRegistry::default(),
801 flush_tx,
802 metrics_prefix: "test".to_string(),
803 default_level: Level::TRACE,
804 current_level: Mutex::new(Level::TRACE),
805 idle_evict_intervals,
806 flush_interval: FLUSH_INTERVAL,
807 });
808 (state, rx)
809 }
810
811 fn resolver() -> MetricsContextResolver {
812 MetricsContextResolver::new(INTERNAL_METRICS_INTERNER_SIZE)
813 }
814
815 fn register_counter(state: &State, name: &'static str) -> Counter {
817 state
818 .registry
819 .get_or_create_counter(&Key::from_name(name), Level::TRACE)
820 }
821
822 fn register_counter_with_origin(state: &State, name: &'static str, origin: &str) -> Counter {
823 let key = Key::from_parts(name, vec![Label::new("origin", origin.to_string())]);
824 state.registry.get_or_create_counter(&key, Level::TRACE)
825 }
826
827 fn register_gauge(state: &State, name: &'static str) -> Gauge {
828 state.registry.get_or_create_gauge(&Key::from_name(name), Level::TRACE)
829 }
830
831 fn register_histogram(state: &State, name: &'static str) -> Histogram {
832 state
833 .registry
834 .get_or_create_histogram(&Key::from_name(name), Level::TRACE)
835 }
836
837 fn counter_present(state: &State, name: &'static str) -> bool {
838 state
839 .registry
840 .maps
841 .lock()
842 .unwrap()
843 .counters
844 .contains_key(&Key::from_name(name))
845 }
846
847 fn counter_count(state: &State) -> usize {
848 state.registry.maps.lock().unwrap().counters.len()
849 }
850
851 fn gauge_present(state: &State, name: &'static str) -> bool {
852 state
853 .registry
854 .maps
855 .lock()
856 .unwrap()
857 .gauges
858 .contains_key(&Key::from_name(name))
859 }
860
861 fn histogram_present(state: &State, name: &'static str) -> bool {
862 state
863 .registry
864 .maps
865 .lock()
866 .unwrap()
867 .histograms
868 .contains_key(&Key::from_name(name))
869 }
870
871 fn drain(rx: &mut Receiver<SharedMetricsSnapshot>) -> Vec<MetricsSnapshot> {
872 let mut out = Vec::new();
873 while let Ok(batch) = rx.try_recv() {
874 out.push((*batch).clone());
875 }
876 out
877 }
878
879 fn evicted_names(snapshots: &[MetricsSnapshot]) -> Vec<String> {
880 snapshots
881 .iter()
882 .flat_map(|snapshot| snapshot.evictions.iter().map(|ctx| ctx.name().to_string()))
883 .collect()
884 }
885
886 fn metric_count(state: &AggregatedMetricsState, name: &str) -> usize {
887 let mut count = 0;
888 state.visit_metrics(|context, _| {
889 if context.name() == name {
890 count += 1;
891 }
892 });
893 count
894 }
895
896 #[test]
897 fn delete_counter_from_state_emits_pending_delta_before_eviction() {
898 let (state, _rx) = make_state(IDLE_EVICT_INTERVALS);
899 let key = Key::from_parts(
900 "deleted_counter",
901 vec![Label::new("origin", "container_id://deleted".to_string())],
902 );
903
904 let counter = state.registry.get_or_create_counter(&key, Level::TRACE);
905 counter.increment(7);
906 drop(counter);
907
908 let snapshot = delete_counter_from_state(&state, &key).expect("counter should be deleted");
909
910 assert!(!state.registry.maps.lock().unwrap().counters.contains_key(&key));
911 assert_eq!(snapshot.evictions.len(), 1);
912 assert_eq!(snapshot.evictions[0].name(), "deleted_counter");
913 assert!(snapshot.evictions[0].tags().has_tag("origin:container_id://deleted"));
914
915 assert_eq!(snapshot.upserts.len(), 1);
916 let Event::Metric(metric) = &snapshot.upserts[0] else {
917 panic!("delete should emit the pending counter delta");
918 };
919 assert_eq!(metric.context(), &snapshot.evictions[0]);
920 let MetricValues::Counter(points) = metric.values() else {
921 panic!("delete should emit a counter metric");
922 };
923 assert_eq!(points.into_iter().map(|(_, value)| value).sum::<f64>(), 7.0);
924 }
925
926 #[tokio::test]
927 async fn evicts_high_cardinality_counters_from_registry_and_aggregated_state() {
928 let (state, mut rx) = make_state(IDLE_EVICT_INTERVALS);
929 let mut resolver = resolver();
930 let mut flush_state = FlushState::default();
931 let agg_processor = AggregatedMetricsProcessor;
932 let agg_state = agg_processor.build_initial_state();
933 let mut expected_eviction_tags = FastHashSet::default();
934 let mut observed_eviction_tags = FastHashSet::default();
935
936 for index in 0..HIGH_CARDINALITY_COUNTERS {
937 let origin = format!("entity://source-{index}");
938 expected_eviction_tags.insert(format!("origin:{origin}"));
939
940 let counter = register_counter_with_origin(&state, HIGH_CARDINALITY_COUNTER_NAME, &origin);
941 counter.increment(1);
942 drop(counter);
943 }
944
945 assert_eq!(counter_count(&state), HIGH_CARDINALITY_COUNTERS);
946
947 for _ in 0..IDLE_EVICT_INTERVALS {
948 flush_once(&state, &mut resolver, &mut flush_state);
949
950 for update in drain(&mut rx) {
951 for context in &update.evictions {
952 if context.name() != HIGH_CARDINALITY_COUNTER_NAME {
953 continue;
954 }
955
956 for tag in &expected_eviction_tags {
957 if context.tags().has_tag(tag) {
958 observed_eviction_tags.insert(tag.clone());
959 }
960 }
961 }
962
963 agg_processor.process(update, &agg_state);
964 }
965 }
966
967 assert_eq!(counter_count(&state), 0);
968 assert_eq!(metric_count(&agg_state, HIGH_CARDINALITY_COUNTER_NAME), 0);
969 assert_eq!(observed_eviction_tags, expected_eviction_tags);
970 }
971
972 #[tokio::test]
973 async fn evicts_counter_within_bounded_time_after_handle_dropped() {
974 let (state, mut rx) = make_state(3);
975 let mut resolver = resolver();
976 let mut flush_state = FlushState::default();
977
978 let counter = register_counter(&state, "dropped_counter");
979 counter.increment(5);
980
981 flush_once(&state, &mut resolver, &mut flush_state);
983 assert!(counter_present(&state, "dropped_counter"));
984
985 drop(counter);
987 let _ = drain(&mut rx);
988 for _ in 0..3 {
989 flush_once(&state, &mut resolver, &mut flush_state);
990 }
991 assert!(!counter_present(&state, "dropped_counter"));
992
993 let updates = drain(&mut rx);
995 assert!(evicted_names(&updates).iter().any(|n| n == "dropped_counter"));
996 }
997
998 #[tokio::test]
999 async fn does_not_evict_while_handle_is_held() {
1000 let (state, _rx) = make_state(3);
1001 let mut resolver = resolver();
1002 let mut flush_state = FlushState::default();
1003
1004 let counter = register_counter(&state, "held_counter");
1005
1006 for _ in 0..8 {
1008 flush_once(&state, &mut resolver, &mut flush_state);
1009 }
1010
1011 assert!(
1012 counter_present(&state, "held_counter"),
1013 "a held metric must never be evicted"
1014 );
1015 drop(counter);
1016 }
1017
1018 #[tokio::test]
1019 async fn reregistration_keeps_uncached_metric_alive() {
1020 let (state, _rx) = make_state(3);
1023 let mut resolver = resolver();
1024 let mut flush_state = FlushState::default();
1025
1026 for _ in 0..8 {
1027 let counter = register_counter(&state, "uncached_counter");
1028 counter.increment(1);
1029 drop(counter);
1030 flush_once(&state, &mut resolver, &mut flush_state);
1031 }
1032 assert!(counter_present(&state, "uncached_counter"));
1033
1034 for _ in 0..3 {
1036 flush_once(&state, &mut resolver, &mut flush_state);
1037 }
1038 assert!(!counter_present(&state, "uncached_counter"));
1039 }
1040
1041 #[tokio::test]
1042 async fn gauge_reset_to_same_value_is_not_evicted() {
1043 let (state, _rx) = make_state(3);
1046 let mut resolver = resolver();
1047 let mut flush_state = FlushState::default();
1048
1049 for _ in 0..8 {
1050 let gauge = register_gauge(&state, "steady_gauge");
1051 gauge.set(0.0);
1052 drop(gauge);
1053 flush_once(&state, &mut resolver, &mut flush_state);
1054 }
1055 assert!(gauge_present(&state, "steady_gauge"));
1056 }
1057
1058 #[tokio::test]
1059 async fn final_counter_delta_reaches_downstream_before_eviction() {
1060 let (state, mut rx) = make_state(1);
1061 let mut resolver = resolver();
1062 let mut flush_state = FlushState::default();
1063
1064 let counter = register_counter(&state, "final_counter");
1065
1066 flush_once(&state, &mut resolver, &mut flush_state);
1068
1069 counter.increment(7);
1072 drop(counter);
1073 flush_once(&state, &mut resolver, &mut flush_state);
1074 assert!(
1075 counter_present(&state, "final_counter"),
1076 "must not evict in an interval that produced a delta"
1077 );
1078
1079 let agg_processor = AggregatedMetricsProcessor;
1081 let agg_state = agg_processor.build_initial_state();
1082 for update in drain(&mut rx) {
1083 agg_processor.process(update, &agg_state);
1084 }
1085 assert_eq!(agg_state.get_aggregated_with_tags("final_counter", &[]), 7.0);
1086
1087 flush_once(&state, &mut resolver, &mut flush_state);
1089 assert!(!counter_present(&state, "final_counter"));
1090 }
1091
1092 #[tokio::test]
1093 async fn gauge_final_value_reaches_downstream_before_eviction() {
1094 let (state, mut rx) = make_state(1);
1095 let mut resolver = resolver();
1096 let mut flush_state = FlushState::default();
1097
1098 let gauge = register_gauge(&state, "final_gauge");
1099 gauge.set(1.0);
1100
1101 flush_once(&state, &mut resolver, &mut flush_state);
1103
1104 gauge.set(42.0);
1108 drop(gauge);
1109 flush_once(&state, &mut resolver, &mut flush_state);
1110 assert!(
1111 gauge_present(&state, "final_gauge"),
1112 "must not evict in an interval that wrote a new value"
1113 );
1114
1115 let agg_processor = AggregatedMetricsProcessor;
1117 let agg_state = agg_processor.build_initial_state();
1118 for update in drain(&mut rx) {
1119 agg_processor.process(update, &agg_state);
1120 }
1121 assert_eq!(agg_state.find_single_with_tags("final_gauge", &[]), Some(42.0));
1122
1123 flush_once(&state, &mut resolver, &mut flush_state);
1125 assert!(!gauge_present(&state, "final_gauge"));
1126 }
1127
1128 #[tokio::test]
1129 async fn histogram_final_samples_reach_downstream_before_eviction() {
1130 let (state, mut rx) = make_state(1);
1142 let mut resolver = resolver();
1143 let mut flush_state = FlushState::default();
1144
1145 let histogram = register_histogram(&state, "final_histogram");
1146
1147 flush_once(&state, &mut resolver, &mut flush_state);
1149
1150 histogram.record(1.5);
1153 drop(histogram);
1154 flush_once(&state, &mut resolver, &mut flush_state);
1155 assert!(
1156 histogram_present(&state, "final_histogram"),
1157 "must not evict in an interval that recorded a sample"
1158 );
1159
1160 let agg_processor = AggregatedMetricsProcessor;
1162 let agg_state = agg_processor.build_initial_state();
1163 for update in drain(&mut rx) {
1164 agg_processor.process(update, &agg_state);
1165 }
1166
1167 let mut downstream_sample_count = None;
1168 agg_state.visit_metrics(|context, value| {
1169 if context.name() == "final_histogram" {
1170 if let AggregatedMetricValue::Histogram(histogram) = value {
1171 downstream_sample_count = Some(histogram.count());
1172 }
1173 }
1174 });
1175 assert_eq!(
1176 downstream_sample_count,
1177 Some(1),
1178 "the final recorded sample must reach downstream before eviction"
1179 );
1180
1181 flush_once(&state, &mut resolver, &mut flush_state);
1183 assert!(!histogram_present(&state, "final_histogram"));
1184 }
1185}
1186
1187#[cfg(all(test, feature = "loom"))]
1209mod loom_tests {
1210 use loom::sync::atomic::{fence, AtomicU64, AtomicUsize, Ordering};
1211 use loom::sync::Arc;
1212
1213 struct Shared {
1216 value: AtomicU64,
1217 strong: AtomicUsize,
1218 }
1219
1220 fn caller_increment_then_drop(shared: &Arc<Shared>, amount: u64) {
1222 shared.value.fetch_add(amount, Ordering::Relaxed);
1223 shared.strong.fetch_sub(1, Ordering::Release); }
1225
1226 fn flush_decision_consume_first(shared: &Arc<Shared>) -> (u64, bool) {
1228 let delta = shared.value.swap(0, Ordering::Relaxed);
1229 let orphaned = shared.strong.load(Ordering::Relaxed) == 1;
1230 (delta, orphaned && delta == 0)
1231 }
1232
1233 fn flush_decision_strong_count_first(shared: &Arc<Shared>) -> (u64, bool) {
1236 let orphaned = shared.strong.load(Ordering::Relaxed) == 1;
1237 if orphaned {
1238 fence(Ordering::Acquire);
1239 }
1240 let delta = shared.value.swap(0, Ordering::Relaxed);
1241 (delta, orphaned && delta == 0)
1242 }
1243
1244 fn model(decision: fn(&Arc<Shared>) -> (u64, bool)) {
1245 loom::model(move || {
1246 const FINAL: u64 = 7;
1247
1248 let shared = Arc::new(Shared {
1250 value: AtomicU64::new(0),
1251 strong: AtomicUsize::new(2),
1252 });
1253 let caller_shared = Arc::clone(&shared);
1254
1255 let caller = loom::thread::spawn(move || {
1256 caller_increment_then_drop(&caller_shared, FINAL);
1257 });
1258
1259 let (delta, evicted) = decision(&shared);
1261 caller.join().unwrap();
1262
1263 if evicted {
1267 assert_eq!(
1268 delta,
1269 FINAL,
1270 "counter evicted while {} counts were never emitted -> permanently lost",
1271 FINAL - delta
1272 );
1273 }
1274 });
1275 }
1276
1277 #[test]
1281 #[should_panic(expected = "permanently lost")]
1282 fn consume_first_ordering_loses_a_final_increment() {
1283 model(flush_decision_consume_first);
1284 }
1285
1286 #[test]
1289 fn strong_count_first_ordering_preserves_a_final_increment() {
1290 model(flush_decision_strong_count_first);
1291 }
1292
1293 fn histogram_model(decision: fn(&Arc<Shared>) -> (u64, bool)) {
1308 loom::model(move || {
1309 const FINAL_SAMPLES: u64 = 1;
1310
1311 let shared = Arc::new(Shared {
1313 value: AtomicU64::new(0),
1314 strong: AtomicUsize::new(2),
1315 });
1316 let caller_shared = Arc::clone(&shared);
1317
1318 let caller = loom::thread::spawn(move || {
1320 caller_shared.value.fetch_add(FINAL_SAMPLES, Ordering::Relaxed); caller_shared.strong.fetch_sub(1, Ordering::Release); });
1323
1324 let (drained, evicted) = decision(&shared);
1326 caller.join().unwrap();
1327
1328 if evicted {
1332 assert_eq!(
1333 drained,
1334 FINAL_SAMPLES,
1335 "histogram evicted while {} recorded sample(s) were never drained -> permanently lost",
1336 FINAL_SAMPLES - drained
1337 );
1338 }
1339 });
1340 }
1341
1342 #[test]
1346 #[should_panic(expected = "permanently lost")]
1347 fn histogram_drain_first_ordering_loses_a_final_sample() {
1348 histogram_model(flush_decision_consume_first);
1349 }
1350
1351 #[test]
1354 fn histogram_strong_count_first_ordering_preserves_a_final_sample() {
1355 histogram_model(flush_decision_strong_count_first);
1356 }
1357}