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