zerodds_corba_ccm_lib/
telemetry.rs1use alloc::boxed::Box;
14use alloc::string::String;
15use alloc::vec::Vec;
16
17use zerodds_corba_ccm::cif::{CifError, ComponentExecutor};
18use zerodds_corba_ccm::context::ComponentContext;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum TelemetryKind {
23 SetContext,
25 Activate,
27 Passivate,
29 Remove,
31 Custom,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct TelemetryEvent {
38 pub sequence: u64,
40 pub kind: TelemetryKind,
42 pub label: String,
44}
45
46pub struct TelemetryComponent {
48 name: String,
49 events: Vec<TelemetryEvent>,
50 seq: u64,
51 activated: bool,
52 ctx: Option<Box<dyn ComponentContext>>,
53}
54
55impl Default for TelemetryComponent {
56 fn default() -> Self {
57 Self::new("anonymous")
58 }
59}
60
61impl core::fmt::Debug for TelemetryComponent {
62 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63 f.debug_struct("TelemetryComponent")
64 .field("name", &self.name)
65 .field("event_count", &self.events.len())
66 .field("activated", &self.activated)
67 .finish_non_exhaustive()
68 }
69}
70
71impl TelemetryComponent {
72 #[must_use]
75 pub fn new(name: &str) -> Self {
76 Self {
77 name: name.into(),
78 events: Vec::new(),
79 seq: 0,
80 activated: false,
81 ctx: None,
82 }
83 }
84
85 fn record(&mut self, kind: TelemetryKind, label: String) {
86 self.seq += 1;
87 self.events.push(TelemetryEvent {
88 sequence: self.seq,
89 kind,
90 label,
91 });
92 }
93
94 pub fn record_custom(&mut self, label: String) {
98 self.record(TelemetryKind::Custom, label);
99 }
100
101 #[must_use]
103 pub fn events(&self) -> &[TelemetryEvent] {
104 &self.events
105 }
106
107 #[must_use]
109 pub fn count_of(&self, kind: TelemetryKind) -> usize {
110 self.events.iter().filter(|e| e.kind == kind).count()
111 }
112
113 #[must_use]
115 pub fn last_event(&self) -> Option<&TelemetryEvent> {
116 self.events.last()
117 }
118
119 pub fn drain(&mut self) -> Vec<TelemetryEvent> {
122 core::mem::take(&mut self.events)
123 }
124
125 #[must_use]
127 pub fn is_active(&self) -> bool {
128 self.activated
129 }
130
131 #[must_use]
133 pub fn name(&self) -> &str {
134 &self.name
135 }
136}
137
138impl ComponentExecutor for TelemetryComponent {
139 fn set_context(&mut self, context: Box<dyn ComponentContext>) {
140 self.ctx = Some(context);
141 let label = self.name.clone();
142 self.record(TelemetryKind::SetContext, label);
143 }
144
145 fn ccm_activate(&mut self) -> Result<(), CifError> {
146 self.activated = true;
147 let label = self.name.clone();
148 self.record(TelemetryKind::Activate, label);
149 Ok(())
150 }
151
152 fn ccm_passivate(&mut self) -> Result<(), CifError> {
153 self.activated = false;
154 let label = self.name.clone();
155 self.record(TelemetryKind::Passivate, label);
156 Ok(())
157 }
158
159 fn ccm_remove(&mut self) -> Result<(), CifError> {
160 self.activated = false;
161 let label = self.name.clone();
162 self.record(TelemetryKind::Remove, label);
163 Ok(())
164 }
165}
166
167#[cfg(test)]
168#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
169mod tests {
170 use super::*;
171
172 struct AnonContext;
173 impl ComponentContext for AnonContext {
174 fn get_caller_principal(&self) -> Option<Vec<u8>> {
175 None
176 }
177 }
178
179 #[test]
180 fn fresh_component_has_no_events() {
181 let t = TelemetryComponent::new("comp");
182 assert!(t.events().is_empty());
183 }
184
185 #[test]
186 fn full_lifecycle_records_four_events() {
187 let mut t = TelemetryComponent::new("comp");
188 t.set_context(Box::new(AnonContext));
189 t.ccm_activate().unwrap();
190 t.ccm_passivate().unwrap();
191 t.ccm_remove().unwrap();
192 assert_eq!(t.events().len(), 4);
193 assert_eq!(t.count_of(TelemetryKind::SetContext), 1);
194 assert_eq!(t.count_of(TelemetryKind::Activate), 1);
195 assert_eq!(t.count_of(TelemetryKind::Passivate), 1);
196 assert_eq!(t.count_of(TelemetryKind::Remove), 1);
197 }
198
199 #[test]
200 fn sequence_numbers_strictly_increase() {
201 let mut t = TelemetryComponent::new("c");
202 t.set_context(Box::new(AnonContext));
203 t.ccm_activate().unwrap();
204 t.ccm_passivate().unwrap();
205 let seqs: alloc::vec::Vec<u64> = t.events().iter().map(|e| e.sequence).collect();
206 assert_eq!(seqs, alloc::vec![1, 2, 3]);
207 }
208
209 #[test]
210 fn record_custom_adds_event() {
211 let mut t = TelemetryComponent::new("c");
212 t.record_custom("user_logged_in".into());
213 assert_eq!(t.events().len(), 1);
214 assert_eq!(t.last_event().unwrap().kind, TelemetryKind::Custom);
215 assert_eq!(t.last_event().unwrap().label, "user_logged_in");
216 }
217
218 #[test]
219 fn drain_empties_buffer() {
220 let mut t = TelemetryComponent::new("c");
221 t.ccm_activate().unwrap();
222 t.ccm_passivate().unwrap();
223 let drained = t.drain();
224 assert_eq!(drained.len(), 2);
225 assert!(t.events().is_empty());
226 }
227
228 #[test]
229 fn label_uses_component_name() {
230 let mut t = TelemetryComponent::new("MyComp");
231 t.ccm_activate().unwrap();
232 assert_eq!(t.last_event().unwrap().label, "MyComp");
233 }
234
235 #[test]
236 fn telemetry_kinds_are_distinct() {
237 for (a, b) in [
238 (TelemetryKind::SetContext, TelemetryKind::Activate),
239 (TelemetryKind::Activate, TelemetryKind::Passivate),
240 (TelemetryKind::Passivate, TelemetryKind::Remove),
241 (TelemetryKind::Remove, TelemetryKind::Custom),
242 ] {
243 assert_ne!(a, b);
244 }
245 }
246
247 #[test]
248 fn activate_then_passivate_toggles_active_flag() {
249 let mut t = TelemetryComponent::new("c");
250 t.ccm_activate().unwrap();
251 assert!(t.is_active());
252 t.ccm_passivate().unwrap();
253 assert!(!t.is_active());
254 }
255}