Skip to main content

icydb_core/metrics/state/
report.rs

1//! Module: metrics::state::report
2//! Responsibility: rich metrics report DTOs and windowed report construction.
3//! Does not own: mutable metrics state updates or compact metrics reports.
4//! Boundary: keeps rich endpoint payload construction separate from raw state.
5
6use candid::CandidType;
7use serde::Deserialize;
8
9use crate::runtime::now_millis;
10
11use super::{EntitySummary, EventOps, EventPerf, entity_summary_from_counters, with_state};
12
13#[cfg_attr(doc, doc = "EventReport\n\nMetrics query payload.")]
14#[derive(CandidType, Clone, Debug, Default, Deserialize)]
15pub struct EventReport {
16    counters: Option<EventCounters>,
17    entity_counters: Vec<EntitySummary>,
18    window_filter_matched: bool,
19    requested_window_start_ms: Option<u64>,
20    active_window_start_ms: u64,
21}
22
23impl EventReport {
24    #[must_use]
25    const fn new(
26        counters: Option<EventCounters>,
27        entity_counters: Vec<EntitySummary>,
28        window_filter_matched: bool,
29        requested_window_start_ms: Option<u64>,
30        active_window_start_ms: u64,
31    ) -> Self {
32        Self {
33            counters,
34            entity_counters,
35            window_filter_matched,
36            requested_window_start_ms,
37            active_window_start_ms,
38        }
39    }
40
41    #[must_use]
42    pub const fn counters(&self) -> Option<&EventCounters> {
43        self.counters.as_ref()
44    }
45
46    #[must_use]
47    pub fn entity_counters(&self) -> &[EntitySummary] {
48        &self.entity_counters
49    }
50
51    #[must_use]
52    pub const fn window_filter_matched(&self) -> bool {
53        self.window_filter_matched
54    }
55
56    #[must_use]
57    pub const fn requested_window_start_ms(&self) -> Option<u64> {
58        self.requested_window_start_ms
59    }
60
61    #[must_use]
62    pub const fn active_window_start_ms(&self) -> u64 {
63        self.active_window_start_ms
64    }
65
66    #[must_use]
67    pub fn into_counters(self) -> Option<EventCounters> {
68        self.counters
69    }
70
71    #[must_use]
72    pub fn into_entity_counters(self) -> Vec<EntitySummary> {
73        self.entity_counters
74    }
75}
76
77//
78// EventCounters
79//
80// Top-level metrics counters returned by the generated metrics endpoint.
81// This keeps aggregate ops/perf totals while leaving per-entity detail to the
82// separate `entity_counters` payload.
83//
84
85#[derive(CandidType, Clone, Debug, Default, Deserialize)]
86pub struct EventCounters {
87    pub(crate) ops: EventOps,
88    pub(crate) perf: EventPerf,
89    pub(crate) window_start_ms: u64,
90    pub(crate) window_end_ms: u64,
91    pub(crate) window_duration_ms: u64,
92}
93
94impl EventCounters {
95    #[must_use]
96    const fn new(ops: EventOps, perf: EventPerf, window_start_ms: u64, window_end_ms: u64) -> Self {
97        Self {
98            ops,
99            perf,
100            window_start_ms,
101            window_end_ms,
102            window_duration_ms: window_end_ms.saturating_sub(window_start_ms),
103        }
104    }
105
106    #[must_use]
107    pub const fn ops(&self) -> &EventOps {
108        &self.ops
109    }
110
111    #[must_use]
112    pub const fn perf(&self) -> &EventPerf {
113        &self.perf
114    }
115
116    #[must_use]
117    pub const fn window_start_ms(&self) -> u64 {
118        self.window_start_ms
119    }
120
121    #[must_use]
122    pub const fn window_end_ms(&self) -> u64 {
123        self.window_end_ms
124    }
125
126    #[must_use]
127    pub const fn window_duration_ms(&self) -> u64 {
128        self.window_duration_ms
129    }
130}
131
132// Build a metrics report gated by `window_start_ms`.
133//
134// This is a window-start filter:
135// - If `window_start_ms` is `None`, return the current window.
136// - If `window_start_ms <= state.window_start_ms`, return the current window.
137// - If `window_start_ms > state.window_start_ms`, return an empty report.
138//
139// IcyDB stores aggregate counters only, so it cannot produce a precise
140// sub-window report after `state.window_start_ms`.
141#[must_use]
142pub(in crate::metrics) fn report_window_start(window_start_ms: Option<u64>) -> EventReport {
143    let snap = with_state(Clone::clone);
144    if let Some(requested_window_start_ms) = window_start_ms
145        && requested_window_start_ms > snap.window_start_ms
146    {
147        return EventReport::new(
148            None,
149            Vec::new(),
150            false,
151            window_start_ms,
152            snap.window_start_ms,
153        );
154    }
155
156    let mut entity_counters: Vec<EntitySummary> = Vec::new();
157    for (path, ops) in &snap.entities {
158        entity_counters.push(entity_summary_from_counters(path, ops));
159    }
160
161    entity_counters.sort_by(|a, b| {
162        b.activity_score()
163            .cmp(&a.activity_score())
164            .then_with(|| b.rows_loaded().cmp(&a.rows_loaded()))
165            .then_with(|| b.rows_saved().cmp(&a.rows_saved()))
166            .then_with(|| b.rows_scanned().cmp(&a.rows_scanned()))
167            .then_with(|| b.rows_deleted().cmp(&a.rows_deleted()))
168            .then_with(|| a.path().cmp(b.path()))
169    });
170
171    EventReport::new(
172        Some(EventCounters::new(
173            snap.ops.clone(),
174            snap.perf.clone(),
175            snap.window_start_ms,
176            now_millis(),
177        )),
178        entity_counters,
179        true,
180        window_start_ms,
181        snap.window_start_ms,
182    )
183}