canic_core/workflow/metrics/
query.rs1use 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
11pub struct MetricsQuery;
19
20impl MetricsQuery {
21 #[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 #[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#[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 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}