fast_telemetry/metric/dynamic/
mod.rs1mod 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#[cfg(feature = "eviction")]
43#[inline]
44pub fn current_cycle() -> u32 {
45 EVICTION_CYCLE.load(Ordering::Relaxed)
46}
47
48#[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#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
71pub struct DynamicLabelSet {
72 labels: Vec<(String, String)>,
73}
74
75impl DynamicLabelSet {
76 #[inline]
78 pub fn from_pairs(labels: &[(&str, &str)]) -> Self {
79 Self {
80 labels: canonicalize_labels(labels),
81 }
82 }
83
84 #[doc(hidden)]
88 pub fn from_canonical_pairs(labels: &[(String, String)]) -> Self {
89 Self {
90 labels: labels.to_vec(),
91 }
92 }
93
94 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
169pub(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}