canic_core/model/metrics/
access.rs

1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3use std::{cell::RefCell, collections::HashMap};
4
5thread_local! {
6    static ACCESS_METRICS: RefCell<HashMap<AccessMetricKey, u64>> = RefCell::new(HashMap::new());
7}
8
9///
10/// AccessMetricKind
11/// Enumerates the access-control stage that rejected the call.
12///
13
14#[derive(
15    CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
16)]
17#[remain::sorted]
18pub enum AccessMetricKind {
19    Auth,
20    Guard,
21    Policy,
22}
23
24///
25/// AccessMetricKey
26/// Uniquely identifies a rejected access attempt by endpoint + stage.
27///
28
29#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
30pub struct AccessMetricKey {
31    pub endpoint: String,
32    pub kind: AccessMetricKind,
33}
34
35///
36/// AccessMetricEntry
37/// Snapshot entry pairing an endpoint/stage with its count.
38///
39
40#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
41pub struct AccessMetricEntry {
42    pub endpoint: String,
43    pub kind: AccessMetricKind,
44    pub count: u64,
45}
46
47///
48/// AccessMetricsSnapshot
49///
50
51pub type AccessMetricsSnapshot = Vec<AccessMetricEntry>;
52
53///
54/// AccessMetrics
55/// Volatile counters for unsuccessful access attempts by endpoint + stage.
56///
57
58pub struct AccessMetrics;
59
60impl AccessMetrics {
61    /// Increment the access-rejection counter for an endpoint/stage pair.
62    pub fn increment(endpoint: &str, kind: AccessMetricKind) {
63        ACCESS_METRICS.with_borrow_mut(|counts| {
64            let key = AccessMetricKey {
65                endpoint: endpoint.to_string(),
66                kind,
67            };
68
69            let entry = counts.entry(key).or_insert(0);
70            *entry = entry.saturating_add(1);
71        });
72    }
73
74    /// Snapshot all access metrics.
75    #[must_use]
76    pub fn snapshot() -> AccessMetricsSnapshot {
77        ACCESS_METRICS.with_borrow(|counts| {
78            counts
79                .iter()
80                .map(|(key, count)| AccessMetricEntry {
81                    endpoint: key.endpoint.clone(),
82                    kind: key.kind,
83                    count: *count,
84                })
85                .collect()
86        })
87    }
88
89    #[cfg(test)]
90    pub fn reset() {
91        ACCESS_METRICS.with_borrow_mut(HashMap::clear);
92    }
93}
94
95///
96/// TESTS
97///
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use std::collections::HashMap;
103
104    #[test]
105    fn access_metrics_track_endpoint_and_stage() {
106        AccessMetrics::reset();
107
108        AccessMetrics::increment("foo", AccessMetricKind::Guard);
109        AccessMetrics::increment("foo", AccessMetricKind::Guard);
110        AccessMetrics::increment("foo", AccessMetricKind::Auth);
111        AccessMetrics::increment("bar", AccessMetricKind::Policy);
112
113        let snapshot = AccessMetrics::snapshot();
114        let mut map: HashMap<(String, AccessMetricKind), u64> = snapshot
115            .into_iter()
116            .map(|entry| ((entry.endpoint, entry.kind), entry.count))
117            .collect();
118
119        assert_eq!(
120            map.remove(&("foo".to_string(), AccessMetricKind::Guard)),
121            Some(2)
122        );
123        assert_eq!(
124            map.remove(&("foo".to_string(), AccessMetricKind::Auth)),
125            Some(1)
126        );
127        assert_eq!(
128            map.remove(&("bar".to_string(), AccessMetricKind::Policy)),
129            Some(1)
130        );
131        assert!(map.is_empty());
132    }
133}