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