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 METRIC_COUNTS: RefCell<HashMap<MetricKind, u64>> = RefCell::new(HashMap::new());
9    static ICC_METRICS: RefCell<HashMap<IccMetricKey, u64>> = RefCell::new(HashMap::new());
10}
11
12// -----------------------------------------------------------------------------
13// Types
14// -----------------------------------------------------------------------------
15
16///
17/// MetricsSnapshot
18///
19
20pub type MetricsSnapshot = Vec<MetricEntry>;
21
22///
23/// MetricKind
24/// Enumerates the resource-heavy actions we track.
25///
26#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
27pub enum MetricKind {
28    CreateCanister,
29    InstallCode,
30    ReinstallCode,
31    UpgradeCode,
32    UninstallCode,
33    DeleteCanister,
34    DepositCycles,
35    CanisterStatus,
36    CanisterCall,
37}
38
39///
40/// MetricEntry
41/// Snapshot entry pairing a metric kind with its count.
42///
43
44#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
45pub struct MetricEntry {
46    pub kind: MetricKind,
47    pub count: u64,
48}
49
50///
51/// IccMetricKey
52/// Uniquely identifies an inter-canister call by target + method.
53///
54#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
55pub struct IccMetricKey {
56    pub target: Principal,
57    pub method: String,
58}
59
60///
61/// IccMetricEntry
62/// Snapshot entry pairing a target/method with its count.
63///
64#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
65pub struct IccMetricEntry {
66    pub target: Principal,
67    pub method: String,
68    pub count: u64,
69}
70
71///
72/// IccMetricsSnapshot
73///
74pub type IccMetricsSnapshot = Vec<IccMetricEntry>;
75
76///
77/// MetricsReport
78/// Composite metrics view bundling action and ICC counters.
79///
80#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
81pub struct MetricsReport {
82    pub system: MetricsSnapshot,
83    pub icc: IccMetricsSnapshot,
84}
85
86// -----------------------------------------------------------------------------
87// State
88// -----------------------------------------------------------------------------
89
90///
91/// MetricsState
92/// Volatile counters for resource-using IC actions.
93///
94pub struct MetricsState;
95
96impl MetricsState {
97    /// Increment a counter and return the new value.
98    pub fn increment(kind: MetricKind) {
99        METRIC_COUNTS.with_borrow_mut(|counts| {
100            let entry = counts.entry(kind).or_insert(0);
101            *entry = entry.saturating_add(1);
102        });
103    }
104
105    /// Return a snapshot of all counters.
106    #[must_use]
107    pub fn snapshot() -> Vec<MetricEntry> {
108        METRIC_COUNTS.with_borrow(|counts| {
109            counts
110                .iter()
111                .map(|(kind, count)| MetricEntry {
112                    kind: *kind,
113                    count: *count,
114                })
115                .collect()
116        })
117    }
118
119    #[cfg(test)]
120    pub fn reset() {
121        METRIC_COUNTS.with_borrow_mut(HashMap::clear);
122    }
123}
124
125///
126/// SystemMetrics
127/// Thin facade over the action metrics counters.
128///
129pub struct SystemMetrics;
130
131impl SystemMetrics {
132    pub fn record(kind: MetricKind) {
133        MetricsState::increment(kind);
134    }
135
136    #[must_use]
137    pub fn snapshot() -> MetricsSnapshot {
138        MetricsState::snapshot()
139    }
140}
141
142///
143/// IccMetrics
144/// Volatile counters for inter-canister calls keyed by target + method.
145///
146pub struct IccMetrics;
147
148impl IccMetrics {
149    /// Increment the ICC counter for a target/method pair.
150    pub fn increment(target: Principal, method: &str) {
151        ICC_METRICS.with_borrow_mut(|counts| {
152            let key = IccMetricKey {
153                target,
154                method: method.to_string(),
155            };
156            let entry = counts.entry(key).or_insert(0);
157            *entry = entry.saturating_add(1);
158        });
159    }
160
161    /// Snapshot all ICC counters.
162    #[must_use]
163    pub fn snapshot() -> IccMetricsSnapshot {
164        ICC_METRICS.with_borrow(|counts| {
165            counts
166                .iter()
167                .map(|(key, count)| IccMetricEntry {
168                    target: key.target,
169                    method: key.method.clone(),
170                    count: *count,
171                })
172                .collect()
173        })
174    }
175
176    #[cfg(test)]
177    pub fn reset() {
178        ICC_METRICS.with_borrow_mut(HashMap::clear);
179    }
180}
181
182///
183/// TESTS
184///
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use std::collections::HashMap;
190
191    #[test]
192    fn increments_and_snapshots() {
193        MetricsState::reset();
194
195        MetricsState::increment(MetricKind::CreateCanister);
196        MetricsState::increment(MetricKind::CreateCanister);
197        MetricsState::increment(MetricKind::InstallCode);
198
199        let snapshot = MetricsState::snapshot();
200        let as_map: HashMap<MetricKind, u64> = snapshot
201            .into_iter()
202            .map(|entry| (entry.kind, entry.count))
203            .collect();
204
205        assert_eq!(as_map.get(&MetricKind::CreateCanister), Some(&2));
206        assert_eq!(as_map.get(&MetricKind::InstallCode), Some(&1));
207        assert!(!as_map.contains_key(&MetricKind::CanisterCall));
208    }
209
210    #[test]
211    fn icc_metrics_track_target_and_method() {
212        IccMetrics::reset();
213
214        let t1 = Principal::from_slice(&[1; 29]);
215        let t2 = Principal::from_slice(&[2; 29]);
216
217        IccMetrics::increment(t1, "foo");
218        IccMetrics::increment(t1, "foo");
219        IccMetrics::increment(t1, "bar");
220        IccMetrics::increment(t2, "foo");
221
222        let snapshot = IccMetrics::snapshot();
223        let mut map: HashMap<(Principal, String), u64> = snapshot
224            .into_iter()
225            .map(|entry| ((entry.target, entry.method), entry.count))
226            .collect();
227
228        assert_eq!(map.remove(&(t1, "foo".to_string())), Some(2));
229        assert_eq!(map.remove(&(t1, "bar".to_string())), Some(1));
230        assert_eq!(map.remove(&(t2, "foo".to_string())), Some(1));
231        assert!(map.is_empty());
232    }
233}