canic_core/model/
metrics.rs

1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3use std::{cell::RefCell, collections::HashMap};
4
5use crate::types::Principal;
6
7thread_local! {
8    static SYSTEM_METRICS: RefCell<HashMap<SystemMetricKind, u64>> = RefCell::new(HashMap::new());
9    static ICC_METRICS: RefCell<HashMap<IccMetricKey, u64>> = RefCell::new(HashMap::new());
10    static HTTP_METRICS: RefCell<HashMap<HttpMetricKey, u64>> = RefCell::new(HashMap::new());
11    static TIMER_METRICS: RefCell<HashMap<TimerMetricKey, u64>> = RefCell::new(HashMap::new());
12}
13
14// -----------------------------------------------------------------------------
15// Types
16// -----------------------------------------------------------------------------
17
18///
19/// SystemMetricsSnapshot
20///
21
22pub type SystemMetricsSnapshot = Vec<SystemMetricEntry>;
23
24///
25/// SystemMetricKind
26/// Enumerates the resource-heavy actions we track.
27///
28
29#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
30#[remain::sorted]
31pub enum SystemMetricKind {
32    CanisterCall,
33    CanisterStatus,
34    CreateCanister,
35    DeleteCanister,
36    DepositCycles,
37    HttpOutcall,
38    InstallCode,
39    ReinstallCode,
40    TimerScheduled,
41    UninstallCode,
42    UpgradeCode,
43}
44
45///
46/// SystemMetricEntry
47/// Snapshot entry pairing a metric kind with its count.
48///
49
50#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
51pub struct SystemMetricEntry {
52    pub kind: SystemMetricKind,
53    pub count: u64,
54}
55
56///
57/// IccMetricKey
58/// Uniquely identifies an inter-canister call by target + method.
59///
60
61#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
62pub struct IccMetricKey {
63    pub target: Principal,
64    pub method: String,
65}
66
67///
68/// IccMetricEntry
69/// Snapshot entry pairing a target/method with its count.
70///
71
72#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
73pub struct IccMetricEntry {
74    pub target: Principal,
75    pub method: String,
76    pub count: u64,
77}
78
79///
80/// HttpMetricKey
81/// Uniquely identifies an HTTP outcall by method + URL.
82///
83
84#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
85pub struct HttpMetricKey {
86    pub method: String,
87    pub url: String,
88}
89
90///
91/// HttpMetricEntry
92/// Snapshot entry pairing a method/url with its count.
93///
94
95#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
96pub struct HttpMetricEntry {
97    pub method: String,
98    pub url: String,
99    pub count: u64,
100}
101
102///
103/// HttpMetricsSnapshot
104///
105
106pub type HttpMetricsSnapshot = Vec<HttpMetricEntry>;
107
108///
109/// TimerMode
110///
111#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
112pub enum TimerMode {
113    Interval,
114    Once,
115}
116
117///
118/// TimerMetricKey
119/// Uniquely identifies a timer by mode + delay (ms) + label.
120///
121#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
122pub struct TimerMetricKey {
123    pub mode: TimerMode,
124    pub delay_ms: u64,
125    pub label: String,
126}
127
128///
129/// TimerMetricEntry
130/// Snapshot entry pairing a timer mode/delay with its count.
131///
132#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
133pub struct TimerMetricEntry {
134    pub mode: TimerMode,
135    pub delay_ms: u64,
136    pub label: String,
137    pub count: u64,
138}
139
140///
141/// TimerMetricsSnapshot
142///
143
144pub type TimerMetricsSnapshot = Vec<TimerMetricEntry>;
145
146///
147/// IccMetricsSnapshot
148///
149
150pub type IccMetricsSnapshot = Vec<IccMetricEntry>;
151
152///
153/// MetricsReport
154/// Composite metrics view bundling action, ICC, HTTP, and timer counters.
155///
156
157#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
158pub struct MetricsReport {
159    pub system: SystemMetricsSnapshot,
160    pub icc: IccMetricsSnapshot,
161    pub http: HttpMetricsSnapshot,
162    pub timer: TimerMetricsSnapshot,
163}
164
165// -----------------------------------------------------------------------------
166// State
167// -----------------------------------------------------------------------------
168
169///
170/// SystemMetrics
171/// Thin facade over the action metrics counters.
172///
173
174pub struct SystemMetrics;
175
176impl SystemMetrics {
177    /// Increment a counter and return the new value.
178    pub fn increment(kind: SystemMetricKind) {
179        SYSTEM_METRICS.with_borrow_mut(|counts| {
180            let entry = counts.entry(kind).or_insert(0);
181            *entry = entry.saturating_add(1);
182        });
183    }
184
185    /// Return a snapshot of all counters.
186    #[must_use]
187    pub fn snapshot() -> Vec<SystemMetricEntry> {
188        SYSTEM_METRICS.with_borrow(|counts| {
189            counts
190                .iter()
191                .map(|(kind, count)| SystemMetricEntry {
192                    kind: *kind,
193                    count: *count,
194                })
195                .collect()
196        })
197    }
198
199    #[cfg(test)]
200    pub fn reset() {
201        SYSTEM_METRICS.with_borrow_mut(HashMap::clear);
202    }
203}
204
205///
206/// IccMetrics
207/// Volatile counters for inter-canister calls keyed by target + method.
208///
209
210pub struct IccMetrics;
211
212impl IccMetrics {
213    /// Increment the ICC counter for a target/method pair.
214    pub fn increment(target: Principal, method: &str) {
215        ICC_METRICS.with_borrow_mut(|counts| {
216            let key = IccMetricKey {
217                target,
218                method: method.to_string(),
219            };
220            let entry = counts.entry(key).or_insert(0);
221            *entry = entry.saturating_add(1);
222        });
223    }
224
225    /// Snapshot all ICC counters.
226    #[must_use]
227    pub fn snapshot() -> IccMetricsSnapshot {
228        ICC_METRICS.with_borrow(|counts| {
229            counts
230                .iter()
231                .map(|(key, count)| IccMetricEntry {
232                    target: key.target,
233                    method: key.method.clone(),
234                    count: *count,
235                })
236                .collect()
237        })
238    }
239
240    #[cfg(test)]
241    pub fn reset() {
242        ICC_METRICS.with_borrow_mut(HashMap::clear);
243    }
244}
245
246///
247/// HttpMetrics
248/// Volatile counters for HTTP outcalls keyed by method + URL.
249///
250
251pub struct HttpMetrics;
252
253impl HttpMetrics {
254    pub fn increment(method: &str, url: &str) {
255        HTTP_METRICS.with_borrow_mut(|counts| {
256            let key = HttpMetricKey {
257                method: method.to_string(),
258                url: url.to_string(),
259            };
260            let entry = counts.entry(key).or_insert(0);
261            *entry = entry.saturating_add(1);
262        });
263    }
264
265    #[must_use]
266    pub fn snapshot() -> HttpMetricsSnapshot {
267        HTTP_METRICS.with_borrow(|counts| {
268            counts
269                .iter()
270                .map(|(key, count)| HttpMetricEntry {
271                    method: key.method.clone(),
272                    url: key.url.clone(),
273                    count: *count,
274                })
275                .collect()
276        })
277    }
278
279    #[cfg(test)]
280    pub fn reset() {
281        HTTP_METRICS.with_borrow_mut(HashMap::clear);
282    }
283}
284
285///
286/// TimerMetrics
287/// Volatile counters for timers keyed by mode + delay.
288///
289
290pub struct TimerMetrics;
291
292impl TimerMetrics {
293    #[allow(clippy::cast_possible_truncation)]
294    pub fn increment(mode: TimerMode, delay: std::time::Duration, label: &str) {
295        let delay_ms = delay.as_millis().min(u128::from(u64::MAX)) as u64;
296
297        TIMER_METRICS.with_borrow_mut(|counts| {
298            let key = TimerMetricKey {
299                mode,
300                delay_ms,
301                label: label.to_string(),
302            };
303            let entry = counts.entry(key).or_insert(0);
304            *entry = entry.saturating_add(1);
305        });
306    }
307
308    #[must_use]
309    pub fn snapshot() -> TimerMetricsSnapshot {
310        TIMER_METRICS.with_borrow(|counts| {
311            counts
312                .iter()
313                .map(|(key, count)| TimerMetricEntry {
314                    mode: key.mode,
315                    delay_ms: key.delay_ms,
316                    label: key.label.clone(),
317                    count: *count,
318                })
319                .collect()
320        })
321    }
322
323    #[cfg(test)]
324    pub fn reset() {
325        TIMER_METRICS.with_borrow_mut(HashMap::clear);
326    }
327}
328
329///
330/// TESTS
331///
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use std::collections::HashMap;
337
338    #[test]
339    fn increments_and_snapshots() {
340        SystemMetrics::reset();
341
342        SystemMetrics::increment(SystemMetricKind::CreateCanister);
343        SystemMetrics::increment(SystemMetricKind::CreateCanister);
344        SystemMetrics::increment(SystemMetricKind::InstallCode);
345
346        let snapshot = SystemMetrics::snapshot();
347        let as_map: HashMap<SystemMetricKind, u64> = snapshot
348            .into_iter()
349            .map(|entry| (entry.kind, entry.count))
350            .collect();
351
352        assert_eq!(as_map.get(&SystemMetricKind::CreateCanister), Some(&2));
353        assert_eq!(as_map.get(&SystemMetricKind::InstallCode), Some(&1));
354        assert!(!as_map.contains_key(&SystemMetricKind::CanisterCall));
355    }
356
357    #[test]
358    fn icc_metrics_track_target_and_method() {
359        IccMetrics::reset();
360
361        let t1 = Principal::from_slice(&[1; 29]);
362        let t2 = Principal::from_slice(&[2; 29]);
363
364        IccMetrics::increment(t1, "foo");
365        IccMetrics::increment(t1, "foo");
366        IccMetrics::increment(t1, "bar");
367        IccMetrics::increment(t2, "foo");
368
369        let snapshot = IccMetrics::snapshot();
370        let mut map: HashMap<(Principal, String), u64> = snapshot
371            .into_iter()
372            .map(|entry| ((entry.target, entry.method), entry.count))
373            .collect();
374
375        assert_eq!(map.remove(&(t1, "foo".to_string())), Some(2));
376        assert_eq!(map.remove(&(t1, "bar".to_string())), Some(1));
377        assert_eq!(map.remove(&(t2, "foo".to_string())), Some(1));
378        assert!(map.is_empty());
379    }
380
381    #[test]
382    fn http_metrics_track_method_and_url() {
383        HttpMetrics::reset();
384
385        HttpMetrics::increment("GET", "https://example.com/a");
386        HttpMetrics::increment("GET", "https://example.com/a");
387        HttpMetrics::increment("POST", "https://example.com/a");
388        HttpMetrics::increment("GET", "https://example.com/b");
389
390        let snapshot = HttpMetrics::snapshot();
391        let mut map: HashMap<(String, String), u64> = snapshot
392            .into_iter()
393            .map(|entry| ((entry.method, entry.url), entry.count))
394            .collect();
395
396        assert_eq!(
397            map.remove(&("GET".to_string(), "https://example.com/a".to_string())),
398            Some(2)
399        );
400        assert_eq!(
401            map.remove(&("POST".to_string(), "https://example.com/a".to_string())),
402            Some(1)
403        );
404        assert_eq!(
405            map.remove(&("GET".to_string(), "https://example.com/b".to_string())),
406            Some(1)
407        );
408        assert!(map.is_empty());
409    }
410
411    #[test]
412    fn timer_metrics_track_mode_delay_and_label() {
413        TimerMetrics::reset();
414
415        TimerMetrics::increment(TimerMode::Once, std::time::Duration::from_secs(1), "once:a");
416        TimerMetrics::increment(TimerMode::Once, std::time::Duration::from_secs(1), "once:a");
417        TimerMetrics::increment(
418            TimerMode::Interval,
419            std::time::Duration::from_millis(500),
420            "interval:b",
421        );
422
423        let snapshot = TimerMetrics::snapshot();
424        let mut map: HashMap<(TimerMode, u64, String), u64> = snapshot
425            .into_iter()
426            .map(|entry| ((entry.mode, entry.delay_ms, entry.label), entry.count))
427            .collect();
428
429        assert_eq!(
430            map.remove(&(TimerMode::Once, 1_000, "once:a".to_string())),
431            Some(2)
432        );
433        assert_eq!(
434            map.remove(&(TimerMode::Interval, 500, "interval:b".to_string())),
435            Some(1)
436        );
437        assert!(map.is_empty());
438    }
439}