//! Logging configuration for telemt. //! //! Supports multiple log destinations: //! - stderr (default, works with systemd journald) //! - syslog (Unix only, for traditional init systems) //! - file (with optional rotation) #![allow(dead_code)] // Infrastructure module - used via CLI flags use std::path::Path; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, fmt, reload}; /// Log destination configuration. #[derive(Debug, Clone, Default)] pub enum LogDestination { /// Log to stderr (default, captured by systemd journald). #[default] Stderr, /// Log to syslog (Unix only). #[cfg(unix)] Syslog, /// Log to a file with optional rotation. File { path: String, /// Rotate daily if true. rotate_daily: bool, }, } /// Logging options parsed from CLI/config. #[derive(Debug, Clone, Default)] pub struct LoggingOptions { /// Where to send logs. pub destination: LogDestination, /// Disable ANSI colors. pub disable_colors: bool, } /// Guard that must be held to keep file logging active. /// When dropped, flushes and closes log files. pub struct LoggingGuard { _guard: Option, } impl LoggingGuard { fn new(guard: Option) -> Self { Self { _guard: guard } } /// Creates a no-op guard for stderr/syslog logging. pub fn noop() -> Self { Self { _guard: None } } } /// Initialize the tracing subscriber with the specified options. /// /// Returns a reload handle for dynamic log level changes and a guard /// that must be kept alive for file logging. pub fn init_logging( opts: &LoggingOptions, initial_filter: &str, ) -> ( reload::Handle, LoggingGuard, ) { let (filter_layer, filter_handle) = reload::Layer::new(EnvFilter::new(initial_filter)); match &opts.destination { LogDestination::Stderr => { let fmt_layer = fmt::Layer::default() .with_ansi(!opts.disable_colors) .with_target(true); tracing_subscriber::registry() .with(filter_layer) .with(fmt_layer) .init(); (filter_handle, LoggingGuard::noop()) } #[cfg(unix)] LogDestination::Syslog => { // Use a custom fmt layer that writes to syslog let fmt_layer = fmt::Layer::default() .with_ansi(false) .with_target(false) .with_level(false) .without_time() .with_writer(SyslogMakeWriter::new()); tracing_subscriber::registry() .with(filter_layer) .with(fmt_layer) .init(); (filter_handle, LoggingGuard::noop()) } LogDestination::File { path, rotate_daily } => { let (non_blocking, guard) = if *rotate_daily { // Extract directory and filename prefix let path = Path::new(path); let dir = path.parent().unwrap_or(Path::new("/var/log")); let prefix = path .file_name() .and_then(|s| s.to_str()) .unwrap_or("telemt"); let file_appender = tracing_appender::rolling::daily(dir, prefix); tracing_appender::non_blocking(file_appender) } else { let file = std::fs::OpenOptions::new() .create(true) .append(true) .open(path) .expect("Failed to open log file"); tracing_appender::non_blocking(file) }; let fmt_layer = fmt::Layer::default() .with_ansi(false) .with_target(true) .with_writer(non_blocking); tracing_subscriber::registry() .with(filter_layer) .with(fmt_layer) .init(); (filter_handle, LoggingGuard::new(Some(guard))) } } } /// Syslog writer for tracing. #[cfg(unix)] #[derive(Clone, Copy)] struct SyslogMakeWriter; #[cfg(unix)] #[derive(Clone, Copy)] struct SyslogWriter { priority: libc::c_int, } #[cfg(unix)] impl SyslogMakeWriter { fn new() -> Self { // Open syslog connection on first use static INIT: std::sync::Once = std::sync::Once::new(); INIT.call_once(|| { unsafe { // Open syslog with ident "telemt", LOG_PID, LOG_DAEMON facility let ident = b"telemt\0".as_ptr() as *const libc::c_char; libc::openlog(ident, libc::LOG_PID | libc::LOG_NDELAY, libc::LOG_DAEMON); } }); Self } } #[cfg(unix)] fn syslog_priority_for_level(level: &tracing::Level) -> libc::c_int { match *level { tracing::Level::ERROR => libc::LOG_ERR, tracing::Level::WARN => libc::LOG_WARNING, tracing::Level::INFO => libc::LOG_INFO, tracing::Level::DEBUG => libc::LOG_DEBUG, tracing::Level::TRACE => libc::LOG_DEBUG, } } #[cfg(unix)] impl std::io::Write for SyslogWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { // Convert to C string, stripping newlines let msg = String::from_utf8_lossy(buf); let msg = msg.trim_end(); if msg.is_empty() { return Ok(buf.len()); } // Write to syslog let c_msg = std::ffi::CString::new(msg.as_bytes()) .unwrap_or_else(|_| std::ffi::CString::new("(invalid utf8)").unwrap()); unsafe { libc::syslog( self.priority, b"%s\0".as_ptr() as *const libc::c_char, c_msg.as_ptr(), ); } Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } #[cfg(unix)] impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SyslogMakeWriter { type Writer = SyslogWriter; fn make_writer(&'a self) -> Self::Writer { SyslogWriter { priority: libc::LOG_INFO, } } fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer { SyslogWriter { priority: syslog_priority_for_level(meta.level()), } } } /// Parse log destination from CLI arguments. pub fn parse_log_destination(args: &[String]) -> LogDestination { let mut i = 0; while i < args.len() { match args[i].as_str() { #[cfg(unix)] "--syslog" => { return LogDestination::Syslog; } "--log-file" => { i += 1; if i < args.len() { return LogDestination::File { path: args[i].clone(), rotate_daily: false, }; } } s if s.starts_with("--log-file=") => { return LogDestination::File { path: s.trim_start_matches("--log-file=").to_string(), rotate_daily: false, }; } "--log-file-daily" => { i += 1; if i < args.len() { return LogDestination::File { path: args[i].clone(), rotate_daily: true, }; } } s if s.starts_with("--log-file-daily=") => { return LogDestination::File { path: s.trim_start_matches("--log-file-daily=").to_string(), rotate_daily: true, }; } _ => {} } i += 1; } LogDestination::Stderr } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_log_destination_default() { let args: Vec = vec![]; assert!(matches!( parse_log_destination(&args), LogDestination::Stderr )); } #[test] fn test_parse_log_destination_file() { let args = vec!["--log-file".to_string(), "/var/log/telemt.log".to_string()]; match parse_log_destination(&args) { LogDestination::File { path, rotate_daily } => { assert_eq!(path, "/var/log/telemt.log"); assert!(!rotate_daily); } _ => panic!("Expected File destination"), } } #[test] fn test_parse_log_destination_file_daily() { let args = vec!["--log-file-daily=/var/log/telemt".to_string()]; match parse_log_destination(&args) { LogDestination::File { path, rotate_daily } => { assert_eq!(path, "/var/log/telemt"); assert!(rotate_daily); } _ => panic!("Expected File destination"), } } #[cfg(unix)] #[test] fn test_parse_log_destination_syslog() { let args = vec!["--syslog".to_string()]; assert!(matches!( parse_log_destination(&args), LogDestination::Syslog )); } #[cfg(unix)] #[test] fn test_syslog_priority_for_level_mapping() { assert_eq!(syslog_priority_for_level(&tracing::Level::ERROR), libc::LOG_ERR); assert_eq!( syslog_priority_for_level(&tracing::Level::WARN), libc::LOG_WARNING ); assert_eq!(syslog_priority_for_level(&tracing::Level::INFO), libc::LOG_INFO); assert_eq!( syslog_priority_for_level(&tracing::Level::DEBUG), libc::LOG_DEBUG ); assert_eq!( syslog_priority_for_level(&tracing::Level::TRACE), libc::LOG_DEBUG ); } }