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::ApplicationSession,
179            AuthMetricOperation::Establish,
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            [
193                "auth",
194                "application_session",
195                "establish",
196                "completed",
197                "created",
198            ],
199        );
200    }
201
202    #[test]
203    fn page_sorts_new_multi_label_metric_families_before_paginating() {
204        metrics::reset_for_tests();
205
206        record_multi_label_sort_metrics();
207
208        assert_first_metric_labels(
209            MetricsKind::Core,
210            ["canister_ops", "create", "app", "started", "ok"],
211        );
212        assert_first_metric_labels(
213            MetricsKind::Storage,
214            [
215                "wasm_store",
216                "chunk_upload",
217                "bootstrap",
218                "skipped",
219                "cache_hit",
220            ],
221        );
222        assert_first_metric_labels(
223            MetricsKind::Placement,
224            ["cascade", "child_send", "state", "failed", "send_failed"],
225        );
226        assert_first_metric_labels(
227            MetricsKind::Runtime,
228            ["intent", "local", "capacity_check", "failed", "capacity"],
229        );
230        assert_first_metric_labels(
231            MetricsKind::Platform,
232            ["platform_call", "generic", "bounded_wait", "started", "ok"],
233        );
234        assert_first_metric_labels(
235            MetricsKind::Security,
236            ["replay", "check", "completed", "fresh"],
237        );
238    }
239
240    // Seed multi-label families used by sorting and pagination coverage.
241    fn record_multi_label_sort_metrics() {
242        CanisterOpsMetrics::record(
243            CanisterOpsMetricOperation::Install,
244            &CanisterRole::new("worker"),
245            CanisterOpsMetricOutcome::Completed,
246            CanisterOpsMetricReason::Ok,
247        );
248        CanisterOpsMetrics::record(
249            CanisterOpsMetricOperation::Create,
250            &CanisterRole::new("app"),
251            CanisterOpsMetricOutcome::Started,
252            CanisterOpsMetricReason::Ok,
253        );
254        WasmStoreMetrics::record(
255            WasmStoreMetricOperation::SourceResolve,
256            WasmStoreMetricSource::Store,
257            WasmStoreMetricOutcome::Completed,
258            WasmStoreMetricReason::Ok,
259        );
260        WasmStoreMetrics::record(
261            WasmStoreMetricOperation::ChunkUpload,
262            WasmStoreMetricSource::Bootstrap,
263            WasmStoreMetricOutcome::Skipped,
264            WasmStoreMetricReason::CacheHit,
265        );
266        CascadeMetrics::record(
267            CascadeMetricOperation::RootFanout,
268            CascadeMetricSnapshot::Topology,
269            CascadeMetricOutcome::Completed,
270            CascadeMetricReason::Ok,
271        );
272        CascadeMetrics::record(
273            CascadeMetricOperation::ChildSend,
274            CascadeMetricSnapshot::State,
275            CascadeMetricOutcome::Failed,
276            CascadeMetricReason::SendFailed,
277        );
278        PlacementIndexMetrics::record(
279            PlacementIndexMetricOperation::Resolve,
280            PlacementIndexMetricOutcome::Started,
281            PlacementIndexMetricReason::Ok,
282        );
283        PlacementIndexMetrics::record(
284            PlacementIndexMetricOperation::Classify,
285            PlacementIndexMetricOutcome::Completed,
286            PlacementIndexMetricReason::PendingFresh,
287        );
288        record_replay_sort_metrics();
289        record_intent_sort_metrics();
290        record_platform_call_sort_metrics();
291        ScalingMetrics::record(
292            ScalingMetricOperation::CreateWorker,
293            ScalingMetricOutcome::Completed,
294            ScalingMetricReason::Ok,
295        );
296        ScalingMetrics::record(
297            ScalingMetricOperation::BootstrapPool,
298            ScalingMetricOutcome::Skipped,
299            ScalingMetricReason::TargetSatisfied,
300        );
301    }
302
303    #[cfg(feature = "sharding")]
304    #[test]
305    fn page_sorts_sharding_metric_family_before_paginating() {
306        metrics::reset_for_tests();
307
308        ShardingMetrics::record(
309            ShardingMetricOperation::PlanAssign,
310            ShardingMetricOutcome::Completed,
311            ShardingMetricReason::ExistingCapacity,
312        );
313        ShardingMetrics::record(
314            ShardingMetricOperation::BootstrapPool,
315            ShardingMetricOutcome::Skipped,
316            ShardingMetricReason::TargetSatisfied,
317        );
318
319        assert_first_metric_labels(
320            MetricsKind::Placement,
321            ["sharding", "bootstrap_pool", "skipped", "target_satisfied"],
322        );
323    }
324
325    #[test]
326    fn sample_query_returns_value_and_current_counter() {
327        let sample = MetricsQuery::sample_query("ok");
328
329        assert_eq!(sample.value, "ok");
330        assert_eq!(sample.local_instructions, 0);
331    }
332
333    // Assert that pagination sees the sorted first row for one metric family.
334    fn assert_first_metric_labels<const N: usize>(kind: MetricsKind, expected: [&str; N]) {
335        let page = MetricsQuery::page(
336            kind,
337            PageRequest {
338                limit: 1,
339                offset: 0,
340            },
341        );
342
343        assert!(page.total > 0);
344        assert_eq!(page.entries[0].labels, expected);
345    }
346
347    // Seed intent rows used by multi-family sorting coverage.
348    fn record_intent_sort_metrics() {
349        IntentMetrics::record(
350            IntentMetricSurface::ReceiptBacked,
351            IntentMetricOperation::Reserve,
352            IntentMetricOutcome::Completed,
353            IntentMetricReason::Ok,
354        );
355        IntentMetrics::record(
356            IntentMetricSurface::Local,
357            IntentMetricOperation::CapacityCheck,
358            IntentMetricOutcome::Failed,
359            IntentMetricReason::Capacity,
360        );
361    }
362
363    // Seed platform call rows used by multi-family sorting coverage.
364    fn record_platform_call_sort_metrics() {
365        PlatformCallMetrics::record(
366            PlatformCallMetricSurface::Management,
367            PlatformCallMetricMode::Update,
368            PlatformCallMetricOutcome::Failed,
369            PlatformCallMetricReason::Infra,
370        );
371        PlatformCallMetrics::record(
372            PlatformCallMetricSurface::Generic,
373            PlatformCallMetricMode::BoundedWait,
374            PlatformCallMetricOutcome::Started,
375            PlatformCallMetricReason::Ok,
376        );
377    }
378
379    // Seed replay rows used by multi-family sorting coverage.
380    fn record_replay_sort_metrics() {
381        ReplayMetrics::record(
382            ReplayMetricOperation::Reserve,
383            ReplayMetricOutcome::Failed,
384            ReplayMetricReason::Capacity,
385        );
386        ReplayMetrics::record(
387            ReplayMetricOperation::Check,
388            ReplayMetricOutcome::Completed,
389            ReplayMetricReason::Fresh,
390        );
391    }
392}