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