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