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
12pub type SystemMetricsSnapshot = Vec<SystemMetricEntry>;
21
22#[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#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
49pub struct SystemMetricEntry {
50 pub kind: SystemMetricKind,
51 pub count: u64,
52}
53
54#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
60pub struct IccMetricKey {
61 pub target: Principal,
62 pub method: String,
63}
64
65#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
71pub struct IccMetricEntry {
72 pub target: Principal,
73 pub method: String,
74 pub count: u64,
75}
76
77#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
83pub struct HttpMetricKey {
84 pub method: String,
85 pub url: String,
86}
87
88#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
94pub struct HttpMetricEntry {
95 pub method: String,
96 pub url: String,
97 pub count: u64,
98}
99
100pub type HttpMetricsSnapshot = Vec<HttpMetricEntry>;
105
106#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
110pub enum TimerMode {
111 Interval,
112 Once,
113}
114
115#[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#[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;
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 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 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 #[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 #[cfg(test)]
386 pub fn reset() {
387 TIMER_METRICS.with_borrow_mut(HashMap::clear);
388 }
389}
390
391#[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}