use alloc::string::String;
use alloc::vec::Vec;
use std::net::{SocketAddr, UdpSocket};
use std::sync::Mutex;
use zerodds_security::logging::{LogLevel, LoggingPlugin};
const FACILITY_LOCAL0: u8 = 16;
fn severity_code(l: LogLevel) -> u8 {
l as u8
}
fn priority(level: LogLevel) -> u8 {
FACILITY_LOCAL0 * 8 + severity_code(level)
}
pub struct SyslogLoggingPlugin {
min_level: LogLevel,
socket: Mutex<UdpSocket>,
target: SocketAddr,
app_name: String,
hostname: String,
}
impl SyslogLoggingPlugin {
pub fn connect(
target: SocketAddr,
app_name: impl Into<String>,
hostname: impl Into<String>,
min_level: LogLevel,
) -> std::io::Result<Self> {
let socket = UdpSocket::bind("0.0.0.0:0")?;
Ok(Self {
min_level,
socket: Mutex::new(socket),
target,
app_name: app_name.into(),
hostname: hostname.into(),
})
}
}
fn hex16(bytes: [u8; 16]) -> String {
let mut s = String::with_capacity(32);
for b in bytes {
s.push_str(&alloc::format!("{b:02x}"));
}
s
}
fn escape_msg(s: &str) -> String {
s.chars()
.map(|c| if c == '\r' || c == '\n' { ' ' } else { c })
.collect()
}
fn build_line(
level: LogLevel,
participant: [u8; 16],
category: &str,
message: &str,
app_name: &str,
hostname: &str,
) -> Vec<u8> {
let pri = priority(level);
let line = alloc::format!(
"<{pri}>1 - {host} {app} - {cat} - participant={pid} {msg}",
host = if hostname.is_empty() { "-" } else { hostname },
app = if app_name.is_empty() { "-" } else { app_name },
cat = if category.is_empty() { "-" } else { category },
pid = hex16(participant),
msg = escape_msg(message),
);
line.into_bytes()
}
impl LoggingPlugin for SyslogLoggingPlugin {
fn log(&self, level: LogLevel, participant: [u8; 16], category: &str, message: &str) {
if level > self.min_level {
return;
}
let line = build_line(
level,
participant,
category,
message,
&self.app_name,
&self.hostname,
);
if let Ok(socket) = self.socket.lock() {
let _ = socket.send_to(&line, self.target);
}
}
fn plugin_class_id(&self) -> &str {
"DDS:Logging:syslog"
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use std::net::{SocketAddr, UdpSocket};
use std::time::Duration;
#[test]
fn priority_combines_facility_and_severity() {
assert_eq!(priority(LogLevel::Critical), 130);
assert_eq!(priority(LogLevel::Emergency), 128);
}
#[test]
fn build_line_rfc5424_shape() {
let line = build_line(
LogLevel::Warning,
[0xAB; 16],
"auth.fail",
"bad cert",
"zerodds",
"host1",
);
let s = std::str::from_utf8(&line).unwrap();
assert!(s.starts_with("<132>1 "), "PRI(132) + version(1), got {s}");
assert!(s.contains("host1"));
assert!(s.contains("zerodds"));
assert!(s.contains("auth.fail"));
assert!(s.contains("participant=abababab"));
assert!(s.ends_with("bad cert"));
}
#[test]
fn build_line_escapes_newlines() {
let line = build_line(
LogLevel::Error,
[0u8; 16],
"cat",
"multi\nline\rmsg",
"app",
"host",
);
let s = std::str::from_utf8(&line).unwrap();
assert!(!s.contains('\n'));
assert!(!s.contains('\r'));
}
#[test]
fn roundtrip_udp_receives_formatted_line() {
let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
receiver
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
let addr: SocketAddr = receiver.local_addr().unwrap();
let plugin =
SyslogLoggingPlugin::connect(addr, "zerodds", "test-host", LogLevel::Debug).unwrap();
plugin.log(
LogLevel::Critical,
[0x42; 16],
"audit.event",
"something happened",
);
let mut buf = [0u8; 1024];
let (n, _) = receiver.recv_from(&mut buf).expect("udp recv timeout");
let got = std::str::from_utf8(&buf[..n]).unwrap();
assert!(got.contains("audit.event"));
assert!(got.contains("participant=42424242"));
assert!(got.contains("something happened"));
}
#[test]
fn below_threshold_events_are_dropped_silently() {
let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
receiver
.set_read_timeout(Some(Duration::from_millis(200)))
.unwrap();
let addr: SocketAddr = receiver.local_addr().unwrap();
let plugin = SyslogLoggingPlugin::connect(addr, "app", "host", LogLevel::Error).unwrap();
plugin.log(LogLevel::Informational, [0u8; 16], "cat", "ignored");
let mut buf = [0u8; 1024];
assert!(
receiver.recv_from(&mut buf).is_err(),
"unter min_level muss nix gesendet werden"
);
}
#[test]
fn plugin_class_id_stable() {
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let plugin = SyslogLoggingPlugin::connect(addr, "x", "y", LogLevel::Debug).unwrap();
assert_eq!(plugin.plugin_class_id(), "DDS:Logging:syslog");
}
}