canic_core/model/metrics/
endpoint.rs

1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3use std::{cell::RefCell, collections::HashMap};
4
5thread_local! {
6    static ENDPOINT_ATTEMPT_METRICS: RefCell<HashMap<&'static str, EndpointAttemptCounts>> =
7        RefCell::new(HashMap::new());
8
9    static ENDPOINT_RESULT_METRICS: RefCell<HashMap<&'static str, EndpointResultCounts>> =
10        RefCell::new(HashMap::new());
11}
12
13// -----------------------------------------------------------------------------
14// Internal counter types (private)
15// -----------------------------------------------------------------------------
16
17///
18/// EndpointAttemptCounts
19/// Internal attempt/completion counters.
20///
21
22#[derive(Default)]
23struct EndpointAttemptCounts {
24    attempted: u64,
25    completed: u64,
26}
27
28///
29/// EndpointResultCounts
30/// Internal ok/err counters for Result-returning endpoints.
31///
32
33#[derive(Default)]
34struct EndpointResultCounts {
35    ok: u64,
36    err: u64,
37}
38
39// -----------------------------------------------------------------------------
40// Public metric DTOs
41// -----------------------------------------------------------------------------
42
43///
44/// EndpointAttemptMetricEntry
45/// Public metric entry for endpoint attempt/completion.
46///
47#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
48pub struct EndpointAttemptMetricEntry {
49    pub endpoint: String,
50    pub attempted: u64,
51    pub completed: u64,
52}
53
54///
55/// EndpointAttemptMetricsSnapshot
56///
57
58pub type EndpointAttemptMetricsSnapshot = Vec<EndpointAttemptMetricEntry>;
59
60///
61/// EndpointResultMetricEntry
62/// Public metric entry for endpoint ok/err outcomes.
63///
64
65#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
66pub struct EndpointResultMetricEntry {
67    pub endpoint: String,
68    pub ok: u64,
69    pub err: u64,
70}
71
72///
73/// EndpointResultMetricsSnapshot
74///
75
76pub type EndpointResultMetricsSnapshot = Vec<EndpointResultMetricEntry>;
77
78// -----------------------------------------------------------------------------
79// Metrics state + operations
80// -----------------------------------------------------------------------------
81
82///
83/// EndpointAttemptMetrics
84/// Best-effort attempt/completion counters per endpoint.
85///
86/// Intended uses:
87/// - Derive access-denial rate via attempted vs denied.
88/// - Detect suspected traps via attempted vs (denied + completed).
89///
90
91pub struct EndpointAttemptMetrics;
92
93impl EndpointAttemptMetrics {
94    pub fn increment_attempted(endpoint: &'static str) {
95        ENDPOINT_ATTEMPT_METRICS.with_borrow_mut(|counts| {
96            let entry = counts.entry(endpoint).or_default();
97            entry.attempted = entry.attempted.saturating_add(1);
98        });
99    }
100
101    pub fn increment_completed(endpoint: &'static str) {
102        ENDPOINT_ATTEMPT_METRICS.with_borrow_mut(|counts| {
103            let entry = counts.entry(endpoint).or_default();
104            entry.completed = entry.completed.saturating_add(1);
105        });
106    }
107
108    #[must_use]
109    pub fn snapshot() -> EndpointAttemptMetricsSnapshot {
110        ENDPOINT_ATTEMPT_METRICS.with_borrow(|counts| {
111            counts
112                .iter()
113                .map(|(endpoint, c)| EndpointAttemptMetricEntry {
114                    endpoint: (*endpoint).to_string(),
115                    attempted: c.attempted,
116                    completed: c.completed,
117                })
118                .collect()
119        })
120    }
121
122    #[cfg(test)]
123    pub fn reset() {
124        ENDPOINT_ATTEMPT_METRICS.with_borrow_mut(HashMap::clear);
125    }
126}
127
128///
129/// EndpointResultMetrics
130/// Best-effort ok/err counters per endpoint for Result-returning endpoints.
131///
132/// Notes:
133/// - Access-denied errors are excluded (pre-dispatch).
134///
135
136pub struct EndpointResultMetrics;
137
138impl EndpointResultMetrics {
139    pub fn increment_ok(endpoint: &'static str) {
140        ENDPOINT_RESULT_METRICS.with_borrow_mut(|counts| {
141            let entry = counts.entry(endpoint).or_default();
142            entry.ok = entry.ok.saturating_add(1);
143        });
144    }
145
146    pub fn increment_err(endpoint: &'static str) {
147        ENDPOINT_RESULT_METRICS.with_borrow_mut(|counts| {
148            let entry = counts.entry(endpoint).or_default();
149            entry.err = entry.err.saturating_add(1);
150        });
151    }
152
153    #[must_use]
154    pub fn snapshot() -> EndpointResultMetricsSnapshot {
155        ENDPOINT_RESULT_METRICS.with_borrow(|counts| {
156            counts
157                .iter()
158                .map(|(endpoint, c)| EndpointResultMetricEntry {
159                    endpoint: (*endpoint).to_string(),
160                    ok: c.ok,
161                    err: c.err,
162                })
163                .collect()
164        })
165    }
166
167    #[cfg(test)]
168    pub fn reset() {
169        ENDPOINT_RESULT_METRICS.with_borrow_mut(HashMap::clear);
170    }
171}
172
173///
174/// TESTS
175///
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use std::collections::HashMap;
181
182    #[test]
183    fn endpoint_attempt_metrics_track_attempted_and_completed() {
184        EndpointAttemptMetrics::reset();
185
186        EndpointAttemptMetrics::increment_attempted("a");
187        EndpointAttemptMetrics::increment_attempted("a");
188        EndpointAttemptMetrics::increment_attempted("b");
189        EndpointAttemptMetrics::increment_completed("a");
190
191        let snapshot = EndpointAttemptMetrics::snapshot();
192        let mut map: HashMap<String, (u64, u64)> = snapshot
193            .into_iter()
194            .map(|e| (e.endpoint, (e.attempted, e.completed)))
195            .collect();
196
197        assert_eq!(map.remove("a"), Some((2, 1)));
198        assert_eq!(map.remove("b"), Some((1, 0)));
199        assert!(map.is_empty());
200    }
201
202    #[test]
203    fn endpoint_result_metrics_track_ok_and_err() {
204        EndpointResultMetrics::reset();
205
206        EndpointResultMetrics::increment_ok("a");
207        EndpointResultMetrics::increment_ok("a");
208        EndpointResultMetrics::increment_err("a");
209        EndpointResultMetrics::increment_err("b");
210
211        let snapshot = EndpointResultMetrics::snapshot();
212        let mut map: HashMap<String, (u64, u64)> = snapshot
213            .into_iter()
214            .map(|e| (e.endpoint, (e.ok, e.err)))
215            .collect();
216
217        assert_eq!(map.remove("a"), Some((2, 1)));
218        assert_eq!(map.remove("b"), Some((0, 1)));
219        assert!(map.is_empty());
220    }
221}