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            directory::{
113                DirectoryMetricOperation, DirectoryMetricOutcome, DirectoryMetricReason,
114                DirectoryMetrics,
115            },
116            intent::{
117                IntentMetricOperation, IntentMetricOutcome, IntentMetricReason,
118                IntentMetricSurface, IntentMetrics,
119            },
120            platform_call::{
121                PlatformCallMetricMode, PlatformCallMetricOutcome, PlatformCallMetricReason,
122                PlatformCallMetricSurface, PlatformCallMetrics,
123            },
124            pool::{PoolMetricOperation, PoolMetricOutcome, PoolMetricReason, PoolMetrics},
125            replay::{
126                ReplayMetricOperation, ReplayMetricOutcome, ReplayMetricReason, ReplayMetrics,
127            },
128            scaling::{
129                ScalingMetricOperation, ScalingMetricOutcome, ScalingMetricReason, ScalingMetrics,
130            },
131            wasm_store::{
132                WasmStoreMetricOperation, WasmStoreMetricOutcome, WasmStoreMetricReason,
133                WasmStoreMetricSource, WasmStoreMetrics,
134            },
135        },
136    };
137
138    #[test]
139    fn page_sorts_metric_rows_before_paginating() {
140        metrics::reset_for_tests();
141
142        AccessMetrics::increment("zeta", AccessMetricKind::Auth, "caller_is_root");
143        AccessMetrics::increment("alpha", AccessMetricKind::Guard, "app_allows_updates");
144
145        let page = MetricsQuery::page(
146            MetricsKind::Security,
147            PageRequest {
148                limit: 1,
149                offset: 0,
150            },
151        );
152
153        assert_eq!(page.total, 2);
154        assert_eq!(
155            page.entries[0].labels,
156            ["access", "alpha", "guard", "app_allows_updates"]
157        );
158
159        let page = MetricsQuery::page(
160            MetricsKind::Security,
161            PageRequest {
162                limit: 1,
163                offset: 1,
164            },
165        );
166
167        assert_eq!(page.total, 2);
168        assert_eq!(
169            page.entries[0].labels,
170            ["access", "zeta", "auth", "caller_is_root"]
171        );
172    }
173
174    #[test]
175    fn page_sorts_auth_metric_family_before_paginating() {
176        metrics::reset_for_tests();
177
178        AuthMetrics::record(
179            AuthMetricSurface::Session,
180            AuthMetricOperation::Session,
181            AuthMetricOutcome::Completed,
182            AuthMetricReason::Created,
183        );
184        AuthMetrics::record(
185            AuthMetricSurface::Attestation,
186            AuthMetricOperation::Verify,
187            AuthMetricOutcome::Failed,
188            AuthMetricReason::VerifyFailed,
189        );
190
191        assert_first_metric_labels(
192            MetricsKind::Security,
193            ["auth", "attestation", "verify", "failed", "verify_failed"],
194        );
195    }
196
197    #[test]
198    fn page_sorts_new_multi_label_metric_families_before_paginating() {
199        metrics::reset_for_tests();
200
201        record_multi_label_sort_metrics();
202
203        assert_first_metric_labels(
204            MetricsKind::Core,
205            ["canister_ops", "create", "app", "started", "ok"],
206        );
207        assert_first_metric_labels(
208            MetricsKind::Storage,
209            [
210                "wasm_store",
211                "chunk_upload",
212                "bootstrap",
213                "skipped",
214                "cache_hit",
215            ],
216        );
217        assert_first_metric_labels(
218            MetricsKind::Placement,
219            ["cascade", "child_send", "state", "failed", "send_failed"],
220        );
221        assert_first_metric_labels(
222            MetricsKind::Runtime,
223            ["intent", "call", "capacity_check", "failed", "capacity"],
224        );
225        assert_first_metric_labels(
226            MetricsKind::Platform,
227            ["platform_call", "generic", "bounded_wait", "started", "ok"],
228        );
229        assert_first_metric_labels(
230            MetricsKind::Security,
231            ["replay", "check", "completed", "fresh"],
232        );
233    }
234
235    // Seed multi-label families used by sorting and pagination coverage.
236    fn record_multi_label_sort_metrics() {
237        CanisterOpsMetrics::record(
238            CanisterOpsMetricOperation::Upgrade,
239            &CanisterRole::new("worker"),
240            CanisterOpsMetricOutcome::Completed,
241            CanisterOpsMetricReason::Ok,
242        );
243        CanisterOpsMetrics::record(
244            CanisterOpsMetricOperation::Create,
245            &CanisterRole::new("app"),
246            CanisterOpsMetricOutcome::Started,
247            CanisterOpsMetricReason::Ok,
248        );
249        WasmStoreMetrics::record(
250            WasmStoreMetricOperation::SourceResolve,
251            WasmStoreMetricSource::Store,
252            WasmStoreMetricOutcome::Completed,
253            WasmStoreMetricReason::Ok,
254        );
255        WasmStoreMetrics::record(
256            WasmStoreMetricOperation::ChunkUpload,
257            WasmStoreMetricSource::Bootstrap,
258            WasmStoreMetricOutcome::Skipped,
259            WasmStoreMetricReason::CacheHit,
260        );
261        CascadeMetrics::record(
262            CascadeMetricOperation::RootFanout,
263            CascadeMetricSnapshot::Topology,
264            CascadeMetricOutcome::Completed,
265            CascadeMetricReason::Ok,
266        );
267        CascadeMetrics::record(
268            CascadeMetricOperation::ChildSend,
269            CascadeMetricSnapshot::State,
270            CascadeMetricOutcome::Failed,
271            CascadeMetricReason::SendFailed,
272        );
273        DirectoryMetrics::record(
274            DirectoryMetricOperation::Resolve,
275            DirectoryMetricOutcome::Started,
276            DirectoryMetricReason::Ok,
277        );
278        DirectoryMetrics::record(
279            DirectoryMetricOperation::Classify,
280            DirectoryMetricOutcome::Completed,
281            DirectoryMetricReason::PendingFresh,
282        );
283        PoolMetrics::record(
284            PoolMetricOperation::ImportQueued,
285            PoolMetricOutcome::Skipped,
286            PoolMetricReason::AlreadyPresent,
287        );
288        PoolMetrics::record(
289            PoolMetricOperation::CreateEmpty,
290            PoolMetricOutcome::Completed,
291            PoolMetricReason::Ok,
292        );
293        record_replay_sort_metrics();
294        record_intent_sort_metrics();
295        record_platform_call_sort_metrics();
296        ScalingMetrics::record(
297            ScalingMetricOperation::CreateWorker,
298            ScalingMetricOutcome::Completed,
299            ScalingMetricReason::Ok,
300        );
301        ScalingMetrics::record(
302            ScalingMetricOperation::BootstrapPool,
303            ScalingMetricOutcome::Skipped,
304            ScalingMetricReason::TargetSatisfied,
305        );
306    }
307
308    #[cfg(feature = "sharding")]
309    #[test]
310    fn page_sorts_sharding_metric_family_before_paginating() {
311        metrics::reset_for_tests();
312
313        ShardingMetrics::record(
314            ShardingMetricOperation::PlanAssign,
315            ShardingMetricOutcome::Completed,
316            ShardingMetricReason::ExistingCapacity,
317        );
318        ShardingMetrics::record(
319            ShardingMetricOperation::BootstrapPool,
320            ShardingMetricOutcome::Skipped,
321            ShardingMetricReason::TargetSatisfied,
322        );
323
324        assert_first_metric_labels(
325            MetricsKind::Placement,
326            ["sharding", "bootstrap_pool", "skipped", "target_satisfied"],
327        );
328    }
329
330    #[test]
331    fn sample_query_returns_value_and_current_counter() {
332        let sample = MetricsQuery::sample_query("ok");
333
334        assert_eq!(sample.value, "ok");
335        assert_eq!(sample.local_instructions, 0);
336    }
337
338    // Assert that pagination sees the sorted first row for one metric family.
339    fn assert_first_metric_labels<const N: usize>(kind: MetricsKind, expected: [&str; N]) {
340        let page = MetricsQuery::page(
341            kind,
342            PageRequest {
343                limit: 1,
344                offset: 0,
345            },
346        );
347
348        assert!(page.total > 0);
349        assert_eq!(page.entries[0].labels, expected);
350    }
351
352    // Seed intent rows used by multi-family sorting coverage.
353    fn record_intent_sort_metrics() {
354        IntentMetrics::record(
355            IntentMetricSurface::Pool,
356            IntentMetricOperation::Reserve,
357            IntentMetricOutcome::Completed,
358            IntentMetricReason::Ok,
359        );
360        IntentMetrics::record(
361            IntentMetricSurface::Call,
362            IntentMetricOperation::CapacityCheck,
363            IntentMetricOutcome::Failed,
364            IntentMetricReason::Capacity,
365        );
366    }
367
368    // Seed platform call rows used by multi-family sorting coverage.
369    fn record_platform_call_sort_metrics() {
370        PlatformCallMetrics::record(
371            PlatformCallMetricSurface::Management,
372            PlatformCallMetricMode::Update,
373            PlatformCallMetricOutcome::Failed,
374            PlatformCallMetricReason::Infra,
375        );
376        PlatformCallMetrics::record(
377            PlatformCallMetricSurface::Generic,
378            PlatformCallMetricMode::BoundedWait,
379            PlatformCallMetricOutcome::Started,
380            PlatformCallMetricReason::Ok,
381        );
382    }
383
384    // Seed replay rows used by multi-family sorting coverage.
385    fn record_replay_sort_metrics() {
386        ReplayMetrics::record(
387            ReplayMetricOperation::Reserve,
388            ReplayMetricOutcome::Failed,
389            ReplayMetricReason::Capacity,
390        );
391        ReplayMetrics::record(
392            ReplayMetricOperation::Check,
393            ReplayMetricOutcome::Completed,
394            ReplayMetricReason::Fresh,
395        );
396    }
397}