Skip to main content

canic_core/workflow/metrics/
query.rs

1use 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
27///
28/// MetricsQuery
29///
30/// Read-only query façade over metric snapshots.
31/// Responsible for mapping, sorting, and pagination only.
32///
33
34pub 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}