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
14pub type SystemMetricsSnapshot = Vec<SystemMetricEntry>;
23
24#[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#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
51pub struct SystemMetricEntry {
52 pub kind: SystemMetricKind,
53 pub count: u64,
54}
55
56#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
62pub struct IccMetricKey {
63 pub target: Principal,
64 pub method: String,
65}
66
67#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
73pub struct IccMetricEntry {
74 pub target: Principal,
75 pub method: String,
76 pub count: u64,
77}
78
79#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
85pub struct HttpMetricKey {
86 pub method: String,
87 pub url: String,
88}
89
90#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
96pub struct HttpMetricEntry {
97 pub method: String,
98 pub url: String,
99 pub count: u64,
100}
101
102pub type HttpMetricsSnapshot = Vec<HttpMetricEntry>;
107
108#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
112pub enum TimerMode {
113 Interval,
114 Once,
115}
116
117#[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#[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
140pub type TimerMetricsSnapshot = Vec<TimerMetricEntry>;
145
146pub type IccMetricsSnapshot = Vec<IccMetricEntry>;
151
152#[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
165pub struct SystemMetrics;
175
176impl SystemMetrics {
177 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 #[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
205pub struct IccMetrics;
211
212impl IccMetrics {
213 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 #[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
246pub 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
285pub 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#[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}