Skip to main content

zerodds_security_logging/
fanout.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Fan-Out: ein Event an mehrere Backends gleichzeitig.
5//!
6//! zerodds-lint: allow no_dyn_in_safe
7//! (Der Fan-Out haelt eine Liste polymorpher `LoggingPlugin`s via
8//! `Box<dyn ...>` โ€” architektur-bedingt, da der Nutzer verschiedene
9//! Backend-Typen kombinieren koennen will.)
10
11use alloc::boxed::Box;
12use alloc::vec::Vec;
13
14use zerodds_security::logging::{LogLevel, LoggingPlugin};
15
16/// Fan-Out-Adapter โ€” broadcast ein Event an alle eingetragenen
17/// Backends. Nuetzlich fuer Setup mit `stderr` + audit-JSON-File
18/// parallel.
19pub struct FanOutLoggingPlugin {
20    sinks: Vec<Box<dyn LoggingPlugin>>,
21}
22
23impl Default for FanOutLoggingPlugin {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl FanOutLoggingPlugin {
30    /// Leerer Fan-Out โ€” Events werden stillschweigend verworfen.
31    #[must_use]
32    pub fn new() -> Self {
33        Self { sinks: Vec::new() }
34    }
35
36    /// Backend hinzufuegen. Builder-Style.
37    #[must_use]
38    pub fn with<P: LoggingPlugin + 'static>(mut self, sink: P) -> Self {
39        self.sinks.push(Box::new(sink));
40        self
41    }
42
43    /// Backend hinzufuegen per `Box<dyn ...>` (wenn Nutzer schon einen
44    /// Box-Sink hat).
45    #[must_use]
46    pub fn with_boxed(mut self, sink: Box<dyn LoggingPlugin>) -> Self {
47        self.sinks.push(sink);
48        self
49    }
50
51    /// Anzahl registrierter Backends.
52    #[must_use]
53    pub fn sink_count(&self) -> usize {
54        self.sinks.len()
55    }
56}
57
58impl LoggingPlugin for FanOutLoggingPlugin {
59    fn log(&self, level: LogLevel, participant: [u8; 16], category: &str, message: &str) {
60        for sink in &self.sinks {
61            sink.log(level, participant, category, message);
62        }
63    }
64
65    fn plugin_class_id(&self) -> &str {
66        "DDS:Logging:fanout"
67    }
68}
69
70#[cfg(test)]
71#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
72mod tests {
73    use super::*;
74    use std::sync::{Arc, Mutex};
75    use zerodds_security::mock::{MockLogEntry, MockLogSink, MockLoggingPlugin};
76
77    fn sink() -> MockLogSink {
78        Arc::new(Mutex::new(Vec::<MockLogEntry>::new()))
79    }
80
81    #[test]
82    fn empty_fanout_drops_events_without_panic() {
83        let f = FanOutLoggingPlugin::new();
84        f.log(LogLevel::Error, [0u8; 16], "cat", "msg");
85        assert_eq!(f.sink_count(), 0);
86    }
87
88    #[test]
89    fn event_reaches_all_registered_sinks() {
90        let s1 = sink();
91        let s2 = sink();
92        let f = FanOutLoggingPlugin::new()
93            .with(MockLoggingPlugin::new(Arc::clone(&s1)))
94            .with(MockLoggingPlugin::new(Arc::clone(&s2)));
95        f.log(LogLevel::Error, [0xAA; 16], "auth.bad", "nope");
96        assert_eq!(s1.lock().unwrap().len(), 1);
97        assert_eq!(s2.lock().unwrap().len(), 1);
98        assert_eq!(s1.lock().unwrap()[0].category, "auth.bad");
99    }
100
101    #[test]
102    fn plugin_class_id_stable() {
103        assert_eq!(
104            FanOutLoggingPlugin::new().plugin_class_id(),
105            "DDS:Logging:fanout"
106        );
107    }
108}