Skip to main content

canic_core/workflow/metrics/
query.rs

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