1pub use crate::model::metrics::{access::*, endpoint::*, http::*, icc::*, system::*, timer::*};
2use crate::{
3 dto::page::{Page, PageRequest},
4 perf::{PerfKey, entries as perf_entries},
5};
6use candid::CandidType;
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeSet, HashMap};
9
10pub struct MetricsOps;
16
17#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
23pub struct EndpointHealthEntry {
24 pub endpoint: String,
25 pub attempted: u64,
26 pub denied: u64,
27 pub completed: u64,
28 pub ok: u64,
29 pub err: u64,
30 pub avg_instr: u64,
31 pub total_instr: u64,
32}
33
34impl MetricsOps {
35 #[must_use]
37 pub fn system_snapshot() -> SystemMetricsSnapshot {
38 let mut entries = SystemMetrics::snapshot();
39 entries.sort_by(|a, b| a.kind.cmp(&b.kind));
40 entries
41 }
42
43 #[must_use]
45 pub fn http_snapshot() -> HttpMetricsSnapshot {
46 HttpMetrics::snapshot()
47 }
48
49 #[must_use]
51 pub fn http_page(request: PageRequest) -> Page<HttpMetricEntry> {
52 let mut entries = Self::http_snapshot();
53 entries.sort_by(|a, b| a.method.cmp(&b.method).then_with(|| a.url.cmp(&b.url)));
54 paginate(entries, request)
55 }
56
57 #[must_use]
59 pub fn icc_snapshot() -> IccMetricsSnapshot {
60 IccMetrics::snapshot()
61 }
62
63 #[must_use]
65 pub fn icc_page(request: PageRequest) -> Page<IccMetricEntry> {
66 let mut entries = Self::icc_snapshot();
67 entries.sort_by(|a, b| {
68 a.target
69 .as_slice()
70 .cmp(b.target.as_slice())
71 .then_with(|| a.method.cmp(&b.method))
72 });
73 paginate(entries, request)
74 }
75
76 #[must_use]
78 pub fn timer_snapshot() -> TimerMetricsSnapshot {
79 TimerMetrics::snapshot()
80 }
81
82 #[must_use]
84 pub fn timer_page(request: PageRequest) -> Page<TimerMetricEntry> {
85 let mut entries = Self::timer_snapshot();
86 entries.sort_by(|a, b| {
87 a.mode
88 .cmp(&b.mode)
89 .then_with(|| a.delay_ms.cmp(&b.delay_ms))
90 .then_with(|| a.label.cmp(&b.label))
91 });
92 paginate(entries, request)
93 }
94
95 #[must_use]
97 pub fn access_snapshot() -> AccessMetricsSnapshot {
98 AccessMetrics::snapshot()
99 }
100
101 #[must_use]
103 pub fn access_page(request: PageRequest) -> Page<AccessMetricEntry> {
104 let mut entries = Self::access_snapshot();
105 entries.sort_by(|a, b| {
106 a.endpoint
107 .cmp(&b.endpoint)
108 .then_with(|| a.kind.cmp(&b.kind))
109 });
110 paginate(entries, request)
111 }
112
113 #[must_use]
115 pub fn endpoint_health_page(request: PageRequest) -> Page<EndpointHealthEntry> {
116 Self::endpoint_health_page_excluding(request, None)
117 }
118
119 #[must_use]
122 pub fn endpoint_health_page_excluding(
123 request: PageRequest,
124 exclude_endpoint: Option<&str>,
125 ) -> Page<EndpointHealthEntry> {
126 let attempt_snapshot = EndpointAttemptMetrics::snapshot();
127 let result_snapshot = EndpointResultMetrics::snapshot();
128 let access_snapshot = AccessMetrics::snapshot();
129 let perf_snapshot = perf_endpoint_snapshot();
130
131 let mut attempts: HashMap<String, (u64, u64)> = HashMap::new();
132 for entry in attempt_snapshot {
133 attempts.insert(entry.endpoint, (entry.attempted, entry.completed));
134 }
135
136 let mut results: HashMap<String, (u64, u64)> = HashMap::new();
137 for entry in result_snapshot {
138 results.insert(entry.endpoint, (entry.ok, entry.err));
139 }
140
141 let mut denied: HashMap<String, u64> = HashMap::new();
142 for entry in access_snapshot {
143 let counter = denied.entry(entry.endpoint).or_insert(0);
144 *counter = counter.saturating_add(entry.count);
145 }
146
147 let mut endpoints = BTreeSet::<String>::new();
148 endpoints.extend(attempts.keys().cloned());
149 endpoints.extend(results.keys().cloned());
150 endpoints.extend(denied.keys().cloned());
151 endpoints.extend(perf_snapshot.keys().cloned());
152
153 let entries = endpoints
154 .into_iter()
155 .filter(|endpoint| match exclude_endpoint {
156 Some(excluded) => endpoint != excluded,
157 None => true,
158 })
159 .map(|endpoint| {
160 let (attempted, completed) = attempts.get(&endpoint).copied().unwrap_or((0, 0));
161
162 let denied = denied.get(&endpoint).copied().unwrap_or(0);
164 let (ok, err) = results.get(&endpoint).copied().unwrap_or((0, 0));
165
166 let (perf_count, total_instr) =
167 perf_snapshot.get(&endpoint).copied().unwrap_or((0, 0));
168 let avg_instr = if perf_count == 0 {
169 0
170 } else {
171 total_instr / perf_count
172 };
173
174 EndpointHealthEntry {
175 endpoint,
176 attempted,
177 denied,
178 completed,
179 ok,
180 err,
181 avg_instr,
182 total_instr,
183 }
184 })
185 .collect::<Vec<_>>();
186
187 paginate(entries, request)
188 }
189}
190
191#[must_use]
196fn paginate<T>(entries: Vec<T>, request: PageRequest) -> Page<T> {
197 let request = request.clamped();
198 let total = entries.len() as u64;
199 let (start, end) = pagination_bounds(total, request);
200
201 let entries = entries.into_iter().skip(start).take(end - start).collect();
202
203 Page { entries, total }
204}
205
206#[allow(clippy::cast_possible_truncation)]
207fn pagination_bounds(total: u64, request: PageRequest) -> (usize, usize) {
208 let start = request.offset.min(total);
209 let end = request.offset.saturating_add(request.limit).min(total);
210
211 let start = start as usize;
212 let end = end as usize;
213
214 (start, end)
215}
216
217#[must_use]
230fn perf_endpoint_snapshot() -> HashMap<String, (u64, u64)> {
231 let mut out = HashMap::<String, (u64, u64)>::new();
232
233 for entry in perf_entries() {
234 let PerfKey::Endpoint(label) = &entry.key else {
235 continue;
236 };
237
238 let slot = out.entry(label.clone()).or_insert((0, 0));
239 slot.0 = slot.0.saturating_add(entry.count);
240 slot.1 = slot.1.saturating_add(entry.total_instructions);
241 }
242
243 out
244}
245
246#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::perf;
254
255 #[test]
256 fn endpoint_health_joins_tables() {
257 EndpointAttemptMetrics::reset();
258 EndpointResultMetrics::reset();
259 AccessMetrics::reset();
260 perf::reset();
261
262 EndpointAttemptMetrics::increment_attempted("a");
263 EndpointAttemptMetrics::increment_attempted("a");
264 EndpointAttemptMetrics::increment_completed("a");
265 EndpointResultMetrics::increment_ok("a");
266 perf::record_endpoint("a", 1_000);
267
268 EndpointAttemptMetrics::increment_attempted("b");
269 AccessMetrics::increment("b", AccessMetricKind::Auth);
270
271 let page = MetricsOps::endpoint_health_page(PageRequest::new(10, 0));
272 assert_eq!(page.total, 2);
273
274 let a = &page.entries[0];
275 assert_eq!(a.endpoint, "a");
276 assert_eq!(a.attempted, 2);
277 assert_eq!(a.denied, 0);
278 assert_eq!(a.completed, 1);
279 assert_eq!(a.ok, 1);
280 assert_eq!(a.err, 0);
281 assert_eq!(a.total_instr, 1_000);
282 assert_eq!(a.avg_instr, 1_000);
283
284 let b = &page.entries[1];
285 assert_eq!(b.endpoint, "b");
286 assert_eq!(b.attempted, 1);
287 assert_eq!(b.denied, 1);
288 assert_eq!(b.completed, 0);
289 assert_eq!(b.ok, 0);
290 assert_eq!(b.err, 0);
291 assert_eq!(b.total_instr, 0);
292 assert_eq!(b.avg_instr, 0);
293 }
294}