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)]
764pub(crate) fn aggregate_upserts(upserts: Vec<Event>) -> AggregatedMetricsState {
765 let processor = AggregatedMetricsProcessor;
766 let state = processor.build_initial_state();
767 processor.process(
768 MetricsSnapshot {
769 upserts,
770 evictions: Vec::new(),
771 },
772 &state,
773 );
774 state
775}
776
777#[cfg(test)]
778mod tests {
779 use metrics::Label;
780 use saluki_common::collections::FastHashSet;
781
782 use super::*;
783
784 const HIGH_CARDINALITY_COUNTER_NAME: &str = "high_cardinality_counter";
785 const HIGH_CARDINALITY_COUNTERS: usize = 64;
786
787 fn make_state(idle_evict_intervals: u32) -> (Arc<State>, Receiver<SharedMetricsSnapshot>) {
789 let (flush_tx, rx) = broadcast::channel(64);
790 let state = Arc::new(State {
791 registry: MetricsRegistry::default(),
792 flush_tx,
793 metrics_prefix: "test".to_string(),
794 default_level: Level::TRACE,
795 current_level: Mutex::new(Level::TRACE),
796 idle_evict_intervals,
797 flush_interval: FLUSH_INTERVAL,
798 });
799 (state, rx)
800 }
801
802 fn resolver() -> MetricsContextResolver {
803 MetricsContextResolver::new(INTERNAL_METRICS_INTERNER_SIZE)
804 }
805
806 fn register_counter(state: &State, name: &'static str) -> Counter {
808 state
809 .registry
810 .get_or_create_counter(&Key::from_name(name), Level::TRACE)
811 }
812
813 fn register_counter_with_origin(state: &State, name: &'static str, origin: &str) -> Counter {
814 let key = Key::from_parts(name, vec![Label::new("origin", origin.to_string())]);
815 state.registry.get_or_create_counter(&key, Level::TRACE)
816 }
817
818 fn register_gauge(state: &State, name: &'static str) -> Gauge {
819 state.registry.get_or_create_gauge(&Key::from_name(name), Level::TRACE)
820 }
821
822 fn register_histogram(state: &State, name: &'static str) -> Histogram {
823 state
824 .registry
825 .get_or_create_histogram(&Key::from_name(name), Level::TRACE)
826 }
827
828 fn counter_present(state: &State, name: &'static str) -> bool {
829 state
830 .registry
831 .maps
832 .lock()
833 .unwrap()
834 .counters
835 .contains_key(&Key::from_name(name))
836 }
837
838 fn counter_count(state: &State) -> usize {
839 state.registry.maps.lock().unwrap().counters.len()
840 }
841
842 fn gauge_present(state: &State, name: &'static str) -> bool {
843 state
844 .registry
845 .maps
846 .lock()
847 .unwrap()
848 .gauges
849 .contains_key(&Key::from_name(name))
850 }
851
852 fn histogram_present(state: &State, name: &'static str) -> bool {
853 state
854 .registry
855 .maps
856 .lock()
857 .unwrap()
858 .histograms
859 .contains_key(&Key::from_name(name))
860 }
861
862 fn drain(rx: &mut Receiver<SharedMetricsSnapshot>) -> Vec<MetricsSnapshot> {
863 let mut out = Vec::new();
864 while let Ok(batch) = rx.try_recv() {
865 out.push((*batch).clone());
866 }
867 out
868 }
869
870 fn evicted_names(snapshots: &[MetricsSnapshot]) -> Vec<String> {
871 snapshots
872 .iter()
873 .flat_map(|snapshot| snapshot.evictions.iter().map(|ctx| ctx.name().to_string()))
874 .collect()
875 }
876
877 fn metric_count(state: &AggregatedMetricsState, name: &str) -> usize {
878 let mut count = 0;
879 state.visit_metrics(|context, _| {
880 if context.name() == name {
881 count += 1;
882 }
883 });
884 count
885 }
886
887 #[test]
888 fn delete_counter_from_state_emits_pending_delta_before_eviction() {
889 let (state, _rx) = make_state(IDLE_EVICT_INTERVALS);
890 let key = Key::from_parts(
891 "deleted_counter",
892 vec![Label::new("origin", "container_id://deleted".to_string())],
893 );
894
895 let counter = state.registry.get_or_create_counter(&key, Level::TRACE);
896 counter.increment(7);
897 drop(counter);
898
899 let snapshot = delete_counter_from_state(&state, &key).expect("counter should be deleted");
900
901 assert!(!state.registry.maps.lock().unwrap().counters.contains_key(&key));
902 assert_eq!(snapshot.evictions.len(), 1);
903 assert_eq!(snapshot.evictions[0].name(), "deleted_counter");
904 assert!(snapshot.evictions[0].tags().has_tag("origin:container_id://deleted"));
905
906 assert_eq!(snapshot.upserts.len(), 1);
907 let Event::Metric(metric) = &snapshot.upserts[0] else {
908 panic!("delete should emit the pending counter delta");
909 };
910 assert_eq!(metric.context(), &snapshot.evictions[0]);
911 let MetricValues::Counter(points) = metric.values() else {
912 panic!("delete should emit a counter metric");
913 };
914 assert_eq!(points.into_iter().map(|(_, value)| value).sum::<f64>(), 7.0);
915 }
916
917 #[tokio::test]
918 async fn evicts_high_cardinality_counters_from_registry_and_aggregated_state() {
919 let (state, mut rx) = make_state(IDLE_EVICT_INTERVALS);
920 let mut resolver = resolver();
921 let mut flush_state = FlushState::default();
922 let agg_processor = AggregatedMetricsProcessor;
923 let agg_state = agg_processor.build_initial_state();
924 let mut expected_eviction_tags = FastHashSet::default();
925 let mut observed_eviction_tags = FastHashSet::default();
926
927 for index in 0..HIGH_CARDINALITY_COUNTERS {
928 let origin = format!("entity://source-{index}");
929 expected_eviction_tags.insert(format!("origin:{origin}"));
930
931 let counter = register_counter_with_origin(&state, HIGH_CARDINALITY_COUNTER_NAME, &origin);
932 counter.increment(1);
933 drop(counter);
934 }
935
936 assert_eq!(counter_count(&state), HIGH_CARDINALITY_COUNTERS);
937
938 for _ in 0..IDLE_EVICT_INTERVALS {
939 flush_once(&state, &mut resolver, &mut flush_state);
940
941 for update in drain(&mut rx) {
942 for context in &update.evictions {
943 if context.name() != HIGH_CARDINALITY_COUNTER_NAME {
944 continue;
945 }
946
947 for tag in &expected_eviction_tags {
948 if context.tags().has_tag(tag) {
949 observed_eviction_tags.insert(tag.clone());
950 }
951 }
952 }
953
954 agg_processor.process(update, &agg_state);
955 }
956 }
957
958 assert_eq!(counter_count(&state), 0);
959 assert_eq!(metric_count(&agg_state, HIGH_CARDINALITY_COUNTER_NAME), 0);
960 assert_eq!(observed_eviction_tags, expected_eviction_tags);
961 }
962
963 #[tokio::test]
964 async fn evicts_counter_within_bounded_time_after_handle_dropped() {
965 let (state, mut rx) = make_state(3);
966 let mut resolver = resolver();
967 let mut flush_state = FlushState::default();
968
969 let counter = register_counter(&state, "dropped_counter");
970 counter.increment(5);
971
972 flush_once(&state, &mut resolver, &mut flush_state);
974 assert!(counter_present(&state, "dropped_counter"));
975
976 drop(counter);
978 let _ = drain(&mut rx);
979 for _ in 0..3 {
980 flush_once(&state, &mut resolver, &mut flush_state);
981 }
982 assert!(!counter_present(&state, "dropped_counter"));
983
984 let updates = drain(&mut rx);
986 assert!(evicted_names(&updates).iter().any(|n| n == "dropped_counter"));
987 }
988
989 #[tokio::test]
990 async fn does_not_evict_while_handle_is_held() {
991 let (state, _rx) = make_state(3);
992 let mut resolver = resolver();
993 let mut flush_state = FlushState::default();
994
995 let counter = register_counter(&state, "held_counter");
996
997 for _ in 0..8 {
999 flush_once(&state, &mut resolver, &mut flush_state);
1000 }
1001
1002 assert!(
1003 counter_present(&state, "held_counter"),
1004 "a held metric must never be evicted"
1005 );
1006 drop(counter);
1007 }
1008
1009 #[tokio::test]
1010 async fn reregistration_keeps_uncached_metric_alive() {
1011 let (state, _rx) = make_state(3);
1014 let mut resolver = resolver();
1015 let mut flush_state = FlushState::default();
1016
1017 for _ in 0..8 {
1018 let counter = register_counter(&state, "uncached_counter");
1019 counter.increment(1);
1020 drop(counter);
1021 flush_once(&state, &mut resolver, &mut flush_state);
1022 }
1023 assert!(counter_present(&state, "uncached_counter"));
1024
1025 for _ in 0..3 {
1027 flush_once(&state, &mut resolver, &mut flush_state);
1028 }
1029 assert!(!counter_present(&state, "uncached_counter"));
1030 }
1031
1032 #[tokio::test]
1033 async fn gauge_reset_to_same_value_is_not_evicted() {
1034 let (state, _rx) = make_state(3);
1037 let mut resolver = resolver();
1038 let mut flush_state = FlushState::default();
1039
1040 for _ in 0..8 {
1041 let gauge = register_gauge(&state, "steady_gauge");
1042 gauge.set(0.0);
1043 drop(gauge);
1044 flush_once(&state, &mut resolver, &mut flush_state);
1045 }
1046 assert!(gauge_present(&state, "steady_gauge"));
1047 }
1048
1049 #[tokio::test]
1050 async fn final_counter_delta_reaches_downstream_before_eviction() {
1051 let (state, mut rx) = make_state(1);
1052 let mut resolver = resolver();
1053 let mut flush_state = FlushState::default();
1054
1055 let counter = register_counter(&state, "final_counter");
1056
1057 flush_once(&state, &mut resolver, &mut flush_state);
1059
1060 counter.increment(7);
1063 drop(counter);
1064 flush_once(&state, &mut resolver, &mut flush_state);
1065 assert!(
1066 counter_present(&state, "final_counter"),
1067 "must not evict in an interval that produced a delta"
1068 );
1069
1070 let agg_processor = AggregatedMetricsProcessor;
1072 let agg_state = agg_processor.build_initial_state();
1073 for update in drain(&mut rx) {
1074 agg_processor.process(update, &agg_state);
1075 }
1076 assert_eq!(agg_state.get_aggregated_with_tags("final_counter", &[]), 7.0);
1077
1078 flush_once(&state, &mut resolver, &mut flush_state);
1080 assert!(!counter_present(&state, "final_counter"));
1081 }
1082
1083 #[tokio::test]
1084 async fn gauge_final_value_reaches_downstream_before_eviction() {
1085 let (state, mut rx) = make_state(1);
1086 let mut resolver = resolver();
1087 let mut flush_state = FlushState::default();
1088
1089 let gauge = register_gauge(&state, "final_gauge");
1090 gauge.set(1.0);
1091
1092 flush_once(&state, &mut resolver, &mut flush_state);
1094
1095 gauge.set(42.0);
1099 drop(gauge);
1100 flush_once(&state, &mut resolver, &mut flush_state);
1101 assert!(
1102 gauge_present(&state, "final_gauge"),
1103 "must not evict in an interval that wrote a new value"
1104 );
1105
1106 let agg_processor = AggregatedMetricsProcessor;
1108 let agg_state = agg_processor.build_initial_state();
1109 for update in drain(&mut rx) {
1110 agg_processor.process(update, &agg_state);
1111 }
1112 assert_eq!(agg_state.find_single_with_tags("final_gauge", &[]), Some(42.0));
1113
1114 flush_once(&state, &mut resolver, &mut flush_state);
1116 assert!(!gauge_present(&state, "final_gauge"));
1117 }
1118
1119 #[tokio::test]
1120 async fn histogram_final_samples_reach_downstream_before_eviction() {
1121 let (state, mut rx) = make_state(1);
1133 let mut resolver = resolver();
1134 let mut flush_state = FlushState::default();
1135
1136 let histogram = register_histogram(&state, "final_histogram");
1137
1138 flush_once(&state, &mut resolver, &mut flush_state);
1140
1141 histogram.record(1.5);
1144 drop(histogram);
1145 flush_once(&state, &mut resolver, &mut flush_state);
1146 assert!(
1147 histogram_present(&state, "final_histogram"),
1148 "must not evict in an interval that recorded a sample"
1149 );
1150
1151 let agg_processor = AggregatedMetricsProcessor;
1153 let agg_state = agg_processor.build_initial_state();
1154 for update in drain(&mut rx) {
1155 agg_processor.process(update, &agg_state);
1156 }
1157
1158 let mut downstream_sample_count = None;
1159 agg_state.visit_metrics(|context, value| {
1160 if context.name() == "final_histogram" {
1161 if let AggregatedMetricValue::Histogram(histogram) = value {
1162 downstream_sample_count = Some(histogram.count());
1163 }
1164 }
1165 });
1166 assert_eq!(
1167 downstream_sample_count,
1168 Some(1),
1169 "the final recorded sample must reach downstream before eviction"
1170 );
1171
1172 flush_once(&state, &mut resolver, &mut flush_state);
1174 assert!(!histogram_present(&state, "final_histogram"));
1175 }
1176}
1177
1178#[cfg(all(test, feature = "loom"))]
1200mod loom_tests {
1201 use loom::sync::atomic::{fence, AtomicU64, AtomicUsize, Ordering};
1202 use loom::sync::Arc;
1203
1204 struct Shared {
1207 value: AtomicU64,
1208 strong: AtomicUsize,
1209 }
1210
1211 fn caller_increment_then_drop(shared: &Arc<Shared>, amount: u64) {
1213 shared.value.fetch_add(amount, Ordering::Relaxed);
1214 shared.strong.fetch_sub(1, Ordering::Release); }
1216
1217 fn flush_decision_consume_first(shared: &Arc<Shared>) -> (u64, bool) {
1219 let delta = shared.value.swap(0, Ordering::Relaxed);
1220 let orphaned = shared.strong.load(Ordering::Relaxed) == 1;
1221 (delta, orphaned && delta == 0)
1222 }
1223
1224 fn flush_decision_strong_count_first(shared: &Arc<Shared>) -> (u64, bool) {
1227 let orphaned = shared.strong.load(Ordering::Relaxed) == 1;
1228 if orphaned {
1229 fence(Ordering::Acquire);
1230 }
1231 let delta = shared.value.swap(0, Ordering::Relaxed);
1232 (delta, orphaned && delta == 0)
1233 }
1234
1235 fn model(decision: fn(&Arc<Shared>) -> (u64, bool)) {
1236 loom::model(move || {
1237 const FINAL: u64 = 7;
1238
1239 let shared = Arc::new(Shared {
1241 value: AtomicU64::new(0),
1242 strong: AtomicUsize::new(2),
1243 });
1244 let caller_shared = Arc::clone(&shared);
1245
1246 let caller = loom::thread::spawn(move || {
1247 caller_increment_then_drop(&caller_shared, FINAL);
1248 });
1249
1250 let (delta, evicted) = decision(&shared);
1252 caller.join().unwrap();
1253
1254 if evicted {
1258 assert_eq!(
1259 delta,
1260 FINAL,
1261 "counter evicted while {} counts were never emitted -> permanently lost",
1262 FINAL - delta
1263 );
1264 }
1265 });
1266 }
1267
1268 #[test]
1272 #[should_panic(expected = "permanently lost")]
1273 fn consume_first_ordering_loses_a_final_increment() {
1274 model(flush_decision_consume_first);
1275 }
1276
1277 #[test]
1280 fn strong_count_first_ordering_preserves_a_final_increment() {
1281 model(flush_decision_strong_count_first);
1282 }
1283
1284 fn histogram_model(decision: fn(&Arc<Shared>) -> (u64, bool)) {
1299 loom::model(move || {
1300 const FINAL_SAMPLES: u64 = 1;
1301
1302 let shared = Arc::new(Shared {
1304 value: AtomicU64::new(0),
1305 strong: AtomicUsize::new(2),
1306 });
1307 let caller_shared = Arc::clone(&shared);
1308
1309 let caller = loom::thread::spawn(move || {
1311 caller_shared.value.fetch_add(FINAL_SAMPLES, Ordering::Relaxed); caller_shared.strong.fetch_sub(1, Ordering::Release); });
1314
1315 let (drained, evicted) = decision(&shared);
1317 caller.join().unwrap();
1318
1319 if evicted {
1323 assert_eq!(
1324 drained,
1325 FINAL_SAMPLES,
1326 "histogram evicted while {} recorded sample(s) were never drained -> permanently lost",
1327 FINAL_SAMPLES - drained
1328 );
1329 }
1330 });
1331 }
1332
1333 #[test]
1337 #[should_panic(expected = "permanently lost")]
1338 fn histogram_drain_first_ordering_loses_a_final_sample() {
1339 histogram_model(flush_decision_consume_first);
1340 }
1341
1342 #[test]
1345 fn histogram_strong_count_first_ordering_preserves_a_final_sample() {
1346 histogram_model(flush_decision_strong_count_first);
1347 }
1348}