//! 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(true) .with_writer(SyslogWriter::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)] struct SyslogWriter { _private: (), } #[cfg(unix)] impl SyslogWriter { 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 { _private: () } } } #[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()); } // Determine priority based on log level in the message let priority = if msg.contains(" ERROR ") || msg.contains(" error ") { libc::LOG_ERR } else if msg.contains(" WARN ") || msg.contains(" warn ") { libc::LOG_WARNING } else if msg.contains(" INFO ") || msg.contains(" info ") { libc::LOG_INFO } else if msg.contains(" DEBUG ") || msg.contains(" debug ") { libc::LOG_DEBUG } else { libc::LOG_INFO }; // 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(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 SyslogWriter { type Writer = SyslogWriter; fn make_writer(&'a self) -> Self::Writer { SyslogWriter::new() } } /// 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)); } }