Skip to main content

zerodds_corba_ccm_lib/
telemetry.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! TelemetryComponent — Component-Lifecycle-Metrik-Emitter.
5//!
6//! Production-CCM-Containers brauchen Beobachtbarkeit: jede
7//! Lifecycle-Transition (`set_context`, `ccm_activate`,
8//! `ccm_passivate`, `ccm_remove`) wird als Event in einen DCPS-Topic
9//! `__ZeroDDS_CcmTelemetry` publiziert. Diese Component liefert das
10//! In-Process-Sample-Modell und Lifecycle-Hooks; die Topic-Publikation
11//! ist Aufgabe der DDS-Runtime.
12
13use 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/// Kind eines Telemetry-Events.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum TelemetryKind {
23    /// `set_context` aufgerufen.
24    SetContext,
25    /// `ccm_activate` aufgerufen.
26    Activate,
27    /// `ccm_passivate` aufgerufen.
28    Passivate,
29    /// `ccm_remove` aufgerufen.
30    Remove,
31    /// User-defined Event (Caller emittiert via `record_custom`).
32    Custom,
33}
34
35/// Telemetry-Event-Sample.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct TelemetryEvent {
38    /// Aufsteigende Sequenz-Nummer (start bei 1).
39    pub sequence: u64,
40    /// Kind.
41    pub kind: TelemetryKind,
42    /// Frei-Format-Label (z.B. Component-Name oder Custom-Tag).
43    pub label: String,
44}
45
46/// TelemetryComponent — production-ready CCM-Component.
47pub 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    /// Konstruktor mit Component-Name (wird als Label-Prefix
73    /// verwendet).
74    #[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    /// Records einen Custom-Event. Caller-Layer (z.B.
95    /// Component-Logic) ruft das auf, um App-spezifische Telemetrie
96    /// zu emittieren.
97    pub fn record_custom(&mut self, label: String) {
98        self.record(TelemetryKind::Custom, label);
99    }
100
101    /// Liste aller bisher emittierten Events (in Reihenfolge).
102    #[must_use]
103    pub fn events(&self) -> &[TelemetryEvent] {
104        &self.events
105    }
106
107    /// Anzahl Events einer bestimmten Kind.
108    #[must_use]
109    pub fn count_of(&self, kind: TelemetryKind) -> usize {
110        self.events.iter().filter(|e| e.kind == kind).count()
111    }
112
113    /// Letzte Event (None wenn leer).
114    #[must_use]
115    pub fn last_event(&self) -> Option<&TelemetryEvent> {
116        self.events.last()
117    }
118
119    /// Drain — leere die Event-Queue und gib sie zurueck. Nuetzlich
120    /// wenn die DDS-Runtime den Buffer ausliest.
121    pub fn drain(&mut self) -> Vec<TelemetryEvent> {
122        core::mem::take(&mut self.events)
123    }
124
125    /// Liefert `true` wenn aktiviert.
126    #[must_use]
127    pub fn is_active(&self) -> bool {
128        self.activated
129    }
130
131    /// Component-Name.
132    #[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}