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            pool::{PoolMetricOperation, PoolMetricOutcome, PoolMetricReason, PoolMetrics},
77            replay::{
78                ReplayMetricOperation, ReplayMetricOutcome, ReplayMetricReason, ReplayMetrics,
79            },
80            scaling::{
81                ScalingMetricOperation, ScalingMetricOutcome, ScalingMetricReason, ScalingMetrics,
82            },
83            wasm_store::{
84                WasmStoreMetricOperation, WasmStoreMetricOutcome, WasmStoreMetricReason,
85                WasmStoreMetricSource, WasmStoreMetrics,
86            },
87        },
88    };
89
90    #[test]
91    fn page_sorts_metric_rows_before_paginating() {
92        metrics::reset_for_tests();
93
94        AccessMetrics::increment("zeta", AccessMetricKind::Auth, "caller_is_root");
95        AccessMetrics::increment("alpha", AccessMetricKind::Guard, "app_allows_updates");
96
97        let page = MetricsQuery::page(
98            MetricsKind::Access,
99            PageRequest {
100                limit: 1,
101                offset: 0,
102            },
103        );
104
105        assert_eq!(page.total, 2);
106        assert_eq!(
107            page.entries[0].labels,
108            ["alpha", "guard", "app_allows_updates"]
109        );
110
111        let page = MetricsQuery::page(
112            MetricsKind::Access,
113            PageRequest {
114                limit: 1,
115                offset: 1,
116            },
117        );
118
119        assert_eq!(page.total, 2);
120        assert_eq!(page.entries[0].labels, ["zeta", "auth", "caller_is_root"]);
121    }
122
123    #[test]
124    fn page_sorts_auth_metric_family_before_paginating() {
125        metrics::reset_for_tests();
126
127        AuthMetrics::record(
128            AuthMetricSurface::Session,
129            AuthMetricOperation::Session,
130            AuthMetricOutcome::Completed,
131            AuthMetricReason::Created,
132        );
133        AuthMetrics::record(
134            AuthMetricSurface::Attestation,
135            AuthMetricOperation::Verify,
136            AuthMetricOutcome::Failed,
137            AuthMetricReason::UnknownKeyId,
138        );
139
140        assert_first_metric_labels(
141            MetricsKind::Auth,
142            ["attestation", "verify", "failed", "unknown_key_id"],
143        );
144    }
145
146    #[test]
147    fn page_sorts_new_multi_label_metric_families_before_paginating() {
148        metrics::reset_for_tests();
149
150        CanisterOpsMetrics::record(
151            CanisterOpsMetricOperation::Upgrade,
152            &CanisterRole::new("worker"),
153            CanisterOpsMetricOutcome::Completed,
154            CanisterOpsMetricReason::Ok,
155        );
156        CanisterOpsMetrics::record(
157            CanisterOpsMetricOperation::Create,
158            &CanisterRole::new("app"),
159            CanisterOpsMetricOutcome::Started,
160            CanisterOpsMetricReason::Ok,
161        );
162        WasmStoreMetrics::record(
163            WasmStoreMetricOperation::SourceResolve,
164            WasmStoreMetricSource::Store,
165            WasmStoreMetricOutcome::Completed,
166            WasmStoreMetricReason::Ok,
167        );
168        WasmStoreMetrics::record(
169            WasmStoreMetricOperation::ChunkUpload,
170            WasmStoreMetricSource::Bootstrap,
171            WasmStoreMetricOutcome::Skipped,
172            WasmStoreMetricReason::CacheHit,
173        );
174        CascadeMetrics::record(
175            CascadeMetricOperation::RootFanout,
176            CascadeMetricSnapshot::Topology,
177            CascadeMetricOutcome::Completed,
178            CascadeMetricReason::Ok,
179        );
180        CascadeMetrics::record(
181            CascadeMetricOperation::ChildSend,
182            CascadeMetricSnapshot::State,
183            CascadeMetricOutcome::Failed,
184            CascadeMetricReason::SendFailed,
185        );
186        DirectoryMetrics::record(
187            DirectoryMetricOperation::Resolve,
188            DirectoryMetricOutcome::Started,
189            DirectoryMetricReason::Ok,
190        );
191        DirectoryMetrics::record(
192            DirectoryMetricOperation::Classify,
193            DirectoryMetricOutcome::Completed,
194            DirectoryMetricReason::PendingFresh,
195        );
196        PoolMetrics::record(
197            PoolMetricOperation::ImportQueued,
198            PoolMetricOutcome::Skipped,
199            PoolMetricReason::AlreadyPresent,
200        );
201        PoolMetrics::record(
202            PoolMetricOperation::CreateEmpty,
203            PoolMetricOutcome::Completed,
204            PoolMetricReason::Ok,
205        );
206        ReplayMetrics::record(
207            ReplayMetricOperation::Reserve,
208            ReplayMetricOutcome::Failed,
209            ReplayMetricReason::Capacity,
210        );
211        ReplayMetrics::record(
212            ReplayMetricOperation::Check,
213            ReplayMetricOutcome::Completed,
214            ReplayMetricReason::Fresh,
215        );
216        ScalingMetrics::record(
217            ScalingMetricOperation::CreateWorker,
218            ScalingMetricOutcome::Completed,
219            ScalingMetricReason::Ok,
220        );
221        ScalingMetrics::record(
222            ScalingMetricOperation::BootstrapPool,
223            ScalingMetricOutcome::Skipped,
224            ScalingMetricReason::TargetSatisfied,
225        );
226
227        assert_first_metric_labels(MetricsKind::CanisterOps, ["create", "app", "started", "ok"]);
228        assert_first_metric_labels(
229            MetricsKind::WasmStore,
230            ["chunk_upload", "bootstrap", "skipped", "cache_hit"],
231        );
232        assert_first_metric_labels(
233            MetricsKind::Cascade,
234            ["child_send", "state", "failed", "send_failed"],
235        );
236        assert_first_metric_labels(
237            MetricsKind::Directory,
238            ["classify", "completed", "pending_fresh"],
239        );
240        assert_first_metric_labels(MetricsKind::Pool, ["create_empty", "completed", "ok"]);
241        assert_first_metric_labels(MetricsKind::Replay, ["check", "completed", "fresh"]);
242        assert_first_metric_labels(
243            MetricsKind::Scaling,
244            ["bootstrap_pool", "skipped", "target_satisfied"],
245        );
246    }
247
248    #[cfg(feature = "sharding")]
249    #[test]
250    fn page_sorts_sharding_metric_family_before_paginating() {
251        metrics::reset_for_tests();
252
253        ShardingMetrics::record(
254            ShardingMetricOperation::PlanAssign,
255            ShardingMetricOutcome::Completed,
256            ShardingMetricReason::ExistingCapacity,
257        );
258        ShardingMetrics::record(
259            ShardingMetricOperation::BootstrapPool,
260            ShardingMetricOutcome::Skipped,
261            ShardingMetricReason::TargetSatisfied,
262        );
263
264        assert_first_metric_labels(
265            MetricsKind::Sharding,
266            ["bootstrap_pool", "skipped", "target_satisfied"],
267        );
268    }
269
270    #[test]
271    fn sample_query_returns_value_and_current_counter() {
272        let sample = MetricsQuery::sample_query("ok");
273
274        assert_eq!(sample.value, "ok");
275        assert_eq!(sample.local_instructions, 0);
276    }
277
278    // Assert that pagination sees the sorted first row for one metric family.
279    fn assert_first_metric_labels<const N: usize>(kind: MetricsKind, expected: [&str; N]) {
280        let page = MetricsQuery::page(
281            kind,
282            PageRequest {
283                limit: 1,
284                offset: 0,
285            },
286        );
287
288        assert_eq!(page.total, 2);
289        assert_eq!(page.entries[0].labels, expected);
290    }
291}