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