canic_core/model/metrics/
http.rs1pub use crate::cdk::mgmt::{HttpHeader, HttpMethod, HttpRequestArgs, HttpRequestResult};
2use candid::CandidType;
3use serde::{Deserialize, Serialize};
4use std::{cell::RefCell, collections::HashMap};
5
6thread_local! {
7 static HTTP_METRICS: RefCell<HashMap<HttpMetricKey, u64>> = RefCell::new(HashMap::new());
8}
9
10#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
16pub struct HttpMetricKey {
17 pub method: HttpMethod,
18 pub url: String,
19}
20
21#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
27pub struct HttpMetricEntry {
28 pub method: HttpMethod,
29 pub url: String,
30 pub count: u64,
31}
32
33pub type HttpMetricsSnapshot = Vec<HttpMetricEntry>;
38
39pub struct HttpMetrics;
46
47impl HttpMetrics {
48 pub fn increment(method: HttpMethod, url: &str) {
49 Self::increment_with_label(method, url, None);
50 }
51
52 pub fn increment_with_label(method: HttpMethod, url: &str, label: Option<&str>) {
53 let label = Self::label_for(url, label);
54
55 HTTP_METRICS.with_borrow_mut(|counts| {
56 let key = HttpMetricKey { method, url: label };
57 let entry = counts.entry(key).or_insert(0);
58 *entry = entry.saturating_add(1);
59 });
60 }
61
62 #[must_use]
63 pub fn snapshot() -> HttpMetricsSnapshot {
64 HTTP_METRICS.with_borrow(|counts| {
65 counts
66 .iter()
67 .map(|(key, count)| HttpMetricEntry {
68 method: key.method,
69 url: key.url.clone(),
70 count: *count,
71 })
72 .collect()
73 })
74 }
75
76 fn label_for(url: &str, label: Option<&str>) -> String {
77 if let Some(label) = label {
78 return label.to_string();
79 }
80
81 Self::normalize(url)
82 }
83
84 fn normalize(url: &str) -> String {
85 let without_fragment = url.split('#').next().unwrap_or(url);
86 let without_query = without_fragment
87 .split('?')
88 .next()
89 .unwrap_or(without_fragment);
90
91 let candidate = without_query.trim();
92 if candidate.is_empty() {
93 url.to_string()
94 } else {
95 candidate.to_string()
96 }
97 }
98
99 #[cfg(test)]
100 pub fn reset() {
101 HTTP_METRICS.with_borrow_mut(HashMap::clear);
102 }
103}
104
105#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn http_metrics_track_method_and_url_normalized() {
115 HttpMetrics::reset();
116
117 HttpMetrics::increment(HttpMethod::GET, "https://example.com/a?query=1#frag");
118 HttpMetrics::increment(HttpMethod::GET, "https://example.com/a?query=2");
119 HttpMetrics::increment(HttpMethod::POST, "https://example.com/a?query=3");
120 HttpMetrics::increment(HttpMethod::GET, "https://example.com/b#x");
121
122 let snapshot = HttpMetrics::snapshot();
123 let mut map: HashMap<(HttpMethod, String), u64> = snapshot
124 .into_iter()
125 .map(|entry| ((entry.method, entry.url), entry.count))
126 .collect();
127
128 assert_eq!(
129 map.remove(&(HttpMethod::GET, "https://example.com/a".to_string())),
130 Some(2)
131 );
132 assert_eq!(
133 map.remove(&(HttpMethod::POST, "https://example.com/a".to_string())),
134 Some(1)
135 );
136 assert_eq!(
137 map.remove(&(HttpMethod::GET, "https://example.com/b".to_string())),
138 Some(1)
139 );
140 assert!(map.is_empty());
141 }
142
143 #[test]
144 fn http_metrics_allow_custom_labels() {
145 HttpMetrics::reset();
146
147 HttpMetrics::increment_with_label(
148 HttpMethod::GET,
149 "https://example.com/search?q=abc",
150 Some("search"),
151 );
152 HttpMetrics::increment_with_label(
153 HttpMethod::GET,
154 "https://example.com/search?q=def",
155 Some("search"),
156 );
157
158 let snapshot = HttpMetrics::snapshot();
159 let mut map: HashMap<(HttpMethod, String), u64> = snapshot
160 .into_iter()
161 .map(|entry| ((entry.method, entry.url), entry.count))
162 .collect();
163
164 assert_eq!(
165 map.remove(&(HttpMethod::GET, "search".to_string())),
166 Some(2)
167 );
168 assert!(map.is_empty());
169 }
170}