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