canic_core/model/
metrics.rs

1use candid::{CandidType, Principal};
2use serde::{Deserialize, Serialize};
3use std::{cell::RefCell, collections::HashMap, time::Duration};
4
5thread_local! {
6    static SYSTEM_METRICS: RefCell<HashMap<SystemMetricKind, u64>> = RefCell::new(HashMap::new());
7    static ICC_METRICS: RefCell<HashMap<IccMetricKey, u64>> = RefCell::new(HashMap::new());
8    static HTTP_METRICS: RefCell<HashMap<HttpMetricKey, u64>> = RefCell::new(HashMap::new());
9    static TIMER_METRICS: RefCell<HashMap<TimerMetricKey, u64>> = RefCell::new(HashMap::new());
10}
11
12// -----------------------------------------------------------------------------
13// Types
14// -----------------------------------------------------------------------------
15
16///
17/// SystemMetricsSnapshot
18///
19
20pub type SystemMetricsSnapshot = Vec<SystemMetricEntry>;
21
22///
23/// SystemMetricKind
24/// Enumerates the resource-heavy actions we track.
25///
26
27#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
28#[remain::sorted]
29pub enum SystemMetricKind {
30    CanisterCall,
31    CanisterStatus,
32    CreateCanister,
33    DeleteCanister,
34    DepositCycles,
35    HttpOutcall,
36    InstallCode,
37    ReinstallCode,
38    TimerScheduled,
39    UninstallCode,
40    UpgradeCode,
41}
42
43///
44/// SystemMetricEntry
45/// Snapshot entry pairing a metric kind with its count.
46///
47
48#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
49pub struct SystemMetricEntry {
50    pub kind: SystemMetricKind,
51    pub count: u64,
52}
53
54///
55/// IccMetricKey
56/// Uniquely identifies an inter-canister call by target + method.
57///
58
59#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
60pub struct IccMetricKey {
61    pub target: Principal,
62    pub method: String,
63}
64
65///
66/// IccMetricEntry
67/// Snapshot entry pairing a target/method with its count.
68///
69
70#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
71pub struct IccMetricEntry {
72    pub target: Principal,
73    pub method: String,
74    pub count: u64,
75}
76
77///
78/// HttpMetricKey
79/// Uniquely identifies an HTTP outcall by method + URL.
80///
81
82#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
83pub struct HttpMetricKey {
84    pub method: String,
85    pub url: String,
86}
87
88///
89/// HttpMetricEntry
90/// Snapshot entry pairing a method/url with its count.
91///
92
93#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
94pub struct HttpMetricEntry {
95    pub method: String,
96    pub url: String,
97    pub count: u64,
98}
99
100///
101/// HttpMetricsSnapshot
102///
103
104pub type HttpMetricsSnapshot = Vec<HttpMetricEntry>;
105
106///
107/// TimerMode
108///
109#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
110pub enum TimerMode {
111    Interval,
112    Once,
113}
114
115///
116/// TimerMetricKey
117/// Uniquely identifies a timer by mode + delay (ms) + label.
118///
119
120#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
121pub struct TimerMetricKey {
122    pub mode: TimerMode,
123    pub delay_ms: u64,
124    pub label: String,
125}
126
127///
128/// TimerMetricEntry
129/// Snapshot entry pairing a timer mode/delay with its count.
130///
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///
288/// Volatile counters for timers keyed by `(mode, delay_ms, label)`.
289///
290/// ## What this measures
291///
292/// `TimerMetrics` is intended to answer two related questions:
293///
294/// 1) **Which timers have been scheduled?**
295///    - Use [`ensure`] at scheduling time to guarantee the timer appears in snapshots,
296///      even if it has not fired yet (e.g., newly-created interval timers).
297///
298/// 2) **How many scheduling events have occurred for a given timer key?**
299///    - Use [`increment`] when you explicitly want to count scheduling operations.
300///
301/// Note that this type does **not** count executions/ticks of interval timers.
302/// Execution counts should be tracked separately (e.g., via perf records or a
303/// dedicated “timer runs” metric), because scheduling and execution are different signals.
304///
305/// ## Cardinality and labels
306///
307/// Labels are used as metric keys. Keep labels stable and low-cardinality (avoid
308/// embedding principals, IDs, or other high-variance values).
309///
310/// ## Thread safety / runtime model
311///
312/// This uses `thread_local!` storage. On the IC, this is the standard way to maintain
313/// mutable global state without `unsafe`.
314///
315
316pub struct TimerMetrics;
317
318impl TimerMetrics {
319    #[allow(clippy::cast_possible_truncation)]
320    fn delay_ms(delay: Duration) -> u64 {
321        delay.as_millis().min(u128::from(u64::MAX)) as u64
322    }
323
324    /// Ensure a timer key exists in the metrics table with an initial count of `0`.
325    ///
326    /// This is used at **schedule time** to make timers visible in snapshots before they
327    /// have fired (particularly important for interval timers).
328    ///
329    /// Idempotent: calling `ensure` repeatedly for the same key does not change the count.
330    pub fn ensure(mode: TimerMode, delay: Duration, label: &str) {
331        let delay_ms = Self::delay_ms(delay);
332
333        TIMER_METRICS.with_borrow_mut(|counts| {
334            let key = TimerMetricKey {
335                mode,
336                delay_ms,
337                label: label.to_string(),
338            };
339
340            counts.entry(key).or_insert(0);
341        });
342    }
343
344    /// Increment the scheduling counter for a timer key.
345    ///
346    /// Use this when you want to count how many times a given timer (identified by
347    /// `(mode, delay_ms, label)`) has been scheduled.
348    ///
349    /// This uses saturating arithmetic to avoid overflow.
350    pub fn increment(mode: TimerMode, delay: Duration, label: &str) {
351        let delay_ms = Self::delay_ms(delay);
352
353        TIMER_METRICS.with_borrow_mut(|counts| {
354            let key = TimerMetricKey {
355                mode,
356                delay_ms,
357                label: label.to_string(),
358            };
359
360            let entry = counts.entry(key).or_insert(0);
361            *entry = entry.saturating_add(1);
362        });
363    }
364
365    /// Snapshot all timer scheduling metrics.
366    ///
367    /// Returns the current contents of the metrics table as a vector of entries.
368    /// Callers may sort or page the results as needed at the API layer.
369    #[must_use]
370    pub fn snapshot() -> TimerMetricsSnapshot {
371        TIMER_METRICS.with_borrow(|counts| {
372            counts
373                .iter()
374                .map(|(key, count)| TimerMetricEntry {
375                    mode: key.mode,
376                    delay_ms: key.delay_ms,
377                    label: key.label.clone(),
378                    count: *count,
379                })
380                .collect()
381        })
382    }
383
384    /// Test-only helper: clear all timer metrics.
385    #[cfg(test)]
386    pub fn reset() {
387        TIMER_METRICS.with_borrow_mut(HashMap::clear);
388    }
389}
390
391///
392/// TESTS
393///
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use std::collections::HashMap;
399
400    #[test]
401    fn increments_and_snapshots() {
402        SystemMetrics::reset();
403
404        SystemMetrics::increment(SystemMetricKind::CreateCanister);
405        SystemMetrics::increment(SystemMetricKind::CreateCanister);
406        SystemMetrics::increment(SystemMetricKind::InstallCode);
407
408        let snapshot = SystemMetrics::snapshot();
409        let as_map: HashMap<SystemMetricKind, u64> = snapshot
410            .into_iter()
411            .map(|entry| (entry.kind, entry.count))
412            .collect();
413
414        assert_eq!(as_map.get(&SystemMetricKind::CreateCanister), Some(&2));
415        assert_eq!(as_map.get(&SystemMetricKind::InstallCode), Some(&1));
416        assert!(!as_map.contains_key(&SystemMetricKind::CanisterCall));
417    }
418
419    #[test]
420    fn icc_metrics_track_target_and_method() {
421        IccMetrics::reset();
422
423        let t1 = Principal::from_slice(&[1; 29]);
424        let t2 = Principal::from_slice(&[2; 29]);
425
426        IccMetrics::increment(t1, "foo");
427        IccMetrics::increment(t1, "foo");
428        IccMetrics::increment(t1, "bar");
429        IccMetrics::increment(t2, "foo");
430
431        let snapshot = IccMetrics::snapshot();
432        let mut map: HashMap<(Principal, String), u64> = snapshot
433            .into_iter()
434            .map(|entry| ((entry.target, entry.method), entry.count))
435            .collect();
436
437        assert_eq!(map.remove(&(t1, "foo".to_string())), Some(2));
438        assert_eq!(map.remove(&(t1, "bar".to_string())), Some(1));
439        assert_eq!(map.remove(&(t2, "foo".to_string())), Some(1));
440        assert!(map.is_empty());
441    }
442
443    #[test]
444    fn http_metrics_track_method_and_url() {
445        HttpMetrics::reset();
446
447        HttpMetrics::increment("GET", "https://example.com/a");
448        HttpMetrics::increment("GET", "https://example.com/a");
449        HttpMetrics::increment("POST", "https://example.com/a");
450        HttpMetrics::increment("GET", "https://example.com/b");
451
452        let snapshot = HttpMetrics::snapshot();
453        let mut map: HashMap<(String, String), u64> = snapshot
454            .into_iter()
455            .map(|entry| ((entry.method, entry.url), entry.count))
456            .collect();
457
458        assert_eq!(
459            map.remove(&("GET".to_string(), "https://example.com/a".to_string())),
460            Some(2)
461        );
462        assert_eq!(
463            map.remove(&("POST".to_string(), "https://example.com/a".to_string())),
464            Some(1)
465        );
466        assert_eq!(
467            map.remove(&("GET".to_string(), "https://example.com/b".to_string())),
468            Some(1)
469        );
470        assert!(map.is_empty());
471    }
472
473    #[test]
474    fn timer_metrics_track_mode_delay_and_label() {
475        TimerMetrics::reset();
476
477        TimerMetrics::increment(TimerMode::Once, Duration::from_secs(1), "once:a");
478        TimerMetrics::increment(TimerMode::Once, Duration::from_secs(1), "once:a");
479        TimerMetrics::increment(
480            TimerMode::Interval,
481            Duration::from_millis(500),
482            "interval:b",
483        );
484
485        let snapshot = TimerMetrics::snapshot();
486        let mut map: HashMap<(TimerMode, u64, String), u64> = snapshot
487            .into_iter()
488            .map(|entry| ((entry.mode, entry.delay_ms, entry.label), entry.count))
489            .collect();
490
491        assert_eq!(
492            map.remove(&(TimerMode::Once, 1_000, "once:a".to_string())),
493            Some(2)
494        );
495        assert_eq!(
496            map.remove(&(TimerMode::Interval, 500, "interval:b".to_string())),
497            Some(1)
498        );
499        assert!(map.is_empty());
500    }
501}