Skip to main content

fast_telemetry/metric/dynamic/
mod.rs

1//! Runtime-labeled metrics for dynamic dimensions.
2//!
3//! Use these types when label values are not known at compile time
4//! (e.g. `endpoint_uuid`, `org_id`, `user_id`).
5//!
6//! Each type provides:
7//! - Thread-local caching for hot paths
8//! - Sharded index for concurrent access
9//! - Label canonicalization (order-independent)
10//! - Series handles for zero-lookup repeated access
11//! - Access-timestamp eviction for bounded cardinality
12
13mod cache;
14mod counter;
15mod counter_set;
16mod distribution;
17mod gauge;
18mod gauge_i64;
19mod histogram;
20
21pub use counter::{DynamicCounter, DynamicCounterSeries};
22pub use counter_set::{DynamicCounterSet, DynamicCounterSetSeries};
23pub use distribution::{DynamicDistribution, DynamicDistributionSeries};
24pub use gauge::{DynamicGauge, DynamicGaugeSeries};
25pub use gauge_i64::{DynamicGaugeI64, DynamicGaugeI64Series};
26pub use histogram::{DynamicHistogram, DynamicHistogramSeries, DynamicHistogramSeriesView};
27
28use std::collections::BTreeMap;
29use std::sync::atomic::AtomicUsize;
30#[cfg(feature = "eviction")]
31use std::sync::atomic::{AtomicU32, Ordering};
32
33pub(crate) use crate::thread_id::thread_id;
34
35#[cfg(feature = "eviction")]
36static EVICTION_CYCLE: AtomicU32 = AtomicU32::new(0);
37
38#[cfg(all(test, feature = "eviction"))]
39static EVICTION_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
40
41const VEC_CANONICALIZATION_MAX_LABELS: usize = 16;
42
43/// Get the current eviction cycle.
44#[cfg(feature = "eviction")]
45#[inline]
46pub fn current_cycle() -> u32 {
47    EVICTION_CYCLE.load(Ordering::Relaxed)
48}
49
50/// Advance the eviction cycle by 1 and return the new value.
51///
52/// Call this from your exporter task before calling `evict_stale()` on metrics.
53#[cfg(feature = "eviction")]
54#[inline]
55pub fn advance_cycle() -> u32 {
56    EVICTION_CYCLE.fetch_add(1, Ordering::Relaxed) + 1
57}
58
59#[cfg(all(test, feature = "eviction"))]
60pub(crate) fn lock_eviction_cycle_for_test() -> std::sync::MutexGuard<'static, ()> {
61    let guard = EVICTION_TEST_LOCK
62        .lock()
63        .unwrap_or_else(|poisoned| poisoned.into_inner());
64    EVICTION_CYCLE.store(0, Ordering::Relaxed);
65    guard
66}
67
68/// Canonicalized runtime label set.
69///
70/// Labels are deduplicated by key (last value wins) and sorted by key to ensure
71/// stable identity regardless of input order.
72#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
73pub struct DynamicLabelSet {
74    labels: Vec<(String, String)>,
75}
76
77impl DynamicLabelSet {
78    /// Build a canonical label set from borrowed key/value pairs.
79    #[inline]
80    pub fn from_pairs(labels: &[(&str, &str)]) -> Self {
81        Self {
82            labels: canonicalize_labels(labels),
83        }
84    }
85
86    /// Build from already-canonicalized owned pairs.
87    ///
88    /// The input must already be sorted by key and deduplicated.
89    #[doc(hidden)]
90    pub fn from_canonical_pairs(labels: &[(String, String)]) -> Self {
91        Self {
92            labels: labels.to_vec(),
93        }
94    }
95
96    /// Returns labels as ordered `(key, value)` pairs.
97    pub fn pairs(&self) -> &[(String, String)] {
98        &self.labels
99    }
100
101    #[inline]
102    pub(crate) fn shard_index(&self, shard_mask: usize) -> usize {
103        let hash = label_set_fingerprint(&self.labels);
104        ((hash ^ (hash >> 32)) as usize) & shard_mask
105    }
106}
107
108pub(crate) type DynamicIndexMap<T> = BTreeMap<DynamicLabelSet, T>;
109
110#[inline]
111pub(crate) fn dynamic_index_map<T>() -> DynamicIndexMap<T> {
112    BTreeMap::new()
113}
114
115#[inline]
116fn canonicalize_labels(labels: &[(&str, &str)]) -> Vec<(String, String)> {
117    match labels {
118        [] => Vec::new(),
119        [(key, value)] => vec![(key.to_string(), value.to_string())],
120        labels if labels.len() <= VEC_CANONICALIZATION_MAX_LABELS => {
121            canonicalize_labels_vec(labels)
122        }
123        labels => canonicalize_labels_btree(labels),
124    }
125}
126
127#[inline]
128fn canonicalize_labels_vec(labels: &[(&str, &str)]) -> Vec<(String, String)> {
129    let mut ordered: Vec<(String, String)> = Vec::with_capacity(labels.len());
130    for (k, v) in labels {
131        match ordered.binary_search_by(|(key, _)| key.as_str().cmp(k)) {
132            Ok(index) => ordered[index].1 = (*v).to_string(),
133            Err(index) => ordered.insert(index, ((*k).to_string(), (*v).to_string())),
134        }
135    }
136    ordered
137}
138
139#[inline]
140fn canonicalize_labels_btree(labels: &[(&str, &str)]) -> Vec<(String, String)> {
141    let mut map = BTreeMap::new();
142    for (k, v) in labels {
143        map.insert((*k).to_string(), (*v).to_string());
144    }
145    map.into_iter().collect()
146}
147
148#[inline]
149fn label_set_fingerprint(labels: &[(String, String)]) -> u64 {
150    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
151    const FNV_PRIME: u64 = 0x100000001b3;
152
153    let mut hash = FNV_OFFSET;
154    for (key, value) in labels {
155        for byte in key.as_bytes() {
156            hash ^= u64::from(*byte);
157            hash = hash.wrapping_mul(FNV_PRIME);
158        }
159        hash ^= 0xff;
160        hash = hash.wrapping_mul(FNV_PRIME);
161        for byte in value.as_bytes() {
162            hash ^= u64::from(*byte);
163            hash = hash.wrapping_mul(FNV_PRIME);
164        }
165        hash ^= 0xfe;
166        hash = hash.wrapping_mul(FNV_PRIME);
167    }
168    hash
169}
170
171// Shared ID generators
172pub(crate) static COUNTER_IDS: AtomicUsize = AtomicUsize::new(1);
173pub(crate) static GAUGE_IDS: AtomicUsize = AtomicUsize::new(1);
174pub(crate) static HISTOGRAM_IDS: AtomicUsize = AtomicUsize::new(1);
175pub(crate) static DISTRIBUTION_IDS: AtomicUsize = AtomicUsize::new(1);
176
177#[cfg(test)]
178mod tests {
179    use super::DynamicLabelSet;
180
181    #[test]
182    fn dynamic_label_set_is_order_independent() {
183        let left = DynamicLabelSet::from_pairs(&[("b", "2"), ("a", "1")]);
184        let right = DynamicLabelSet::from_pairs(&[("a", "1"), ("b", "2")]);
185
186        assert_eq!(left, right);
187        assert_eq!(
188            left.pairs(),
189            &[
190                ("a".to_string(), "1".to_string()),
191                ("b".to_string(), "2".to_string())
192            ]
193        );
194    }
195
196    #[test]
197    fn dynamic_label_set_duplicate_key_last_value_wins() {
198        let labels = DynamicLabelSet::from_pairs(&[("b", "1"), ("a", "1"), ("b", "2")]);
199
200        assert_eq!(
201            labels.pairs(),
202            &[
203                ("a".to_string(), "1".to_string()),
204                ("b".to_string(), "2".to_string())
205            ]
206        );
207    }
208
209    #[test]
210    fn dynamic_label_set_canonicalizes_large_label_sets() {
211        let labels = DynamicLabelSet::from_pairs(&[
212            ("m", "13"),
213            ("l", "12"),
214            ("k", "11"),
215            ("j", "10"),
216            ("i", "9"),
217            ("h", "8"),
218            ("g", "7"),
219            ("f", "6"),
220            ("e", "5"),
221            ("d", "4"),
222            ("c", "3"),
223            ("b", "2"),
224            ("a", "1"),
225        ]);
226
227        assert_eq!(
228            labels
229                .pairs()
230                .first()
231                .map(|(k, v)| (k.as_str(), v.as_str())),
232            Some(("a", "1"))
233        );
234        assert_eq!(
235            labels.pairs().last().map(|(k, v)| (k.as_str(), v.as_str())),
236            Some(("m", "13"))
237        );
238    }
239}