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    dto::{
9        metrics::{MetricEntry, MetricsKind, QueryPerfSample},
10        page::{Page, PageRequest},
11    },
12    ops::runtime::metrics,
13    perf,
14    workflow::view::paginate::paginate_vec,
15};
16
17///
18/// MetricsQuery
19///
20/// Read-only query façade over metric snapshots.
21/// Responsible for mapping, sorting, and pagination only.
22///
23
24pub struct MetricsQuery;
25
26impl MetricsQuery {
27    /// Return one sorted, paginated metrics family snapshot.
28    #[must_use]
29    pub fn page(kind: MetricsKind, page: PageRequest) -> Page<MetricEntry> {
30        Self::page_entries(metrics::entries(kind), page)
31    }
32
33    #[must_use]
34    pub fn core(page: PageRequest) -> Page<MetricEntry> {
35        Self::page_entries(metrics::core_entries(), page)
36    }
37
38    #[must_use]
39    pub fn placement(page: PageRequest) -> Page<MetricEntry> {
40        Self::page_entries(metrics::placement_entries(), page)
41    }
42
43    #[must_use]
44    pub fn platform(page: PageRequest) -> Page<MetricEntry> {
45        Self::page_entries(metrics::platform_entries(), page)
46    }
47
48    #[must_use]
49    pub fn runtime(page: PageRequest) -> Page<MetricEntry> {
50        Self::page_entries(metrics::runtime_entries(), page)
51    }
52
53    #[must_use]
54    pub fn security(page: PageRequest) -> Page<MetricEntry> {
55        Self::page_entries(metrics::security_entries(), page)
56    }
57
58    #[must_use]
59    pub fn storage(page: PageRequest) -> Page<MetricEntry> {
60        Self::page_entries(metrics::storage_entries(), page)
61    }
62
63    fn page_entries(mut entries: Vec<MetricEntry>, page: PageRequest) -> Page<MetricEntry> {
64        entries.sort_by(|a, b| {
65            a.labels
66                .cmp(&b.labels)
67                .then_with(|| a.principal.cmp(&b.principal))
68        });
69
70        paginate_vec(entries, page)
71    }
72
73    /// Wrap a query result with the current same-call local instruction count.
74    #[must_use]
75    pub fn sample_query<T>(value: T) -> QueryPerfSample<T> {
76        QueryPerfSample {
77            value,
78            local_instructions: perf::perf_counter(),
79        }
80    }
81}
82
83// -----------------------------------------------------------------------------
84// Tests
85// -----------------------------------------------------------------------------
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    #[cfg(feature = "sharding")]
91    use crate::ops::runtime::metrics::sharding::{
92        ShardingMetricOperation, ShardingMetricOutcome, ShardingMetricReason, ShardingMetrics,
93    };
94    use crate::{
95        ids::{AccessMetricKind, CanisterRole},
96        ops::runtime::metrics::{
97            self,
98            access::AccessMetrics,
99            auth::{
100                AuthMetricOperation, AuthMetricOutcome, AuthMetricReason, AuthMetricSurface,
101                AuthMetrics,
102            },
103            canister_ops::{
104                CanisterOpsMetricOperation, CanisterOpsMetricOutcome, CanisterOpsMetricReason,
105                CanisterOpsMetrics,
106            },
107            cascade::{
108                CascadeMetricOperation, CascadeMetricOutcome, CascadeMetricReason,
109                CascadeMetricSnapshot, CascadeMetrics,
110            },
111            directory::{
112                DirectoryMetricOperation, DirectoryMetricOutcome, DirectoryMetricReason,
113                DirectoryMetrics,
114            },
115            intent::{
116                IntentMetricOperation, IntentMetricOutcome, IntentMetricReason,
117                IntentMetricSurface, IntentMetrics,
118            },
119            platform_call::{
120                PlatformCallMetricMode, PlatformCallMetricOutcome, PlatformCallMetricReason,
121                PlatformCallMetricSurface, PlatformCallMetrics,
122            },
123            pool::{PoolMetricOperation, PoolMetricOutcome, PoolMetricReason, PoolMetrics},
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, "app_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", "app_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", "call", "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::Upgrade,
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        DirectoryMetrics::record(
273            DirectoryMetricOperation::Resolve,
274            DirectoryMetricOutcome::Started,
275            DirectoryMetricReason::Ok,
276        );
277        DirectoryMetrics::record(
278            DirectoryMetricOperation::Classify,
279            DirectoryMetricOutcome::Completed,
280            DirectoryMetricReason::PendingFresh,
281        );
282        PoolMetrics::record(
283            PoolMetricOperation::ImportQueued,
284            PoolMetricOutcome::Skipped,
285            PoolMetricReason::AlreadyPresent,
286        );
287        PoolMetrics::record(
288            PoolMetricOperation::CreateEmpty,
289            PoolMetricOutcome::Completed,
290            PoolMetricReason::Ok,
291        );
292        record_replay_sort_metrics();
293        record_intent_sort_metrics();
294        record_platform_call_sort_metrics();
295        ScalingMetrics::record(
296            ScalingMetricOperation::CreateWorker,
297            ScalingMetricOutcome::Completed,
298            ScalingMetricReason::Ok,
299        );
300        ScalingMetrics::record(
301            ScalingMetricOperation::BootstrapPool,
302            ScalingMetricOutcome::Skipped,
303            ScalingMetricReason::TargetSatisfied,
304        );
305    }
306
307    #[cfg(feature = "sharding")]
308    #[test]
309    fn page_sorts_sharding_metric_family_before_paginating() {
310        metrics::reset_for_tests();
311
312        ShardingMetrics::record(
313            ShardingMetricOperation::PlanAssign,
314            ShardingMetricOutcome::Completed,
315            ShardingMetricReason::ExistingCapacity,
316        );
317        ShardingMetrics::record(
318            ShardingMetricOperation::BootstrapPool,
319            ShardingMetricOutcome::Skipped,
320            ShardingMetricReason::TargetSatisfied,
321        );
322
323        assert_first_metric_labels(
324            MetricsKind::Placement,
325            ["sharding", "bootstrap_pool", "skipped", "target_satisfied"],
326        );
327    }
328
329    #[test]
330    fn sample_query_returns_value_and_current_counter() {
331        let sample = MetricsQuery::sample_query("ok");
332
333        assert_eq!(sample.value, "ok");
334        assert_eq!(sample.local_instructions, 0);
335    }
336
337    // Assert that pagination sees the sorted first row for one metric family.
338    fn assert_first_metric_labels<const N: usize>(kind: MetricsKind, expected: [&str; N]) {
339        let page = MetricsQuery::page(
340            kind,
341            PageRequest {
342                limit: 1,
343                offset: 0,
344            },
345        );
346
347        assert!(page.total > 0);
348        assert_eq!(page.entries[0].labels, expected);
349    }
350
351    // Seed intent rows used by multi-family sorting coverage.
352    fn record_intent_sort_metrics() {
353        IntentMetrics::record(
354            IntentMetricSurface::Pool,
355            IntentMetricOperation::Reserve,
356            IntentMetricOutcome::Completed,
357            IntentMetricReason::Ok,
358        );
359        IntentMetrics::record(
360            IntentMetricSurface::Call,
361            IntentMetricOperation::CapacityCheck,
362            IntentMetricOutcome::Failed,
363            IntentMetricReason::Capacity,
364        );
365    }
366
367    // Seed platform call rows used by multi-family sorting coverage.
368    fn record_platform_call_sort_metrics() {
369        PlatformCallMetrics::record(
370            PlatformCallMetricSurface::Management,
371            PlatformCallMetricMode::Update,
372            PlatformCallMetricOutcome::Failed,
373            PlatformCallMetricReason::Infra,
374        );
375        PlatformCallMetrics::record(
376            PlatformCallMetricSurface::Generic,
377            PlatformCallMetricMode::BoundedWait,
378            PlatformCallMetricOutcome::Started,
379            PlatformCallMetricReason::Ok,
380        );
381    }
382
383    // Seed replay rows used by multi-family sorting coverage.
384    fn record_replay_sort_metrics() {
385        ReplayMetrics::record(
386            ReplayMetricOperation::Reserve,
387            ReplayMetricOutcome::Failed,
388            ReplayMetricReason::Capacity,
389        );
390        ReplayMetrics::record(
391            ReplayMetricOperation::Check,
392            ReplayMetricOutcome::Completed,
393            ReplayMetricReason::Fresh,
394        );
395    }
396}