Skip to main content

canic_core/workflow/metrics/
query.rs

1//! Module: workflow::metrics::query
2//!
3//! Responsibility: project, sort, and paginate read-only metrics snapshots.
4//! Does not own: metric recording, endpoint authorization, or DTO schemas.
5//! Boundary: workflow query facade over runtime metrics projections.
6
7use crate::{
8    domain::metrics::MetricsKind,
9    dto::{
10        metrics::{MetricEntry, QueryPerfSample},
11        page::{Page, PageRequest},
12    },
13    ops::runtime::metrics,
14    perf,
15    workflow::view::paginate::paginate_vec,
16};
17
18///
19/// MetricsQuery
20///
21/// Read-only query façade over metric snapshots.
22/// Responsible for mapping, sorting, and pagination only.
23///
24
25pub struct MetricsQuery;
26
27impl MetricsQuery {
28    /// Return one sorted, paginated metrics family snapshot.
29    #[must_use]
30    pub fn page(kind: MetricsKind, page: PageRequest) -> Page<MetricEntry> {
31        Self::page_entries(metrics::entries(kind), page)
32    }
33
34    #[must_use]
35    pub fn core(page: PageRequest) -> Page<MetricEntry> {
36        Self::page_entries(metrics::core_entries(), page)
37    }
38
39    #[must_use]
40    pub fn placement(page: PageRequest) -> Page<MetricEntry> {
41        Self::page_entries(metrics::placement_entries(), page)
42    }
43
44    #[must_use]
45    pub fn platform(page: PageRequest) -> Page<MetricEntry> {
46        Self::page_entries(metrics::platform_entries(), page)
47    }
48
49    #[must_use]
50    pub fn runtime(page: PageRequest) -> Page<MetricEntry> {
51        Self::page_entries(metrics::runtime_entries(), page)
52    }
53
54    #[must_use]
55    pub fn security(page: PageRequest) -> Page<MetricEntry> {
56        Self::page_entries(metrics::security_entries(), page)
57    }
58
59    #[must_use]
60    pub fn storage(page: PageRequest) -> Page<MetricEntry> {
61        Self::page_entries(metrics::storage_entries(), page)
62    }
63
64    fn page_entries(mut entries: Vec<MetricEntry>, page: PageRequest) -> Page<MetricEntry> {
65        entries.sort_by(|a, b| {
66            a.labels
67                .cmp(&b.labels)
68                .then_with(|| a.principal.cmp(&b.principal))
69        });
70
71        paginate_vec(entries, page)
72    }
73
74    /// Wrap a query result with the current same-call local instruction count.
75    #[must_use]
76    pub fn sample_query<T>(value: T) -> QueryPerfSample<T> {
77        QueryPerfSample {
78            value,
79            local_instructions: perf::perf_counter(),
80        }
81    }
82}
83
84// -----------------------------------------------------------------------------
85// Tests
86// -----------------------------------------------------------------------------
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    #[cfg(feature = "sharding")]
92    use crate::ops::runtime::metrics::sharding::{
93        ShardingMetricOperation, ShardingMetricOutcome, ShardingMetricReason, ShardingMetrics,
94    };
95    use crate::{
96        ids::{AccessMetricKind, CanisterRole},
97        ops::runtime::metrics::{
98            self,
99            access::AccessMetrics,
100            auth::{
101                AuthMetricOperation, AuthMetricOutcome, AuthMetricReason, AuthMetricSurface,
102                AuthMetrics,
103            },
104            canister_ops::{
105                CanisterOpsMetricOperation, CanisterOpsMetricOutcome, CanisterOpsMetricReason,
106                CanisterOpsMetrics,
107            },
108            cascade::{
109                CascadeMetricOperation, CascadeMetricOutcome, CascadeMetricReason,
110                CascadeMetricSnapshot, CascadeMetrics,
111            },
112            intent::{
113                IntentMetricOperation, IntentMetricOutcome, IntentMetricReason,
114                IntentMetricSurface, IntentMetrics,
115            },
116            placement_index::{
117                PlacementIndexMetricOperation, PlacementIndexMetricOutcome,
118                PlacementIndexMetricReason, PlacementIndexMetrics,
119            },
120            platform_call::{
121                PlatformCallMetricMode, PlatformCallMetricOutcome, PlatformCallMetricReason,
122                PlatformCallMetricSurface, PlatformCallMetrics,
123            },
124            replay::{
125                ReplayMetricOperation, ReplayMetricOutcome, ReplayMetricReason, ReplayMetrics,
126            },
127            scaling::{
128                ScalingMetricOperation, ScalingMetricOutcome, ScalingMetricReason, ScalingMetrics,
129            },
130            wasm_store::{
131                WasmStoreMetricOperation, WasmStoreMetricOutcome, WasmStoreMetricReason,
132                WasmStoreMetricSource, WasmStoreMetrics,
133            },
134        },
135    };
136
137    #[test]
138    fn page_sorts_metric_rows_before_paginating() {
139        metrics::reset_for_tests();
140
141        AccessMetrics::increment("zeta", AccessMetricKind::Auth, "caller_is_root");
142        AccessMetrics::increment("alpha", AccessMetricKind::Guard, "fleet_allows_updates");
143
144        let page = MetricsQuery::page(
145            MetricsKind::Security,
146            PageRequest {
147                limit: 1,
148                offset: 0,
149            },
150        );
151
152        assert_eq!(page.total, 2);
153        assert_eq!(
154            page.entries[0].labels,
155            ["access", "alpha", "guard", "fleet_allows_updates"]
156        );
157
158        let page = MetricsQuery::page(
159            MetricsKind::Security,
160            PageRequest {
161                limit: 1,
162                offset: 1,
163            },
164        );
165
166        assert_eq!(page.total, 2);
167        assert_eq!(
168            page.entries[0].labels,
169            ["access", "zeta", "auth", "caller_is_root"]
170        );
171    }
172
173    #[test]
174    fn page_sorts_auth_metric_family_before_paginating() {
175        metrics::reset_for_tests();
176
177        AuthMetrics::record(
178            AuthMetricSurface::Session,
179            AuthMetricOperation::Session,
180            AuthMetricOutcome::Completed,
181            AuthMetricReason::Created,
182        );
183        AuthMetrics::record(
184            AuthMetricSurface::Attestation,
185            AuthMetricOperation::Verify,
186            AuthMetricOutcome::Failed,
187            AuthMetricReason::VerifyFailed,
188        );
189
190        assert_first_metric_labels(
191            MetricsKind::Security,
192            ["auth", "attestation", "verify", "failed", "verify_failed"],
193        );
194    }
195
196    #[test]
197    fn page_sorts_new_multi_label_metric_families_before_paginating() {
198        metrics::reset_for_tests();
199
200        record_multi_label_sort_metrics();
201
202        assert_first_metric_labels(
203            MetricsKind::Core,
204            ["canister_ops", "create", "app", "started", "ok"],
205        );
206        assert_first_metric_labels(
207            MetricsKind::Storage,
208            [
209                "wasm_store",
210                "chunk_upload",
211                "bootstrap",
212                "skipped",
213                "cache_hit",
214            ],
215        );
216        assert_first_metric_labels(
217            MetricsKind::Placement,
218            ["cascade", "child_send", "state", "failed", "send_failed"],
219        );
220        assert_first_metric_labels(
221            MetricsKind::Runtime,
222            ["intent", "local", "capacity_check", "failed", "capacity"],
223        );
224        assert_first_metric_labels(
225            MetricsKind::Platform,
226            ["platform_call", "generic", "bounded_wait", "started", "ok"],
227        );
228        assert_first_metric_labels(
229            MetricsKind::Security,
230            ["replay", "check", "completed", "fresh"],
231        );
232    }
233
234    // Seed multi-label families used by sorting and pagination coverage.
235    fn record_multi_label_sort_metrics() {
236        CanisterOpsMetrics::record(
237            CanisterOpsMetricOperation::Install,
238            &CanisterRole::new("worker"),
239            CanisterOpsMetricOutcome::Completed,
240            CanisterOpsMetricReason::Ok,
241        );
242        CanisterOpsMetrics::record(
243            CanisterOpsMetricOperation::Create,
244            &CanisterRole::new("app"),
245            CanisterOpsMetricOutcome::Started,
246            CanisterOpsMetricReason::Ok,
247        );
248        WasmStoreMetrics::record(
249            WasmStoreMetricOperation::SourceResolve,
250            WasmStoreMetricSource::Store,
251            WasmStoreMetricOutcome::Completed,
252            WasmStoreMetricReason::Ok,
253        );
254        WasmStoreMetrics::record(
255            WasmStoreMetricOperation::ChunkUpload,
256            WasmStoreMetricSource::Bootstrap,
257            WasmStoreMetricOutcome::Skipped,
258            WasmStoreMetricReason::CacheHit,
259        );
260        CascadeMetrics::record(
261            CascadeMetricOperation::RootFanout,
262            CascadeMetricSnapshot::Topology,
263            CascadeMetricOutcome::Completed,
264            CascadeMetricReason::Ok,
265        );
266        CascadeMetrics::record(
267            CascadeMetricOperation::ChildSend,
268            CascadeMetricSnapshot::State,
269            CascadeMetricOutcome::Failed,
270            CascadeMetricReason::SendFailed,
271        );
272        PlacementIndexMetrics::record(
273            PlacementIndexMetricOperation::Resolve,
274            PlacementIndexMetricOutcome::Started,
275            PlacementIndexMetricReason::Ok,
276        );
277        PlacementIndexMetrics::record(
278            PlacementIndexMetricOperation::Classify,
279            PlacementIndexMetricOutcome::Completed,
280            PlacementIndexMetricReason::PendingFresh,
281        );
282        record_replay_sort_metrics();
283        record_intent_sort_metrics();
284        record_platform_call_sort_metrics();
285        ScalingMetrics::record(
286            ScalingMetricOperation::CreateWorker,
287            ScalingMetricOutcome::Completed,
288            ScalingMetricReason::Ok,
289        );
290        ScalingMetrics::record(
291            ScalingMetricOperation::BootstrapPool,
292            ScalingMetricOutcome::Skipped,
293            ScalingMetricReason::TargetSatisfied,
294        );
295    }
296
297    #[cfg(feature = "sharding")]
298    #[test]
299    fn page_sorts_sharding_metric_family_before_paginating() {
300        metrics::reset_for_tests();
301
302        ShardingMetrics::record(
303            ShardingMetricOperation::PlanAssign,
304            ShardingMetricOutcome::Completed,
305            ShardingMetricReason::ExistingCapacity,
306        );
307        ShardingMetrics::record(
308            ShardingMetricOperation::BootstrapPool,
309            ShardingMetricOutcome::Skipped,
310            ShardingMetricReason::TargetSatisfied,
311        );
312
313        assert_first_metric_labels(
314            MetricsKind::Placement,
315            ["sharding", "bootstrap_pool", "skipped", "target_satisfied"],
316        );
317    }
318
319    #[test]
320    fn sample_query_returns_value_and_current_counter() {
321        let sample = MetricsQuery::sample_query("ok");
322
323        assert_eq!(sample.value, "ok");
324        assert_eq!(sample.local_instructions, 0);
325    }
326
327    // Assert that pagination sees the sorted first row for one metric family.
328    fn assert_first_metric_labels<const N: usize>(kind: MetricsKind, expected: [&str; N]) {
329        let page = MetricsQuery::page(
330            kind,
331            PageRequest {
332                limit: 1,
333                offset: 0,
334            },
335        );
336
337        assert!(page.total > 0);
338        assert_eq!(page.entries[0].labels, expected);
339    }
340
341    // Seed intent rows used by multi-family sorting coverage.
342    fn record_intent_sort_metrics() {
343        IntentMetrics::record(
344            IntentMetricSurface::ReceiptBacked,
345            IntentMetricOperation::Reserve,
346            IntentMetricOutcome::Completed,
347            IntentMetricReason::Ok,
348        );
349        IntentMetrics::record(
350            IntentMetricSurface::Local,
351            IntentMetricOperation::CapacityCheck,
352            IntentMetricOutcome::Failed,
353            IntentMetricReason::Capacity,
354        );
355    }
356
357    // Seed platform call rows used by multi-family sorting coverage.
358    fn record_platform_call_sort_metrics() {
359        PlatformCallMetrics::record(
360            PlatformCallMetricSurface::Management,
361            PlatformCallMetricMode::Update,
362            PlatformCallMetricOutcome::Failed,
363            PlatformCallMetricReason::Infra,
364        );
365        PlatformCallMetrics::record(
366            PlatformCallMetricSurface::Generic,
367            PlatformCallMetricMode::BoundedWait,
368            PlatformCallMetricOutcome::Started,
369            PlatformCallMetricReason::Ok,
370        );
371    }
372
373    // Seed replay rows used by multi-family sorting coverage.
374    fn record_replay_sort_metrics() {
375        ReplayMetrics::record(
376            ReplayMetricOperation::Reserve,
377            ReplayMetricOutcome::Failed,
378            ReplayMetricReason::Capacity,
379        );
380        ReplayMetrics::record(
381            ReplayMetricOperation::Check,
382            ReplayMetricOutcome::Completed,
383            ReplayMetricReason::Fresh,
384        );
385    }
386}