use std::fs::{self, OpenOptions};
use std::path::Path;
use tracing::info;
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
use tracing_subscriber::{EnvFilter, Registry, fmt, layer::SubscriberExt, util::SubscriberInitExt};
use crate::config::Args;
use crate::error::{Result, SshMcpError};
pub fn init_logging(args: &Args) -> Result<Option<WorkerGuard>> {
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level));
let (file_writer, guard) = if let Some(log_file) = &args.log_file {
let (writer, worker_guard) = setup_file_logging(log_file, &args.log_rotation)?;
(Some(writer), Some(worker_guard))
} else {
(None, None)
};
let registry = Registry::default().with(env_filter);
let stderr_layer = fmt::layer().with_target(false).with_writer(std::io::stderr);
match (file_writer, args.log_format.as_str()) {
(Some(writer), "json") => {
let file_layer = fmt::layer().json().with_target(false).with_writer(writer);
registry.with(stderr_layer).with(file_layer).init();
}
(Some(writer), _) => {
let file_layer = fmt::layer().with_target(false).with_writer(writer);
registry.with(stderr_layer).with(file_layer).init();
}
(None, _) => {
registry.with(stderr_layer).init();
}
}
if let Some(log_file) = &args.log_file {
let rotation_note = match args.log_rotation.as_str() {
"daily" => format!(" (actual file: {}.YYYY-MM-DD)", log_file.display()),
"hourly" => format!(" (actual file: {}.YYYY-MM-DD-HH)", log_file.display()),
_ => String::new(),
};
info!(
"Logging initialized: file={}, format={}, rotation={}{}",
log_file.display(),
args.log_format,
args.log_rotation,
rotation_note
);
} else {
info!("Logging initialized: stderr only, level={}", args.log_level);
}
Ok(guard)
}
fn setup_file_logging(
log_file: &Path,
rotation: &str,
) -> Result<(non_blocking::NonBlocking, WorkerGuard)> {
let log_dir = log_file.parent().unwrap_or(Path::new(".")).to_path_buf();
if !log_dir.as_os_str().is_empty() && !log_dir.exists() {
fs::create_dir_all(&log_dir).map_err(|e| {
SshMcpError::Config(format!(
"Failed to create log directory {}: {}",
log_dir.display(),
e
))
})?;
}
let log_name = log_file
.file_name()
.ok_or_else(|| SshMcpError::Config("Log file path has no file name".to_string()))?
.to_str()
.ok_or_else(|| SshMcpError::Config("Log file name is not valid UTF-8".to_string()))?;
match rotation {
"hourly" => {
let appender = rolling::hourly(&log_dir, log_name);
Ok(non_blocking(appender))
}
"never" => {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(log_file)
.map_err(|e| {
SshMcpError::Config(format!(
"Failed to open log file {}: {}",
log_file.display(),
e
))
})?;
Ok(non_blocking(file))
}
_ => {
let appender = rolling::daily(&log_dir, log_name);
Ok(non_blocking(appender))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_setup_file_logging_creates_dirs() {
let temp_dir = TempDir::new().unwrap();
let log_file = temp_dir.path().join("a").join("b").join("test.log");
let result = setup_file_logging(&log_file, "never");
assert!(result.is_ok());
assert!(log_file.parent().unwrap().exists());
assert!(log_file.exists());
}
#[test]
fn test_setup_file_logging_append_mode() {
let temp_dir = TempDir::new().unwrap();
let log_file = temp_dir.path().join("test.log");
std::fs::write(&log_file, "initial content\n").unwrap();
{
let (_writer, _guard) = setup_file_logging(&log_file, "never").unwrap();
}
let contents = std::fs::read_to_string(&log_file).unwrap();
assert_eq!(contents, "initial content\n");
}
}