Skip to main content

provide_telemetry/
cardinality.rs

1// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-Comment: Part of provide-telemetry.
4//
5
6use std::collections::{BTreeMap, HashMap};
7use std::sync::{Mutex, OnceLock};
8
9pub const OVERFLOW_VALUE: &str = "__overflow__";
10
11#[derive(Clone, Debug, PartialEq)]
12pub struct CardinalityLimit {
13    pub max_values: usize,
14    pub ttl_seconds: f64,
15}
16
17static LIMITS: OnceLock<Mutex<BTreeMap<String, CardinalityLimit>>> = OnceLock::new();
18
19fn limits() -> &'static Mutex<BTreeMap<String, CardinalityLimit>> {
20    LIMITS.get_or_init(|| Mutex::new(BTreeMap::new()))
21}
22
23pub fn register_cardinality_limit(key: impl Into<String>, limit: CardinalityLimit) {
24    limits().lock().expect("cardinality lock poisoned").insert(
25        key.into(),
26        CardinalityLimit {
27            max_values: limit.max_values.max(1),
28            ttl_seconds: limit.ttl_seconds.max(1.0),
29        },
30    );
31}
32
33pub fn get_cardinality_limits() -> BTreeMap<String, CardinalityLimit> {
34    limits().lock().expect("cardinality lock poisoned").clone()
35}
36
37pub fn clear_cardinality_limits() {
38    limits().lock().expect("cardinality lock poisoned").clear();
39}
40
41/// Enforce cardinality limits on an attribute map.
42/// Values that exceed the registered per-key limit are replaced with `OVERFLOW_VALUE`.
43pub fn guard_attributes(attributes: HashMap<String, String>) -> HashMap<String, String> {
44    let snapshot = limits().lock().expect("cardinality lock poisoned").clone();
45    if snapshot.is_empty() {
46        return attributes;
47    }
48    let mut seen_counts: HashMap<String, usize> = HashMap::new();
49    let mut out = HashMap::new();
50    for (key, value) in attributes {
51        if let Some(limit) = snapshot.get(&key) {
52            let count = seen_counts.entry(key.clone()).or_insert(0);
53            if *count < limit.max_values {
54                *count += 1;
55                out.insert(key, value);
56            } else {
57                out.insert(key, OVERFLOW_VALUE.to_string());
58            }
59        } else {
60            out.insert(key, value);
61        }
62    }
63    out
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::testing::acquire_test_state_lock;
70
71    #[test]
72    fn cardinality_test_clear_limits_removes_registered_entries() {
73        let _guard = acquire_test_state_lock();
74        clear_cardinality_limits();
75        register_cardinality_limit(
76            "user.id",
77            CardinalityLimit {
78                max_values: 5,
79                ttl_seconds: 60.0,
80            },
81        );
82        assert!(get_cardinality_limits().contains_key("user.id"));
83
84        clear_cardinality_limits();
85
86        assert!(get_cardinality_limits().is_empty());
87    }
88}