mirror of https://github.com/telemt/telemt.git
292 lines
8.7 KiB
Rust
292 lines
8.7 KiB
Rust
//! 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<tracing_appender::non_blocking::WorkerGuard>,
|
|
}
|
|
|
|
impl LoggingGuard {
|
|
fn new(guard: Option<tracing_appender::non_blocking::WorkerGuard>) -> 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<EnvFilter, impl tracing::Subscriber + Send + Sync>, 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<usize> {
|
|
// 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<String> = 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));
|
|
}
|
|
}
|