Skip to main content

zerodds_security_logging/
stderr_sink.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Stderr-Backend.
5
6use alloc::string::String;
7use std::io::{self, Write};
8use std::sync::Mutex;
9
10use zerodds_security::logging::{LogLevel, LoggingPlugin};
11
12/// Loggt Security-Events nach `stderr` als Human-Readable-Text.
13///
14/// Format:
15/// ```text
16/// [SEC][<LEVEL>] participant=<hex16> category=<...> msg=<...>
17/// ```
18///
19/// Mutex um `io::Stderr` serialisiert Writes — sonst koennten parallele
20/// Writer die Zeilen ineinander mischen (stderr ist line-buffered
21/// **nur** wenn es ans Terminal geht; in der Pipeline ist es
22/// fully-buffered bzw. per write(2) atomisch nur bis PIPE_BUF Bytes).
23pub struct StderrLoggingPlugin {
24    min_level: LogLevel,
25    serializer: Mutex<()>,
26}
27
28impl Default for StderrLoggingPlugin {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl StderrLoggingPlugin {
35    /// Konstruktor mit Default-Level `Warning`.
36    #[must_use]
37    pub fn new() -> Self {
38        Self::with_level(LogLevel::Warning)
39    }
40
41    /// Mit explizitem Min-Level.
42    #[must_use]
43    pub fn with_level(min_level: LogLevel) -> Self {
44        Self {
45            min_level,
46            serializer: Mutex::new(()),
47        }
48    }
49}
50
51fn level_label(l: LogLevel) -> &'static str {
52    match l {
53        LogLevel::Emergency => "EMERG",
54        LogLevel::Alert => "ALERT",
55        LogLevel::Critical => "CRIT",
56        LogLevel::Error => "ERROR",
57        LogLevel::Warning => "WARN",
58        LogLevel::Notice => "NOTICE",
59        LogLevel::Informational => "INFO",
60        LogLevel::Debug => "DEBUG",
61    }
62}
63
64fn hex16(bytes: [u8; 16]) -> String {
65    let mut s = String::with_capacity(32);
66    for b in bytes {
67        s.push_str(&alloc::format!("{b:02x}"));
68    }
69    s
70}
71
72impl LoggingPlugin for StderrLoggingPlugin {
73    fn log(&self, level: LogLevel, participant: [u8; 16], category: &str, message: &str) {
74        // `level <= min_level` weil LogLevel 0=Emergency und 7=Debug.
75        if level > self.min_level {
76            return;
77        }
78        let _guard = self.serializer.lock().ok();
79        let mut out = io::stderr().lock();
80        let _ = writeln!(
81            out,
82            "[SEC][{level}] participant={pid} category={cat} msg={msg}",
83            level = level_label(level),
84            pid = hex16(participant),
85            cat = category,
86            msg = message,
87        );
88    }
89
90    fn plugin_class_id(&self) -> &str {
91        "DDS:Logging:stderr"
92    }
93}
94
95#[cfg(test)]
96#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn plugin_class_id_stable() {
102        assert_eq!(
103            StderrLoggingPlugin::new().plugin_class_id(),
104            "DDS:Logging:stderr"
105        );
106    }
107
108    #[test]
109    fn level_label_covers_all_variants() {
110        for &lvl in &[
111            LogLevel::Emergency,
112            LogLevel::Alert,
113            LogLevel::Critical,
114            LogLevel::Error,
115            LogLevel::Warning,
116            LogLevel::Notice,
117            LogLevel::Informational,
118            LogLevel::Debug,
119        ] {
120            assert!(!level_label(lvl).is_empty());
121        }
122    }
123
124    #[test]
125    fn hex16_pads_single_digits() {
126        let bytes = [0x01, 0x0a, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
127        assert_eq!(hex16(bytes), "010aff00000000000000000000000000");
128    }
129}