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            wasm_store::{
61                WasmStoreMetricOperation, WasmStoreMetricOutcome, WasmStoreMetricReason,
62                WasmStoreMetricSource, WasmStoreMetrics,
63            },
64        },
65    };
66
67    #[test]
68    fn page_sorts_metric_rows_before_paginating() {
69        metrics::reset_for_tests();
70
71        AccessMetrics::increment("zeta", AccessMetricKind::Auth, "caller_is_root");
72        AccessMetrics::increment("alpha", AccessMetricKind::Guard, "app_allows_updates");
73
74        let page = MetricsQuery::page(
75            MetricsKind::Access,
76            PageRequest {
77                limit: 1,
78                offset: 0,
79            },
80        );
81
82        assert_eq!(page.total, 2);
83        assert_eq!(
84            page.entries[0].labels,
85            ["alpha", "guard", "app_allows_updates"]
86        );
87
88        let page = MetricsQuery::page(
89            MetricsKind::Access,
90            PageRequest {
91                limit: 1,
92                offset: 1,
93            },
94        );
95
96        assert_eq!(page.total, 2);
97        assert_eq!(page.entries[0].labels, ["zeta", "auth", "caller_is_root"]);
98    }
99
100    #[test]
101    fn page_sorts_new_multi_label_metric_families_before_paginating() {
102        metrics::reset_for_tests();
103
104        CanisterOpsMetrics::record(
105            CanisterOpsMetricOperation::Upgrade,
106            &CanisterRole::new("worker"),
107            CanisterOpsMetricOutcome::Completed,
108            CanisterOpsMetricReason::Ok,
109        );
110        CanisterOpsMetrics::record(
111            CanisterOpsMetricOperation::Create,
112            &CanisterRole::new("app"),
113            CanisterOpsMetricOutcome::Started,
114            CanisterOpsMetricReason::Ok,
115        );
116        WasmStoreMetrics::record(
117            WasmStoreMetricOperation::SourceResolve,
118            WasmStoreMetricSource::Store,
119            WasmStoreMetricOutcome::Completed,
120            WasmStoreMetricReason::Ok,
121        );
122        WasmStoreMetrics::record(
123            WasmStoreMetricOperation::ChunkUpload,
124            WasmStoreMetricSource::Bootstrap,
125            WasmStoreMetricOutcome::Skipped,
126            WasmStoreMetricReason::CacheHit,
127        );
128
129        let page = MetricsQuery::page(
130            MetricsKind::CanisterOps,
131            PageRequest {
132                limit: 1,
133                offset: 0,
134            },
135        );
136
137        assert_eq!(page.total, 2);
138        assert_eq!(page.entries[0].labels, ["create", "app", "started", "ok"]);
139
140        let page = MetricsQuery::page(
141            MetricsKind::WasmStore,
142            PageRequest {
143                limit: 1,
144                offset: 0,
145            },
146        );
147
148        assert_eq!(page.total, 2);
149        assert_eq!(
150            page.entries[0].labels,
151            ["chunk_upload", "bootstrap", "skipped", "cache_hit"]
152        );
153    }
154
155    #[test]
156    fn sample_query_returns_value_and_current_counter() {
157        let sample = MetricsQuery::sample_query("ok");
158
159        assert_eq!(sample.value, "ok");
160        assert_eq!(sample.local_instructions, 0);
161    }
162}