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