fast_telemetry/metric/dynamic/
gauge.rs1use super::cache::{CacheableSeries, LabelCache, SERIES_CACHE_SIZE};
4#[cfg(feature = "eviction")]
5use super::current_cycle;
6use super::{DynamicIndexMap, DynamicLabelSet, GAUGE_IDS, dynamic_index_map};
7use crossbeam_utils::CachePadded;
8use parking_lot::RwLock;
9use std::cell::RefCell;
10#[cfg(feature = "eviction")]
11use std::sync::atomic::AtomicU32;
12use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
13use std::sync::{Arc, Weak};
14
15const DEFAULT_MAX_SERIES: usize = 2000;
16const OVERFLOW_LABEL_KEY: &str = "__ft_overflow";
17const OVERFLOW_LABEL_VALUE: &str = "true";
18
19type GaugeIndexShard = CachePadded<RwLock<DynamicIndexMap<Arc<GaugeSeries>>>>;
20
21struct GaugeSeries {
22 bits: CachePadded<AtomicU64>,
23 evicted: AtomicBool,
25 #[cfg(feature = "eviction")]
27 last_accessed_cycle: AtomicU32,
28}
29
30impl GaugeSeries {
31 #[cfg(feature = "eviction")]
32 fn new(cycle: u32) -> Self {
33 Self {
34 bits: CachePadded::new(AtomicU64::new(0.0_f64.to_bits())),
35 evicted: AtomicBool::new(false),
36 last_accessed_cycle: AtomicU32::new(cycle),
37 }
38 }
39
40 #[cfg(not(feature = "eviction"))]
41 fn new() -> Self {
42 Self {
43 bits: CachePadded::new(AtomicU64::new(0.0_f64.to_bits())),
44 evicted: AtomicBool::new(false),
45 }
46 }
47
48 #[inline]
49 fn set(&self, value: f64) {
50 self.bits.store(value.to_bits(), Ordering::Relaxed);
51 }
54
55 #[cfg(feature = "eviction")]
57 #[inline]
58 fn touch(&self, cycle: u32) {
59 self.last_accessed_cycle.store(cycle, Ordering::Relaxed);
60 }
61
62 #[inline]
63 fn get(&self) -> f64 {
64 f64::from_bits(self.bits.load(Ordering::Relaxed))
65 }
66
67 #[inline]
68 fn is_evicted(&self) -> bool {
69 self.evicted.load(Ordering::Relaxed)
70 }
71
72 #[cfg(feature = "eviction")]
73 fn mark_evicted(&self) {
74 self.evicted.store(true, Ordering::Relaxed);
75 }
76}
77
78impl CacheableSeries for GaugeSeries {
79 fn is_evicted(&self) -> bool {
80 self.is_evicted()
81 }
82}
83
84#[derive(Clone)]
90pub struct DynamicGaugeSeries {
91 series: Arc<GaugeSeries>,
92}
93
94impl DynamicGaugeSeries {
95 #[inline]
97 pub fn set(&self, value: f64) {
98 self.series.set(value);
99 }
100
101 #[inline]
103 pub fn get(&self) -> f64 {
104 self.series.get()
105 }
106
107 #[inline]
109 pub fn is_evicted(&self) -> bool {
110 self.series.is_evicted()
111 }
112}
113
114thread_local! {
115 static SERIES_CACHE: RefCell<LabelCache<Weak<GaugeSeries>, SERIES_CACHE_SIZE>> =
116 RefCell::new(LabelCache::new());
117}
118
119pub struct DynamicGauge {
123 id: usize,
124 max_series: usize,
125 shard_mask: usize,
126 index_shards: Vec<GaugeIndexShard>,
127 series_count: AtomicUsize,
129 overflow_count: AtomicU64,
131}
132
133impl DynamicGauge {
134 pub fn new(shard_count: usize) -> Self {
136 Self::with_max_series(shard_count, DEFAULT_MAX_SERIES)
137 }
138
139 pub fn with_max_series(shard_count: usize, max_series: usize) -> Self {
147 let shard_count = shard_count.next_power_of_two();
148 let id = GAUGE_IDS.fetch_add(1, Ordering::Relaxed);
149 Self {
150 id,
151 max_series,
152 shard_mask: shard_count - 1,
153 index_shards: (0..shard_count)
154 .map(|_| CachePadded::new(RwLock::new(dynamic_index_map())))
155 .collect(),
156 series_count: AtomicUsize::new(0),
157 overflow_count: AtomicU64::new(0),
158 }
159 }
160
161 pub fn series(&self, labels: &[(&str, &str)]) -> DynamicGaugeSeries {
165 if let Some(series) = self.cached_series(labels) {
166 return DynamicGaugeSeries { series };
167 }
168 let series = self.lookup_or_create(labels);
169 self.update_cache(labels, &series);
170 DynamicGaugeSeries { series }
171 }
172
173 #[inline]
175 pub fn set(&self, labels: &[(&str, &str)], value: f64) {
176 if let Some(series) = self.cached_series(labels) {
177 series.set(value);
178 return;
179 }
180
181 let series = self.lookup_or_create(labels);
182 self.update_cache(labels, &series);
183 series.set(value);
184 }
185
186 pub fn get(&self, labels: &[(&str, &str)]) -> f64 {
188 let key = DynamicLabelSet::from_pairs(labels);
189 let index_shard = self.index_shard_for(&key);
190 self.index_shards[index_shard]
191 .read()
192 .get(&key)
193 .map(|series| series.get())
194 .unwrap_or(0.0)
195 }
196
197 pub fn snapshot(&self) -> Vec<(DynamicLabelSet, f64)> {
199 let mut out = Vec::new();
200 for shard in &self.index_shards {
201 let guard = shard.read();
202 for (labels, series) in guard.iter() {
203 out.push((labels.clone(), series.get()));
204 }
205 }
206 out
207 }
208
209 pub fn cardinality(&self) -> usize {
211 self.index_shards
212 .iter()
213 .map(|shard| shard.read().len())
214 .sum()
215 }
216
217 pub fn overflow_count(&self) -> u64 {
222 self.overflow_count.load(Ordering::Relaxed)
223 }
224
225 #[doc(hidden)]
232 pub fn visit_series(&self, mut f: impl FnMut(&[(String, String)], f64)) {
233 for shard in &self.index_shards {
234 let guard = shard.read();
235 for (labels, series) in guard.iter() {
236 f(labels.pairs(), series.get());
237 }
238 }
239 }
240
241 #[cfg(feature = "eviction")]
252 pub fn evict_stale(&self, max_staleness: u32) -> usize {
253 let cycle = current_cycle();
254 let mut removed = 0;
255
256 for shard in &self.index_shards {
257 let mut guard = shard.write();
258 guard.retain(|_labels, series| {
259 if Arc::strong_count(series) > 1 {
262 return true;
263 }
264 let last = series.last_accessed_cycle.load(Ordering::Relaxed);
266 let stale = cycle.saturating_sub(last) > max_staleness;
267 if stale {
268 series.mark_evicted();
269 removed += 1;
270 self.series_count.fetch_sub(1, Ordering::Relaxed);
271 }
272 !stale
273 });
274 }
275
276 removed
277 }
278
279 fn lookup_or_create(&self, labels: &[(&str, &str)]) -> Arc<GaugeSeries> {
280 let requested_key = DynamicLabelSet::from_pairs(labels);
281 let requested_shard = self.index_shard_for(&requested_key);
282 #[cfg(feature = "eviction")]
283 let cycle = current_cycle();
284
285 if let Some(series) = self.index_shards[requested_shard]
287 .read()
288 .get(&requested_key)
289 {
290 #[cfg(feature = "eviction")]
291 series.touch(cycle);
292 return Arc::clone(series);
293 }
294
295 let key = if self.max_series > 0
297 && self.series_count.load(Ordering::Relaxed) >= self.max_series
298 {
299 self.overflow_count.fetch_add(1, Ordering::Relaxed);
300 DynamicLabelSet::from_pairs(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)])
301 } else {
302 requested_key
303 };
304 let shard = self.index_shard_for(&key);
305
306 if let Some(series) = self.index_shards[shard].read().get(&key) {
307 #[cfg(feature = "eviction")]
308 series.touch(cycle);
309 return Arc::clone(series);
310 }
311
312 let mut guard = self.index_shards[shard].write();
313 if let Some(series) = guard.get(&key) {
314 #[cfg(feature = "eviction")]
315 series.touch(cycle);
316 return Arc::clone(series);
317 }
318 #[cfg(feature = "eviction")]
319 let series = Arc::new(GaugeSeries::new(cycle));
320 #[cfg(not(feature = "eviction"))]
321 let series = Arc::new(GaugeSeries::new());
322 guard.insert(key, Arc::clone(&series));
323 self.series_count.fetch_add(1, Ordering::Relaxed);
324 series
325 }
326
327 fn index_shard_for(&self, key: &DynamicLabelSet) -> usize {
328 key.shard_index(self.shard_mask)
329 }
330
331 fn cached_series(&self, labels: &[(&str, &str)]) -> Option<Arc<GaugeSeries>> {
332 SERIES_CACHE.with(|cache| {
333 let series = cache.borrow_mut().get(self.id, labels)?;
334 #[cfg(feature = "eviction")]
335 series.touch(current_cycle());
336 Some(series)
337 })
338 }
339
340 fn update_cache(&self, labels: &[(&str, &str)], series: &Arc<GaugeSeries>) {
341 SERIES_CACHE.with(|cache| {
342 cache
343 .borrow_mut()
344 .insert(self.id, labels, Arc::downgrade(series));
345 });
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_basic_operations() {
355 let gauge = DynamicGauge::new(4);
356 gauge.set(&[("org_id", "42"), ("endpoint_uuid", "abc")], 100.5);
357
358 assert!(
359 (gauge.get(&[("org_id", "42"), ("endpoint_uuid", "abc")]) - 100.5).abs() < f64::EPSILON
360 );
361 }
362
363 #[test]
364 fn test_label_order_is_canonicalized() {
365 let gauge = DynamicGauge::new(4);
366 gauge.set(&[("org_id", "42"), ("endpoint_uuid", "abc")], 50.0);
367
368 assert!(
369 (gauge.get(&[("endpoint_uuid", "abc"), ("org_id", "42")]) - 50.0).abs() < f64::EPSILON
370 );
371 }
372
373 #[test]
374 fn test_series_handle() {
375 let gauge = DynamicGauge::new(4);
376 let series = gauge.series(&[("org_id", "42"), ("endpoint_uuid", "abc")]);
377 series.set(123.456);
378
379 assert!((series.get() - 123.456).abs() < f64::EPSILON);
380 assert!(
381 (gauge.get(&[("org_id", "42"), ("endpoint_uuid", "abc")]) - 123.456).abs()
382 < f64::EPSILON
383 );
384 }
385
386 #[test]
387 fn test_snapshot() {
388 let gauge = DynamicGauge::new(4);
389 gauge.set(&[("org_id", "1")], 10.0);
390 gauge.set(&[("org_id", "2")], 20.0);
391
392 let snap = gauge.snapshot();
393 assert_eq!(snap.len(), 2);
394
395 let total: f64 = snap.iter().map(|(_, v)| v).sum();
396 assert!((total - 30.0).abs() < f64::EPSILON);
397 }
398
399 #[test]
400 fn test_overflow_bucket_routes_new_series_at_capacity() {
401 let gauge = DynamicGauge::with_max_series(4, 1);
402 gauge.set(&[("org_id", "1")], 1.0);
403 gauge.set(&[("org_id", "2")], 2.0);
404
405 assert_eq!(gauge.cardinality(), 2);
406 assert!(
407 (gauge.get(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)]) - 2.0).abs() < f64::EPSILON
408 );
409 }
410}