Skip to main content

khive_storage/
telemetry.rs

1//! Typed payload structs for the ADR-094 lifecycle telemetry events.
2//!
3//! These are a documentation/test convenience only: the persisted
4//! discriminant on a stored [`crate::Event`] remains `khive_types::EventKind`,
5//! and each payload here is serialized into that event's JSON `payload`
6//! field by the emitting call site. Nothing here changes storage schema.
7
8use serde::{Deserialize, Serialize};
9
10/// Mirrors the eight ADR-094 lifecycle `EventKind` variants plus the three
11/// ADR-103 Stage 1 phase-span variants. Not itself persisted — see the
12/// module docs.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum LifecycleEvent {
16    ChannelPollStarted,
17    ChannelPollSucceeded,
18    ChannelPollFailed,
19    ChannelBackoffArmed,
20    ChannelBackoffReset,
21    ChannelHeartbeatPersistFailed,
22    ConfigLocked,
23    CheckpointOutcomeRecorded,
24    PhaseStarted,
25    PhaseCompleted,
26    PhaseCancelled,
27}
28
29/// Payload for [`khive_types::EventKind::ChannelPollStarted`].
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct ChannelPollStartedPayload {
32    pub channel_kind: String,
33    pub channel_slug: String,
34    pub since_rfc3339: String,
35}
36
37/// Payload for [`khive_types::EventKind::ChannelPollSucceeded`].
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct ChannelPollSucceededPayload {
40    pub channel_kind: String,
41    pub channel_slug: String,
42    pub envelope_count: usize,
43    pub previous_backoff_attempt: u32,
44}
45
46/// Payload for [`khive_types::EventKind::ChannelPollFailed`].
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct ChannelPollFailedPayload {
49    pub channel_kind: String,
50    pub channel_slug: String,
51    pub error_class: String,
52    pub error_message: String,
53}
54
55/// Payload for [`khive_types::EventKind::ChannelBackoffArmed`].
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct ChannelBackoffArmedPayload {
58    pub channel_kind: String,
59    pub channel_slug: String,
60    pub attempt: u32,
61    pub step_ms: u64,
62    pub delay_ms: u64,
63}
64
65/// Payload for [`khive_types::EventKind::ChannelBackoffReset`].
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct ChannelBackoffResetPayload {
68    pub channel_kind: String,
69    pub channel_slug: String,
70    pub previous_backoff_attempt: u32,
71}
72
73/// Payload for [`khive_types::EventKind::ChannelHeartbeatPersistFailed`].
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct ChannelHeartbeatPersistFailedPayload {
76    pub channel_kind: String,
77    pub channel_slug: String,
78    pub error: String,
79}
80
81/// Payload for [`khive_types::EventKind::ConfigLocked`].
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct ConfigLockedPayload {
84    pub key: String,
85    pub value: String,
86}
87
88/// Payload for [`khive_types::EventKind::CheckpointOutcomeRecorded`].
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct CheckpointOutcomeRecordedPayload {
91    pub wal_pages: u64,
92    pub warn_pages: u64,
93    pub high_water_pages: u64,
94    pub truncate_high_water_pages: u64,
95    pub above_warn: bool,
96    pub above_high_water: bool,
97    pub above_truncate_high_water: bool,
98}
99
100/// Payload for [`khive_types::EventKind::PhaseStarted`] (ADR-103 Stage 1).
101///
102/// `work_class` is the closed ADR-103 enum (`interactive` | `warm` |
103/// `maintenance` | `inference`), carried as a plain string here since the
104/// enum itself is defined in a downstream crate; producers are responsible
105/// for using the closed set of values. `corpus_size` is populated only when
106/// it is cheaply known at phase start (e.g. a corpus count already on hand);
107/// `None` when unknown at this point.
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct PhaseStartedPayload {
110    pub work_class: String,
111    pub phase: String,
112    pub corpus_size: Option<u64>,
113}
114
115/// Payload for [`khive_types::EventKind::PhaseCompleted`] (ADR-103 Stage 1).
116///
117/// `cpu_us` is a process-level `getrusage` delta across the phase (see
118/// `khive_runtime::resource`), not a per-thread measurement — `None` when
119/// the underlying read is unavailable on this platform.
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct PhaseCompletedPayload {
122    pub work_class: String,
123    pub phase: String,
124    pub wall_us: i64,
125    pub cpu_us: Option<i64>,
126}
127
128/// Payload for [`khive_types::EventKind::PhaseCancelled`] (ADR-103 Stage 1).
129/// Same shape as [`PhaseCompletedPayload`] — the phase ran for `wall_us`
130/// before being cut short rather than returning a result.
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct PhaseCancelledPayload {
133    pub work_class: String,
134    pub phase: String,
135    pub wall_us: i64,
136    pub cpu_us: Option<i64>,
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn lifecycle_event_roundtrips_through_json() {
145        for kind in [
146            LifecycleEvent::ChannelPollStarted,
147            LifecycleEvent::ChannelPollSucceeded,
148            LifecycleEvent::ChannelPollFailed,
149            LifecycleEvent::ChannelBackoffArmed,
150            LifecycleEvent::ChannelBackoffReset,
151            LifecycleEvent::ChannelHeartbeatPersistFailed,
152            LifecycleEvent::ConfigLocked,
153            LifecycleEvent::CheckpointOutcomeRecorded,
154            LifecycleEvent::PhaseStarted,
155            LifecycleEvent::PhaseCompleted,
156            LifecycleEvent::PhaseCancelled,
157        ] {
158            let json = serde_json::to_string(&kind).expect("serialize");
159            let parsed: LifecycleEvent = serde_json::from_str(&json).expect("deserialize");
160            assert_eq!(parsed, kind);
161        }
162    }
163
164    #[test]
165    fn checkpoint_outcome_recorded_payload_roundtrips() {
166        let payload = CheckpointOutcomeRecordedPayload {
167            wal_pages: 2500,
168            warn_pages: 2000,
169            high_water_pages: 6000,
170            truncate_high_water_pages: 20_000,
171            above_warn: true,
172            above_high_water: false,
173            above_truncate_high_water: false,
174        };
175        let json = serde_json::to_value(&payload).expect("serialize");
176        let parsed: CheckpointOutcomeRecordedPayload =
177            serde_json::from_value(json).expect("deserialize");
178        assert_eq!(parsed, payload);
179    }
180
181    #[test]
182    fn phase_started_payload_roundtrips() {
183        let payload = PhaseStartedPayload {
184            work_class: "warm".into(),
185            phase: "ann_warm".into(),
186            corpus_size: Some(553_000),
187        };
188        let json = serde_json::to_value(&payload).expect("serialize");
189        let parsed: PhaseStartedPayload = serde_json::from_value(json).expect("deserialize");
190        assert_eq!(parsed, payload);
191    }
192
193    #[test]
194    fn phase_completed_payload_roundtrips_with_absent_cpu_us() {
195        let payload = PhaseCompletedPayload {
196            work_class: "warm".into(),
197            phase: "ann_warm".into(),
198            wall_us: 41_000_000,
199            cpu_us: None,
200        };
201        let json = serde_json::to_value(&payload).expect("serialize");
202        let parsed: PhaseCompletedPayload = serde_json::from_value(json).expect("deserialize");
203        assert_eq!(parsed, payload);
204    }
205
206    #[test]
207    fn phase_cancelled_payload_roundtrips() {
208        let payload = PhaseCancelledPayload {
209            work_class: "warm".into(),
210            phase: "ann_warm".into(),
211            wall_us: 12_000,
212            cpu_us: Some(9_500),
213        };
214        let json = serde_json::to_value(&payload).expect("serialize");
215        let parsed: PhaseCancelledPayload = serde_json::from_value(json).expect("deserialize");
216        assert_eq!(parsed, payload);
217    }
218}