canic_core/workflow/metrics/
query.rs1use crate::{
2 dto::{
3 metrics::{
4 AccessMetricEntry, AuthMetricEntry, AuthRolloutMetricEntry, CyclesFundingMetricEntry,
5 DelegationMetricEntry, EndpointHealth, HttpMetricEntry, IccMetricEntry, MetricsKind,
6 MetricsRequest, MetricsResponse, RootCapabilityMetricEntry, SystemMetricEntry,
7 TimerMetricEntry,
8 },
9 page::{Page, PageRequest},
10 },
11 ops::{
12 perf::PerfOps,
13 runtime::metrics::{
14 MetricsOps,
15 mapper::{
16 AccessMetricEntryMapper, AuthMetricEntryMapper, AuthRolloutMetricEntryMapper,
17 CyclesFundingMetricEntryMapper, DelegationMetricEntryMapper, EndpointHealthMapper,
18 HttpMetricEntryMapper, IccMetricEntryMapper, RootCapabilityMetricEntryMapper,
19 SystemMetricEntryMapper, TimerMetricEntryMapper,
20 },
21 },
22 },
23 perf::PerfEntry,
24 workflow::view::paginate::paginate_vec,
25};
26
27pub struct MetricsQuery;
35
36impl MetricsQuery {
37 #[must_use]
38 pub fn dispatch(req: MetricsRequest) -> MetricsResponse {
39 match req.kind {
40 MetricsKind::System => MetricsResponse::System(Self::system_snapshot()),
41 MetricsKind::Icc => MetricsResponse::Icc(Self::icc_page(req.page)),
42 MetricsKind::Http => MetricsResponse::Http(Self::http_page(req.page)),
43 MetricsKind::Timer => MetricsResponse::Timer(Self::timer_page(req.page)),
44 MetricsKind::Access => MetricsResponse::Access(Self::access_page(req.page)),
45 MetricsKind::Auth => MetricsResponse::Auth(Self::auth_page(req.page)),
46 MetricsKind::AuthRollout => {
47 MetricsResponse::AuthRollout(Self::auth_rollout_page(req.page))
48 }
49 MetricsKind::Delegation => MetricsResponse::Delegation(Self::delegation_page(req.page)),
50 MetricsKind::RootCapability => {
51 MetricsResponse::RootCapability(Self::root_capability_page(req.page))
52 }
53 MetricsKind::CyclesFunding => {
54 MetricsResponse::CyclesFunding(Self::cycles_funding_page(req.page))
55 }
56 MetricsKind::Perf => MetricsResponse::Perf(Self::perf_page(req.page)),
57 MetricsKind::EndpointHealth => MetricsResponse::EndpointHealth(
58 Self::endpoint_health_page(req.page, Some(crate::protocol::CANIC_METRICS)),
59 ),
60 }
61 }
62
63 #[must_use]
64 pub fn system_snapshot() -> Vec<SystemMetricEntry> {
65 let snapshot = MetricsOps::system_snapshot();
66 let mut entries = SystemMetricEntryMapper::record_to_view(snapshot.entries);
67
68 entries.sort_by(|a, b| a.kind.cmp(&b.kind));
69
70 entries
71 }
72
73 #[must_use]
74 pub fn icc_page(page: PageRequest) -> Page<IccMetricEntry> {
75 let snapshot = MetricsOps::icc_snapshot();
76 let mut entries = IccMetricEntryMapper::record_to_view(snapshot.entries);
77
78 entries.sort_by(|a, b| {
79 a.target
80 .as_slice()
81 .cmp(b.target.as_slice())
82 .then_with(|| a.method.cmp(&b.method))
83 });
84
85 paginate_vec(entries, page)
86 }
87
88 #[must_use]
89 pub fn http_page(page: PageRequest) -> Page<HttpMetricEntry> {
90 let snapshot = MetricsOps::http_snapshot();
91 let mut entries = HttpMetricEntryMapper::record_to_view(snapshot.entries);
92
93 entries.sort_by(|a, b| a.method.cmp(&b.method).then_with(|| a.label.cmp(&b.label)));
94
95 paginate_vec(entries, page)
96 }
97
98 #[must_use]
99 pub fn timer_page(page: PageRequest) -> Page<TimerMetricEntry> {
100 let snapshot = MetricsOps::timer_snapshot();
101 let mut entries = TimerMetricEntryMapper::record_to_view(snapshot.entries);
102
103 entries.sort_by(|a, b| {
104 a.mode
105 .cmp(&b.mode)
106 .then_with(|| a.delay_ms.cmp(&b.delay_ms))
107 .then_with(|| a.label.cmp(&b.label))
108 });
109
110 paginate_vec(entries, page)
111 }
112
113 #[must_use]
114 pub fn access_page(page: PageRequest) -> Page<AccessMetricEntry> {
115 let snapshot = MetricsOps::access_snapshot();
116 let mut entries = AccessMetricEntryMapper::record_to_view(snapshot.entries);
117
118 entries.sort_by(|a, b| {
119 a.endpoint
120 .cmp(&b.endpoint)
121 .then_with(|| a.kind.cmp(&b.kind))
122 .then_with(|| a.predicate.cmp(&b.predicate))
123 });
124
125 paginate_vec(entries, page)
126 }
127
128 #[must_use]
129 pub fn auth_page(page: PageRequest) -> Page<AuthMetricEntry> {
130 let snapshot = MetricsOps::access_snapshot();
131 let mut entries = AuthMetricEntryMapper::record_to_view(snapshot.entries);
132
133 entries.sort_by(|a, b| {
134 a.endpoint
135 .cmp(&b.endpoint)
136 .then_with(|| a.predicate.cmp(&b.predicate))
137 });
138
139 paginate_vec(entries, page)
140 }
141
142 #[must_use]
143 pub fn auth_rollout_page(page: PageRequest) -> Page<AuthRolloutMetricEntry> {
144 let snapshot = MetricsOps::access_snapshot();
145 let entries = AuthRolloutMetricEntryMapper::record_to_view(snapshot.entries);
146
147 paginate_vec(entries, page)
148 }
149
150 #[must_use]
151 pub fn delegation_page(page: PageRequest) -> Page<DelegationMetricEntry> {
152 let snapshot = MetricsOps::delegation_snapshot();
153 let mut entries = DelegationMetricEntryMapper::record_to_view(snapshot.entries);
154
155 entries.sort_by(|a, b| a.authority.as_slice().cmp(b.authority.as_slice()));
156
157 paginate_vec(entries, page)
158 }
159
160 #[must_use]
161 pub fn root_capability_page(page: PageRequest) -> Page<RootCapabilityMetricEntry> {
162 let snapshot = MetricsOps::root_capability_snapshot();
163 let mut entries = RootCapabilityMetricEntryMapper::record_to_view(snapshot.entries);
164
165 entries.sort_by(|a, b| {
166 a.capability
167 .cmp(&b.capability)
168 .then_with(|| a.event_type.cmp(&b.event_type))
169 .then_with(|| a.outcome.cmp(&b.outcome))
170 .then_with(|| a.proof_mode.cmp(&b.proof_mode))
171 });
172
173 paginate_vec(entries, page)
174 }
175
176 #[must_use]
177 pub fn cycles_funding_page(page: PageRequest) -> Page<CyclesFundingMetricEntry> {
178 let snapshot = MetricsOps::cycles_funding_snapshot();
179 let mut entries = CyclesFundingMetricEntryMapper::record_to_view(snapshot.entries);
180
181 entries.sort_by(|a, b| {
182 a.metric
183 .cmp(&b.metric)
184 .then_with(|| a.child_principal.cmp(&b.child_principal))
185 .then_with(|| a.reason.cmp(&b.reason))
186 });
187
188 paginate_vec(entries, page)
189 }
190
191 #[must_use]
192 pub fn perf_page(page: PageRequest) -> Page<PerfEntry> {
193 let snapshot = PerfOps::snapshot();
194 paginate_vec(snapshot.entries, page)
195 }
196
197 #[must_use]
198 pub fn endpoint_health_page(
199 page: PageRequest,
200 exclude_endpoint: Option<&str>,
201 ) -> Page<EndpointHealth> {
202 let snapshot = MetricsOps::endpoint_health_snapshot();
203 let mut entries = EndpointHealthMapper::record_to_view(
204 snapshot.attempts,
205 snapshot.results,
206 snapshot.access,
207 exclude_endpoint,
208 );
209
210 entries.sort_by(|a, b| a.endpoint.cmp(&b.endpoint));
211
212 paginate_vec(entries, page)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::{
220 dto::metrics::AuthRolloutMetricClass, ids::AccessMetricKind,
221 ops::runtime::metrics::access::AccessMetrics,
222 };
223
224 #[test]
225 fn auth_page_filters_non_auth_metrics() {
226 AccessMetrics::reset();
227
228 AccessMetrics::increment(
229 "auth_verifier",
230 AccessMetricKind::Auth,
231 "token_rejected_proof_miss",
232 );
233 AccessMetrics::increment(
234 "auth_verifier",
235 AccessMetricKind::Auth,
236 "token_rejected_proof_miss",
237 );
238 AccessMetrics::increment(
239 "canic_metrics",
240 AccessMetricKind::Guard,
241 "caller_is_controller",
242 );
243
244 let page = MetricsQuery::auth_page(PageRequest {
245 offset: 0,
246 limit: 10,
247 });
248
249 assert_eq!(page.entries.len(), 1);
250 assert_eq!(page.entries[0].endpoint, "auth_verifier");
251 assert_eq!(page.entries[0].predicate, "token_rejected_proof_miss");
252 assert_eq!(page.entries[0].count, 2);
253 }
254
255 #[test]
256 fn auth_rollout_page_groups_gate_and_operational_signals() {
257 AccessMetrics::reset();
258
259 AccessMetrics::increment(
260 "auth_verifier",
261 AccessMetricKind::Auth,
262 "token_rejected_proof_miss",
263 );
264 AccessMetrics::increment(
265 "auth_verifier",
266 AccessMetricKind::Auth,
267 "token_rejected_proof_mismatch",
268 );
269 AccessMetrics::increment(
270 "auth_verifier",
271 AccessMetricKind::Auth,
272 "proof_cache_evictions_total{class=\"active\"}",
273 );
274 AccessMetrics::increment(
275 "auth_verifier",
276 AccessMetricKind::Auth,
277 "proof_cache_utilization{bucket=\"95_100\"}",
278 );
279 AccessMetrics::increment(
280 "auth_signer",
281 AccessMetricKind::Auth,
282 "delegation_push_failed{role=\"verifier\",origin=\"repair\"}",
283 );
284 AccessMetrics::increment(
285 "auth_signer",
286 AccessMetricKind::Auth,
287 "delegation_install_validation_failed{intent=\"prewarm\",stage=\"post_normalization\",reason=\"verify_proof\"}",
288 );
289 AccessMetrics::increment(
290 "auth_verifier",
291 AccessMetricKind::Auth,
292 "proof_cache_evictions_total{class=\"cold\"}",
293 );
294 AccessMetrics::increment(
295 "canic_metrics",
296 AccessMetricKind::Guard,
297 "caller_is_controller",
298 );
299
300 let page = MetricsQuery::auth_rollout_page(PageRequest {
301 offset: 0,
302 limit: 20,
303 });
304
305 assert_eq!(page.entries.len(), 7);
306 assert_eq!(
307 rollout_entry(&page, "proof_miss"),
308 Some((AuthRolloutMetricClass::HardGate, 1))
309 );
310 assert_eq!(
311 rollout_entry(&page, "proof_mismatch"),
312 Some((AuthRolloutMetricClass::HardGate, 1))
313 );
314 assert_eq!(
315 rollout_entry(&page, "active_proof_eviction"),
316 Some((AuthRolloutMetricClass::HardGate, 1))
317 );
318 assert_eq!(
319 rollout_entry(&page, "repair_failure"),
320 Some((AuthRolloutMetricClass::HardGate, 1))
321 );
322 assert_eq!(
323 rollout_entry(&page, "cache_saturation"),
324 Some((AuthRolloutMetricClass::HardGate, 1))
325 );
326 assert_eq!(
327 rollout_entry(&page, "cold_proof_eviction"),
328 Some((AuthRolloutMetricClass::Operational, 1))
329 );
330 assert_eq!(
331 rollout_entry(&page, "prewarm_failure"),
332 Some((AuthRolloutMetricClass::Operational, 1))
333 );
334 }
335
336 fn rollout_entry(
337 page: &Page<AuthRolloutMetricEntry>,
338 signal: &str,
339 ) -> Option<(AuthRolloutMetricClass, u64)> {
340 page.entries
341 .iter()
342 .find_map(|entry| (entry.signal == signal).then_some((entry.class, entry.count)))
343 }
344}