1use std::fs::{self, OpenOptions};
8use std::path::Path;
9
10use tracing::info;
11use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
12use tracing_subscriber::{EnvFilter, Registry, fmt, layer::SubscriberExt, util::SubscriberInitExt};
13
14use crate::config::Args;
15use crate::error::{Result, SshMcpError};
16
17pub fn init_logging(args: &Args) -> Result<Option<WorkerGuard>> {
35 let env_filter =
37 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level));
38
39 let (file_writer, guard) = if let Some(log_file) = &args.log_file {
41 let (writer, worker_guard) = setup_file_logging(log_file, &args.log_rotation)?;
42 (Some(writer), Some(worker_guard))
43 } else {
44 (None, None)
45 };
46
47 let registry = Registry::default().with(env_filter);
49
50 let stderr_layer = fmt::layer().with_target(false).with_writer(std::io::stderr);
52
53 match (file_writer, args.log_format.as_str()) {
54 (Some(writer), "json") => {
55 let file_layer = fmt::layer().json().with_target(false).with_writer(writer);
57 registry.with(stderr_layer).with(file_layer).init();
58 }
59 (Some(writer), _) => {
60 let file_layer = fmt::layer().with_target(false).with_writer(writer);
62 registry.with(stderr_layer).with(file_layer).init();
63 }
64 (None, _) => {
65 registry.with(stderr_layer).init();
67 }
68 }
69
70 if let Some(log_file) = &args.log_file {
72 let rotation_note = match args.log_rotation.as_str() {
73 "daily" => format!(" (actual file: {}.YYYY-MM-DD)", log_file.display()),
74 "hourly" => format!(" (actual file: {}.YYYY-MM-DD-HH)", log_file.display()),
75 _ => String::new(),
76 };
77 info!(
78 "Logging initialized: file={}, format={}, rotation={}{}",
79 log_file.display(),
80 args.log_format,
81 args.log_rotation,
82 rotation_note
83 );
84 } else {
85 info!("Logging initialized: stderr only, level={}", args.log_level);
86 }
87
88 Ok(guard)
89}
90
91fn setup_file_logging(
97 log_file: &Path,
98 rotation: &str,
99) -> Result<(non_blocking::NonBlocking, WorkerGuard)> {
100 let log_dir = log_file.parent().unwrap_or(Path::new(".")).to_path_buf();
102
103 if !log_dir.as_os_str().is_empty() && !log_dir.exists() {
105 fs::create_dir_all(&log_dir).map_err(|e| {
106 SshMcpError::Config(format!(
107 "Failed to create log directory {}: {}",
108 log_dir.display(),
109 e
110 ))
111 })?;
112 }
113
114 let log_name = log_file
115 .file_name()
116 .ok_or_else(|| SshMcpError::Config("Log file path has no file name".to_string()))?
117 .to_str()
118 .ok_or_else(|| SshMcpError::Config("Log file name is not valid UTF-8".to_string()))?;
119
120 match rotation {
122 "hourly" => {
123 let appender = rolling::hourly(&log_dir, log_name);
124 Ok(non_blocking(appender))
125 }
126 "never" => {
127 let file = OpenOptions::new()
129 .create(true)
130 .append(true)
131 .open(log_file)
132 .map_err(|e| {
133 SshMcpError::Config(format!(
134 "Failed to open log file {}: {}",
135 log_file.display(),
136 e
137 ))
138 })?;
139 Ok(non_blocking(file))
140 }
141 _ => {
142 let appender = rolling::daily(&log_dir, log_name);
144 Ok(non_blocking(appender))
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use tempfile::TempDir;
153
154 #[test]
155 fn test_setup_file_logging_creates_dirs() {
156 let temp_dir = TempDir::new().unwrap();
157 let log_file = temp_dir.path().join("a").join("b").join("test.log");
158
159 let result = setup_file_logging(&log_file, "never");
160 assert!(result.is_ok());
161
162 assert!(log_file.parent().unwrap().exists());
163 assert!(log_file.exists());
164 }
165
166 #[test]
167 fn test_setup_file_logging_append_mode() {
168 let temp_dir = TempDir::new().unwrap();
169 let log_file = temp_dir.path().join("test.log");
170
171 std::fs::write(&log_file, "initial content\n").unwrap();
173
174 {
175 let (_writer, _guard) = setup_file_logging(&log_file, "never").unwrap();
176 }
178
179 let contents = std::fs::read_to_string(&log_file).unwrap();
180 assert_eq!(contents, "initial content\n");
181 }
182}