Skip to main content

vector/sinks/util/buffer/metrics/
normalize.rs

1use std::{
2    marker::PhantomData,
3    time::{Duration, Instant},
4};
5
6use indexmap::IndexMap;
7use lru::LruCache;
8use serde_with::serde_as;
9use snafu::Snafu;
10use vector_config_macros::configurable_component;
11use vector_lib::{
12    ByteSizeOf,
13    event::{
14        EventMetadata, Metric, MetricKind,
15        metric::{MetricData, MetricSeries},
16    },
17};
18
19#[derive(Debug, Snafu, PartialEq, Eq)]
20pub enum NormalizerError {
21    #[snafu(display("`max_bytes` must be greater than zero"))]
22    InvalidMaxBytes,
23    #[snafu(display("`max_events` must be greater than zero"))]
24    InvalidMaxEvents,
25    #[snafu(display("`time_to_live` must be greater than zero"))]
26    InvalidTimeToLive,
27}
28
29/// Defines behavior for creating the MetricNormalizer
30#[serde_as]
31#[configurable_component]
32#[derive(Clone, Copy, Debug, Default)]
33pub struct NormalizerConfig<D: NormalizerSettings + Clone> {
34    /// The maximum size in bytes of the events in the metrics normalizer cache, excluding cache overhead.
35    #[serde(default = "default_max_bytes::<D>")]
36    #[configurable(metadata(docs::type_unit = "bytes"))]
37    pub max_bytes: Option<usize>,
38
39    /// The maximum number of events of the metrics normalizer cache
40    #[serde(default = "default_max_events::<D>")]
41    #[configurable(metadata(docs::type_unit = "events"))]
42    pub max_events: Option<usize>,
43
44    /// The maximum age of a metric not being updated before it is evicted from the metrics normalizer cache.
45    #[serde(default = "default_time_to_live::<D>")]
46    #[configurable(metadata(docs::type_unit = "seconds"))]
47    #[configurable(metadata(docs::human_name = "Time To Live"))]
48    pub time_to_live: Option<u64>,
49
50    #[serde(skip)]
51    pub _d: PhantomData<D>,
52}
53
54const fn default_max_bytes<D: NormalizerSettings>() -> Option<usize> {
55    D::MAX_BYTES
56}
57
58const fn default_max_events<D: NormalizerSettings>() -> Option<usize> {
59    D::MAX_EVENTS
60}
61
62const fn default_time_to_live<D: NormalizerSettings>() -> Option<u64> {
63    D::TIME_TO_LIVE
64}
65
66impl<D: NormalizerSettings + Clone> NormalizerConfig<D> {
67    pub fn validate(&self) -> Result<NormalizerConfig<D>, NormalizerError> {
68        let config = NormalizerConfig::<D> {
69            max_bytes: self.max_bytes.or(D::MAX_BYTES),
70            max_events: self.max_events.or(D::MAX_EVENTS),
71            time_to_live: self.time_to_live.or(D::TIME_TO_LIVE),
72            _d: Default::default(),
73        };
74        match (config.max_bytes, config.max_events, config.time_to_live) {
75            (Some(0), _, _) => Err(NormalizerError::InvalidMaxBytes),
76            (_, Some(0), _) => Err(NormalizerError::InvalidMaxEvents),
77            (_, _, Some(0)) => Err(NormalizerError::InvalidTimeToLive),
78            _ => Ok(config),
79        }
80    }
81
82    pub const fn into_settings(self) -> MetricSetSettings {
83        MetricSetSettings {
84            max_bytes: self.max_bytes,
85            max_events: self.max_events,
86            time_to_live: self.time_to_live,
87        }
88    }
89}
90
91pub trait NormalizerSettings {
92    const MAX_EVENTS: Option<usize>;
93    const MAX_BYTES: Option<usize>;
94    const TIME_TO_LIVE: Option<u64>;
95}
96
97#[derive(Clone, Copy, Debug, Default)]
98pub struct DefaultNormalizerSettings;
99
100impl NormalizerSettings for DefaultNormalizerSettings {
101    const MAX_EVENTS: Option<usize> = None;
102    const MAX_BYTES: Option<usize> = None;
103    const TIME_TO_LIVE: Option<u64> = None;
104}
105
106/// Normalizes metrics according to a set of rules.
107///
108/// Depending on the system in which they are being sent to, metrics may have to be modified in order to fit the data
109/// model or constraints placed on that system.  Typically, this boils down to whether or not the system can accept
110/// absolute metrics or incremental metrics: the latest value of a metric, or the delta between the last time the
111/// metric was observed and now, respective. Other rules may need to be applied, such as dropping metrics of a specific
112/// type that the system does not support.
113///
114/// The trait provides a simple interface to apply this logic uniformly, given a reference to a simple state container
115/// that allows tracking the necessary information of a given metric over time. As well, given the optional return, it
116/// composes nicely with iterators (i.e. using `filter_map`) in order to filter metrics within existing
117/// iterator/stream-based approaches.
118pub trait MetricNormalize {
119    /// Normalizes the metric against the given state.
120    ///
121    /// If the metric was normalized successfully, `Some(metric)` will be returned. Otherwise, `None` is returned.
122    ///
123    /// In some cases, a metric may be successfully added/tracked within the given state, but due to the normalization
124    /// logic, it cannot yet be emitted. An example of this is normalizing all metrics to be incremental.
125    ///
126    /// In this example, if an incoming metric is already incremental, it can be passed through unchanged.  If the
127    /// incoming metric is absolute, however, we need to see it at least twice in order to calculate the incremental
128    /// delta necessary to emit an incremental version. This means that the first time an absolute metric is seen,
129    /// `normalize` would return `None`, and the subsequent calls would return `Some(metric)`.
130    ///
131    /// However, a metric may simply not be supported by a normalization implementation, and so `None` may or may not be
132    /// a common return value. This behavior is, thus, implementation defined.
133    fn normalize(&mut self, state: &mut MetricSet, metric: Metric) -> Option<Metric>;
134}
135
136/// A self-contained metric normalizer.
137///
138/// The normalization state is stored internally, and it can only be created from a normalizer implementation that is
139/// either `Default` or is constructed ahead of time, so it is primarily useful for constructing a usable normalizer
140/// via implicit conversion methods or when no special parameters are required for configuring the underlying normalizer.
141pub struct MetricNormalizer<N> {
142    state: MetricSet,
143    normalizer: N,
144}
145
146impl<N> MetricNormalizer<N> {
147    /// Creates a new normalizer with the given configuration.
148    pub fn with_config<D: NormalizerSettings + Clone>(
149        normalizer: N,
150        config: NormalizerConfig<D>,
151    ) -> Self {
152        let settings = config
153            .validate()
154            .unwrap_or_else(|e| panic!("Invalid cache settings: {e:?}"))
155            .into_settings();
156        Self {
157            state: MetricSet::new(settings),
158            normalizer,
159        }
160    }
161
162    /// Creates a new normalizer with a time-to-live policy.
163    pub fn with_ttl(normalizer: N, ttl: Duration) -> Self {
164        Self {
165            state: MetricSet::with_policies(None, Some(TtlPolicy::new(ttl))),
166            normalizer,
167        }
168    }
169
170    /// Gets a mutable reference to the current metric state for this normalizer.
171    pub const fn get_state_mut(&mut self) -> &mut MetricSet {
172        &mut self.state
173    }
174}
175
176impl<N: MetricNormalize> MetricNormalizer<N> {
177    /// Normalizes the metric against the internal normalization state.
178    ///
179    /// For more information about normalization, see the documentation for [`MetricNormalize::normalize`].
180    pub fn normalize(&mut self, metric: Metric) -> Option<Metric> {
181        self.normalizer.normalize(&mut self.state, metric)
182    }
183}
184
185impl<N: Default> Default for MetricNormalizer<N> {
186    fn default() -> Self {
187        Self {
188            state: MetricSet::default(),
189            normalizer: N::default(),
190        }
191    }
192}
193
194impl<N> From<N> for MetricNormalizer<N> {
195    fn from(normalizer: N) -> Self {
196        Self {
197            state: MetricSet::default(),
198            normalizer,
199        }
200    }
201}
202
203/// Represents a stored metric entry with its data, metadata, and timestamp.
204#[derive(Clone, Debug)]
205pub struct MetricEntry {
206    /// The metric data containing the value and kind
207    pub data: MetricData,
208    /// Event metadata associated with this metric
209    pub metadata: EventMetadata,
210    /// Optional timestamp for TTL tracking
211    pub timestamp: Option<Instant>,
212}
213
214impl ByteSizeOf for MetricEntry {
215    fn allocated_bytes(&self) -> usize {
216        self.data.allocated_bytes() + self.metadata.allocated_bytes()
217    }
218}
219
220impl MetricEntry {
221    /// Creates a new MetricEntry with the given data, metadata, and timestamp.
222    pub const fn new(
223        data: MetricData,
224        metadata: EventMetadata,
225        timestamp: Option<Instant>,
226    ) -> Self {
227        Self {
228            data,
229            metadata,
230            timestamp,
231        }
232    }
233
234    /// Creates a new MetricEntry from a Metric.
235    pub fn from_metric(metric: Metric, timestamp: Option<Instant>) -> (MetricSeries, Self) {
236        let (series, data, metadata) = metric.into_parts();
237        let entry = Self::new(data, metadata, timestamp);
238        (series, entry)
239    }
240
241    /// Converts this entry back to a Metric with the given series.
242    pub fn into_metric(self, series: MetricSeries) -> Metric {
243        Metric::from_parts(series, self.data, self.metadata)
244    }
245
246    /// Updates this entry's timestamp.
247    pub const fn update_timestamp(&mut self, timestamp: Option<Instant>) {
248        self.timestamp = timestamp;
249    }
250
251    /// Checks if this entry has expired based on the given TTL and reference time.
252    ///
253    /// Using a provided reference time ensures consistency across multiple expiration checks.
254    pub fn is_expired(&self, ttl: Duration, reference_time: Instant) -> bool {
255        match self.timestamp {
256            Some(ts) => reference_time.duration_since(ts) >= ttl,
257            None => false,
258        }
259    }
260}
261
262/// Configuration for capacity-based eviction (memory and/or entry count limits).
263#[derive(Clone, Debug)]
264pub struct CapacityPolicy {
265    /// Maximum memory usage in bytes
266    pub max_bytes: Option<usize>,
267    /// Maximum number of entries
268    pub max_events: Option<usize>,
269    /// Current memory usage tracking
270    current_memory: usize,
271}
272
273impl CapacityPolicy {
274    /// Creates a new capacity policy with both memory and entry limits.
275    pub const fn new(max_bytes: Option<usize>, max_events: Option<usize>) -> Self {
276        Self {
277            max_bytes,
278            max_events,
279            current_memory: 0,
280        }
281    }
282
283    /// Gets the current memory usage.
284    pub const fn current_memory(&self) -> usize {
285        self.current_memory
286    }
287
288    /// Updates memory tracking when an entry is removed.
289    const fn remove_memory(&mut self, bytes: usize) {
290        self.current_memory = self.current_memory.saturating_sub(bytes);
291    }
292
293    /// Frees the memory for an item if max_bytes is set.
294    /// Only calculates and tracks memory when max_bytes is specified.
295    pub fn free_item(&mut self, series: &MetricSeries, entry: &MetricEntry) {
296        if self.max_bytes.is_some() {
297            let freed_memory = self.item_size(series, entry);
298            self.remove_memory(freed_memory);
299        }
300    }
301
302    /// Updates memory tracking.
303    const fn replace_memory(&mut self, old_bytes: usize, new_bytes: usize) {
304        self.current_memory = self
305            .current_memory
306            .saturating_sub(old_bytes)
307            .saturating_add(new_bytes);
308    }
309
310    /// Checks if the current state exceeds memory limits.
311    const fn exceeds_memory_limit(&self) -> bool {
312        if let Some(max_bytes) = self.max_bytes {
313            self.current_memory > max_bytes
314        } else {
315            false
316        }
317    }
318
319    /// Checks if the given entry count exceeds entry limits.
320    const fn exceeds_entry_limit(&self, entry_count: usize) -> bool {
321        if let Some(max_events) = self.max_events {
322            entry_count > max_events
323        } else {
324            false
325        }
326    }
327
328    /// Returns true if any limits are currently exceeded.
329    const fn needs_eviction(&self, entry_count: usize) -> bool {
330        self.exceeds_memory_limit() || self.exceeds_entry_limit(entry_count)
331    }
332
333    /// Gets the total memory size of entry/series, excluding LRU cache overhead.
334    pub fn item_size(&self, series: &MetricSeries, entry: &MetricEntry) -> usize {
335        entry.allocated_bytes() + series.allocated_bytes()
336    }
337}
338
339#[derive(Clone, Debug)]
340pub struct TtlPolicy {
341    /// Time-to-live for entries
342    pub ttl: Duration,
343    /// How often to run cleanup
344    pub cleanup_interval: Duration,
345    /// Last time cleanup was performed
346    pub(crate) last_cleanup: Instant,
347}
348
349/// Configuration for automatic cleanup of expired entries.
350impl TtlPolicy {
351    /// Creates a new TTL policy with the given duration.
352    /// Cleanup interval defaults to TTL/10 with a 10-second minimum.
353    pub fn new(ttl: Duration) -> Self {
354        Self {
355            ttl,
356            cleanup_interval: ttl.div_f32(10.0).max(Duration::from_secs(10)),
357            last_cleanup: Instant::now(),
358        }
359    }
360
361    /// Checks if it's time to run cleanup.
362    ///
363    /// Returns Some(current_time) if cleanup should be performed, None otherwise.
364    pub fn should_cleanup(&self) -> Option<Instant> {
365        let now = Instant::now();
366        if now.duration_since(self.last_cleanup) >= self.cleanup_interval {
367            Some(now)
368        } else {
369            None
370        }
371    }
372
373    /// Marks cleanup as having been performed with the provided timestamp.
374    pub const fn mark_cleanup_done(&mut self, now: Instant) {
375        self.last_cleanup = now;
376    }
377}
378
379#[derive(Debug, Clone, Copy, Default)]
380pub struct MetricSetSettings {
381    pub max_bytes: Option<usize>,
382    pub max_events: Option<usize>,
383    pub time_to_live: Option<u64>,
384}
385
386/// Inner storage for `MetricSet`.
387///
388/// Uses `IndexMap` when no capacity eviction policy is configured — avoiding the
389/// per-access LRU bookkeeping (pointer chasing in a doubly-linked list) that
390/// `LruCache::get_mut` performs unconditionally.  `LruCache` is used only when a
391/// capacity policy is set, so that LRU eviction order is maintained correctly.
392#[derive(Clone, Debug)]
393enum MetricSetInner {
394    /// Unbounded storage with no eviction.  Hash-map lookup only, no LRU overhead.
395    Unbounded(IndexMap<MetricSeries, MetricEntry>),
396    /// Bounded storage with LRU eviction semantics.
397    Bounded(LruCache<MetricSeries, MetricEntry>),
398}
399
400impl MetricSetInner {
401    fn len(&self) -> usize {
402        match self {
403            Self::Unbounded(m) => m.len(),
404            Self::Bounded(m) => m.len(),
405        }
406    }
407
408    fn is_empty(&self) -> bool {
409        match self {
410            Self::Unbounded(m) => m.is_empty(),
411            Self::Bounded(m) => m.is_empty(),
412        }
413    }
414
415    /// Returns a mutable reference to the entry.
416    ///
417    /// For `Unbounded` this is a plain hash-map lookup.
418    /// For `Bounded` this also promotes the entry to the MRU end of the LRU list.
419    fn get_mut(&mut self, key: &MetricSeries) -> Option<&mut MetricEntry> {
420        match self {
421            Self::Unbounded(m) => m.get_mut(key),
422            Self::Bounded(m) => m.get_mut(key),
423        }
424    }
425
426    /// Inserts or replaces an entry, returning the previous value if any.
427    fn put(&mut self, key: MetricSeries, value: MetricEntry) -> Option<MetricEntry> {
428        match self {
429            Self::Unbounded(m) => m.insert(key, value),
430            Self::Bounded(m) => m.put(key, value),
431        }
432    }
433
434    /// Removes an entry by key, returning it if present.
435    fn pop(&mut self, key: &MetricSeries) -> Option<MetricEntry> {
436        match self {
437            // swap_remove is O(1) vs shift_remove's O(n); insertion order is not required here.
438            Self::Unbounded(m) => m.swap_remove(key),
439            Self::Bounded(m) => m.pop(key),
440        }
441    }
442
443    fn iter(&self) -> MetricSetIter<'_> {
444        match self {
445            Self::Unbounded(m) => MetricSetIter::Unbounded(m.iter()),
446            Self::Bounded(m) => MetricSetIter::Bounded(m.iter()),
447        }
448    }
449}
450
451enum MetricSetIter<'a> {
452    Unbounded(indexmap::map::Iter<'a, MetricSeries, MetricEntry>),
453    Bounded(lru::Iter<'a, MetricSeries, MetricEntry>),
454}
455
456impl<'a> Iterator for MetricSetIter<'a> {
457    type Item = (&'a MetricSeries, &'a MetricEntry);
458
459    fn next(&mut self) -> Option<Self::Item> {
460        match self {
461            Self::Unbounded(it) => it.next(),
462            Self::Bounded(it) => it.next(),
463        }
464    }
465}
466
467/// Dual-limit cache for metric normalization with optional capacity and TTL policies.
468///
469/// Uses `IndexMap` internally when no capacity eviction policy is configured, avoiding
470/// the per-access LRU pointer-manipulation overhead of `LruCache`. Switches to
471/// `LruCache` only when a `max_bytes` or `max_events` capacity policy is set, so that
472/// LRU eviction ordering is preserved for those cases.
473#[derive(Clone, Debug)]
474pub struct MetricSet {
475    inner: MetricSetInner,
476    /// Optional capacity policy for memory and/or entry count limits
477    capacity_policy: Option<CapacityPolicy>,
478    /// Optional TTL policy for time-based expiration
479    ttl_policy: Option<TtlPolicy>,
480}
481
482impl MetricSet {
483    /// Creates a new MetricSet with the given settings.
484    pub fn new(settings: MetricSetSettings) -> Self {
485        // Create capacity policy if any capacity limit is set
486        let capacity_policy = match (settings.max_bytes, settings.max_events) {
487            (None, None) => None,
488            (max_bytes, max_events) => Some(CapacityPolicy::new(max_bytes, max_events)),
489        };
490
491        // Create TTL policy if time-to-live is set
492        let ttl_policy = settings
493            .time_to_live
494            .map(|ttl| TtlPolicy::new(Duration::from_secs(ttl)));
495
496        Self::with_policies(capacity_policy, ttl_policy)
497    }
498
499    /// Creates a new MetricSet with the given policies.
500    pub fn with_policies(
501        capacity_policy: Option<CapacityPolicy>,
502        ttl_policy: Option<TtlPolicy>,
503    ) -> Self {
504        // Use LruCache only when a capacity policy requires LRU eviction ordering.
505        // Without a capacity policy, IndexMap avoids the per-access LRU overhead.
506        let inner = if capacity_policy.is_some() {
507            MetricSetInner::Bounded(LruCache::unbounded())
508        } else {
509            MetricSetInner::Unbounded(IndexMap::default())
510        };
511        Self {
512            inner,
513            capacity_policy,
514            ttl_policy,
515        }
516    }
517
518    /// Gets the current capacity policy.
519    pub const fn capacity_policy(&self) -> Option<&CapacityPolicy> {
520        self.capacity_policy.as_ref()
521    }
522
523    /// Gets the current TTL policy.
524    pub const fn ttl_policy(&self) -> Option<&TtlPolicy> {
525        self.ttl_policy.as_ref()
526    }
527
528    /// Gets a mutable reference to the TTL policy configuration.
529    pub const fn ttl_policy_mut(&mut self) -> Option<&mut TtlPolicy> {
530        self.ttl_policy.as_mut()
531    }
532
533    /// Gets the current number of entries in the cache.
534    pub fn len(&self) -> usize {
535        self.inner.len()
536    }
537
538    /// Returns true if the cache contains no entries.
539    pub fn is_empty(&self) -> bool {
540        self.inner.is_empty()
541    }
542
543    /// Gets the current memory usage in bytes.
544    pub fn weighted_size(&self) -> u64 {
545        self.capacity_policy
546            .as_ref()
547            .map_or(0, |cp| cp.current_memory() as u64)
548    }
549
550    /// Creates a timestamp if TTL is enabled.
551    fn create_timestamp(&self) -> Option<Instant> {
552        self.ttl_policy.as_ref().map(|_| Instant::now())
553    }
554
555    /// Enforce memory and entry limits by evicting LRU entries.
556    fn enforce_capacity_policy(&mut self) {
557        let Some(ref mut capacity_policy) = self.capacity_policy else {
558            return; // No capacity limits configured
559        };
560
561        // A capacity policy is only set when inner is Bounded; this should always be true.
562        let MetricSetInner::Bounded(ref mut lru) = self.inner else {
563            debug_assert!(false, "capacity policy set but inner is not Bounded");
564            return;
565        };
566
567        // Keep evicting until we're within limits
568        while capacity_policy.needs_eviction(lru.len()) {
569            if let Some((series, entry)) = lru.pop_lru() {
570                capacity_policy.free_item(&series, &entry);
571            } else {
572                break; // No more entries to evict
573            }
574        }
575    }
576
577    /// Perform TTL cleanup if configured and needed.
578    fn maybe_cleanup(&mut self) {
579        // Check if cleanup is needed and get the current timestamp in one operation
580        let now = match self.ttl_policy().and_then(|config| config.should_cleanup()) {
581            Some(timestamp) => timestamp,
582            None => return, // No cleanup needed
583        };
584
585        // Perform the cleanup using the same timestamp
586        self.cleanup_expired(now);
587
588        // Mark cleanup as done with the same timestamp
589        if let Some(config) = self.ttl_policy_mut() {
590            config.mark_cleanup_done(now);
591        }
592    }
593
594    /// Remove expired entries based on TTL using the provided timestamp.
595    fn cleanup_expired(&mut self, now: Instant) {
596        // Get the TTL from the policy
597        let Some(ttl) = self.ttl_policy().map(|policy| policy.ttl) else {
598            return; // No TTL policy, nothing to do
599        };
600
601        // Collect expired keys using the provided timestamp
602        let expired_keys: Vec<MetricSeries> = self
603            .inner
604            .iter()
605            .filter(|(_, e)| e.is_expired(ttl, now))
606            .map(|(s, _)| s.clone())
607            .collect();
608
609        // Remove expired entries and update memory tracking (if max_bytes is set)
610        for series in expired_keys {
611            if let Some(entry) = self.inner.pop(&series)
612                && let Some(ref mut capacity_policy) = self.capacity_policy
613            {
614                capacity_policy.free_item(&series, &entry);
615            }
616        }
617    }
618
619    /// Internal insert that updates memory tracking and enforces limits.
620    fn insert_with_tracking(&mut self, series: MetricSeries, entry: MetricEntry) {
621        let Some(ref mut capacity_policy) = self.capacity_policy else {
622            self.inner.put(series, entry);
623            return; // No capacity limits configured, return immediately
624        };
625
626        // Handle differently based on whether we need to track memory
627        if capacity_policy.max_bytes.is_some() {
628            // When tracking memory, we need to calculate sizes before and after
629            let entry_size = capacity_policy.item_size(&series, &entry);
630
631            if let Some(existing_entry) = self.inner.put(series.clone(), entry) {
632                // If we had an existing entry, calculate its size and adjust memory tracking
633                let existing_size = capacity_policy.item_size(&series, &existing_entry);
634                capacity_policy.replace_memory(existing_size, entry_size);
635            } else {
636                // No existing entry, just add the new entry's size
637                capacity_policy.replace_memory(0, entry_size);
638            }
639        } else {
640            // When not tracking memory (only entry count limits), just put directly
641            self.inner.put(series, entry);
642        }
643
644        // Enforce limits after insertion
645        self.enforce_capacity_policy();
646    }
647
648    /// Consumes this MetricSet and returns a vector of Metric.
649    pub fn into_metrics(mut self) -> Vec<Metric> {
650        // Clean up expired entries first (using current time)
651        self.cleanup_expired(Instant::now());
652        match self.inner {
653            MetricSetInner::Unbounded(m) => m
654                .into_iter()
655                .map(|(series, entry)| entry.into_metric(series))
656                .collect(),
657            MetricSetInner::Bounded(mut m) => {
658                let mut metrics = Vec::with_capacity(m.len());
659                while let Some((series, entry)) = m.pop_lru() {
660                    metrics.push(entry.into_metric(series));
661                }
662                metrics
663            }
664        }
665    }
666
667    /// Either pass the metric through as-is if absolute, or convert it
668    /// to absolute if incremental.
669    pub fn make_absolute(&mut self, metric: Metric) -> Option<Metric> {
670        self.maybe_cleanup();
671        match metric.kind() {
672            MetricKind::Absolute => Some(metric),
673            MetricKind::Incremental => Some(self.incremental_to_absolute(metric)),
674        }
675    }
676
677    /// Either convert the metric to incremental if absolute, or
678    /// aggregate it with any previous value if already incremental.
679    pub fn make_incremental(&mut self, metric: Metric) -> Option<Metric> {
680        self.maybe_cleanup();
681        match metric.kind() {
682            MetricKind::Absolute => self.absolute_to_incremental(metric),
683            MetricKind::Incremental => Some(metric),
684        }
685    }
686
687    /// Convert the incremental metric into an absolute one, using the
688    /// state buffer to keep track of the value throughout the entire
689    /// application uptime.
690    fn incremental_to_absolute(&mut self, mut metric: Metric) -> Metric {
691        let timestamp = self.create_timestamp();
692        // We always call insert() to track memory usage
693        if let Some(existing) = self.inner.get_mut(metric.series()) {
694            let mut new_value = existing.data.value().clone();
695            if new_value.add(metric.value()) {
696                // Update the stored value
697                metric = metric.with_value(new_value);
698            }
699        }
700        // Strip finalizers from the clone before caching. The normalization cache must not
701        // hold Arc<EventFinalizer> references, as that prevents the disk buffer from
702        // acknowledging events — leading to a deadlock once the buffer fills.
703        let mut cache_metric = metric.clone();
704        cache_metric.metadata_mut().take_finalizers();
705        self.insert(cache_metric, timestamp);
706        metric.into_absolute()
707    }
708
709    /// Convert the absolute metric into an incremental by calculating
710    /// the increment from the last saved absolute state.
711    fn absolute_to_incremental(&mut self, mut metric: Metric) -> Option<Metric> {
712        // NOTE: Crucially, like I did, you may wonder: why do we not always return a metric? Could
713        // this lead to issues where a metric isn't seen again and we, in effect, never emit it?
714        //
715        // You're not wrong, and that does happen based on the logic below.  However, the main
716        // problem this logic solves is avoiding massive counter updates when Vector restarts.
717        //
718        // If we emitted a metric for a newly-seen absolute metric in this method, we would
719        // naturally have to emit an incremental version where the value was the absolute value,
720        // with subsequent updates being only delta updates.  If we restarted Vector, however, we
721        // would be back to not having yet seen the metric before, so the first emission of the
722        // metric after converting it here would be... its absolute value.  Even if the value only
723        // changed by 1 between Vector stopping and restarting, we could be incrementing the counter
724        // by some outrageous amount.
725        //
726        // Thus, we only emit a metric when we've calculated an actual delta for it, which means
727        // that, yes, we're risking never seeing a metric if it's not re-emitted, and we're
728        // introducing a small amount of lag before a metric is emitted by having to wait to see it
729        // again, but this is a behavior we have to observe for sinks that can only handle
730        // incremental updates.
731        let timestamp = self.create_timestamp();
732        // We always call insert() to track memory usage
733        if let Some(reference) = self.inner.get_mut(metric.series()) {
734            let new_value = metric.value().clone();
735            // Create a copy of the reference so we can insert and
736            // replace the existing entry, tracking memory usage
737            let mut new_reference = reference.clone();
738            // From the stored reference value, emit an increment
739            if metric.subtract(&reference.data) {
740                new_reference.data.value = new_value;
741                new_reference.timestamp = timestamp;
742                self.insert_with_tracking(metric.series().clone(), new_reference);
743                return Some(metric.into_incremental());
744            }
745            // Metric changed type — fall through to store as new baseline
746        }
747        // No reference, or metric changed type: cache as baseline and emit nothing.
748        // Strip finalizers before caching (see incremental_to_absolute).
749        metric.metadata_mut().take_finalizers();
750        self.insert(metric, timestamp);
751        None
752    }
753
754    fn insert(&mut self, metric: Metric, timestamp: Option<Instant>) {
755        let (series, entry) = MetricEntry::from_metric(metric, timestamp);
756        self.insert_with_tracking(series, entry);
757    }
758
759    pub fn insert_update(&mut self, metric: Metric) {
760        self.maybe_cleanup();
761        let timestamp = self.create_timestamp();
762        let update = match metric.kind() {
763            MetricKind::Absolute => Some(metric),
764            MetricKind::Incremental => {
765                // Incremental metrics update existing entries, if present
766                match self.inner.get_mut(metric.series()) {
767                    Some(existing) => {
768                        // Create a copy of the reference so we can insert and
769                        // replace the existing entry, tracking memory usage
770                        let mut new_existing = existing.clone();
771                        let (series, data, metadata) = metric.into_parts();
772                        if new_existing.data.update(&data) {
773                            new_existing.metadata.merge(metadata);
774                            new_existing.update_timestamp(timestamp);
775                            self.insert_with_tracking(series, new_existing);
776                            None
777                        } else {
778                            warn!(message = "Metric changed type, dropping old value.", %series);
779                            Some(Metric::from_parts(series, data, metadata))
780                        }
781                    }
782                    None => Some(metric),
783                }
784            }
785        };
786        if let Some(metric) = update {
787            self.insert(metric, timestamp);
788        }
789    }
790
791    /// Removes a series from the cache.
792    ///
793    /// If the series existed and was removed, returns true.  Otherwise, false.
794    pub fn remove(&mut self, series: &MetricSeries) -> bool {
795        if let Some(entry) = self.inner.pop(series) {
796            if let Some(ref mut capacity_policy) = self.capacity_policy {
797                capacity_policy.free_item(series, &entry);
798            }
799            return true;
800        }
801        false
802    }
803}
804
805impl Default for MetricSet {
806    fn default() -> Self {
807        Self::new(MetricSetSettings::default())
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use vector_lib::event::metric::{MetricKind, MetricValue};
814
815    use super::*;
816
817    fn counter(name: &str, value: f64, kind: MetricKind) -> Metric {
818        Metric::new(name, kind, MetricValue::Counter { value })
819    }
820
821    // Verifies that the default (no capacity policy) path uses IndexMap and that
822    // make_absolute / into_metrics behave correctly across multiple updates.
823    #[test]
824    fn unbounded_incremental_to_absolute_accumulates() {
825        let mut set = MetricSet::default();
826        assert!(matches!(set.inner, MetricSetInner::Unbounded(_)));
827
828        // First incremental: stored as reference, emitted as absolute 1.0
829        let out = set.make_absolute(counter("hits", 1.0, MetricKind::Incremental));
830        assert_eq!(out.unwrap().value(), &MetricValue::Counter { value: 1.0 });
831
832        // Second incremental: accumulated with previous, emitted as absolute 3.0
833        let out = set.make_absolute(counter("hits", 2.0, MetricKind::Incremental));
834        assert_eq!(out.unwrap().value(), &MetricValue::Counter { value: 3.0 });
835
836        // into_metrics drains the set and returns all tracked series
837        let metrics = set.into_metrics();
838        assert_eq!(metrics.len(), 1);
839        assert_eq!(metrics[0].name(), "hits");
840    }
841
842    #[test]
843    fn unbounded_absolute_passes_through() {
844        let mut set = MetricSet::default();
845
846        let out = set.make_absolute(counter("rps", 42.0, MetricKind::Absolute));
847        assert_eq!(out.unwrap().value(), &MetricValue::Counter { value: 42.0 });
848
849        // Absolute metrics are not stored in the set
850        assert!(set.is_empty());
851    }
852
853    // Verifies that capacity policy switches to the LruCache (Bounded) path.
854    #[test]
855    fn bounded_path_selected_when_capacity_policy_set() {
856        let set = MetricSet::new(MetricSetSettings {
857            max_events: Some(10),
858            ..Default::default()
859        });
860        assert!(matches!(set.inner, MetricSetInner::Bounded(_)));
861    }
862}