1use crate::config::LoggingConfig;
6use anyhow::{Context as AnyhowContext, Result};
7use std::sync::{Arc, Mutex};
8use std::fs::OpenOptions;
9use tracing_appender::non_blocking;
10use tracing_subscriber::{
11 layer::SubscriberExt,
12 EnvFilter, Registry,
13 fmt,
14};
15
16pub struct LoggingModule {
23 _file_guard: Arc<Mutex<Option<non_blocking::WorkerGuard>>>,
25}
26
27impl LoggingModule {
28 pub fn new() -> Self {
30 Self {
31 _file_guard: Arc::new(Mutex::new(None)),
32 }
33 }
34}
35
36impl Default for LoggingModule {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl LoggingModule {
43 pub fn init(&self, config: Option<LoggingConfig>) -> Result<()> {
48 let log_config = config.unwrap_or_else(LoggingConfig::default);
50
51 if let Some(file_config) = &log_config.file {
53 if let Some(logrotate_config) = &file_config.logrotate {
54 if logrotate_config.enabled {
55 match logrotate_config.generate_config_file(None) {
58 Ok(path) => {
59 println!("Logrotate config generated: {}", path);
61 println!("To test it, run: logrotate -d {}", path);
62 }
63 Err(e) => {
64 eprintln!("Warning: Failed to generate logrotate config: {}", e);
66 }
67 }
68 }
69 }
70 }
71
72 let (subscriber, file_guard) = build_subscriber(&log_config)
74 .context("Failed to build tracing subscriber")?;
75
76 if let Some(guard) = file_guard {
78 *self._file_guard.lock().unwrap() = Some(guard);
79 }
80
81 match tracing::subscriber::set_global_default(subscriber) {
85 Ok(_) => {
86 tracing::info!("Logging module initialized");
87 }
88 Err(e) => {
89 let error_msg = format!("{}", e);
92 if error_msg.contains("already been set") {
93 println!("Logging module initialized (using existing subscriber)");
95 } else {
96 return Err(anyhow::anyhow!("Failed to set global subscriber: {}", e));
98 }
99 }
100 }
101
102 Ok(())
103 }
104
105 pub fn shutdown(&self) {
109 tracing::info!("Logging module shutdown, flushing logs...");
110 }
111}
112
113
114fn build_subscriber(
119 config: &LoggingConfig,
120) -> Result<(
121 Box<dyn tracing::Subscriber + Send + Sync>,
122 Option<non_blocking::WorkerGuard>,
123)> {
124 let filter = EnvFilter::try_from_default_env()
126 .unwrap_or_else(|_| EnvFilter::new(&config.level));
127
128 let registry = Registry::default().with(filter);
130
131 let has_file = matches!(config.output_mode, crate::config::OutputMode::File | crate::config::OutputMode::Both)
134 && config.file.as_ref().map(|f| f.enabled).unwrap_or(false);
135 let has_console = matches!(config.output_mode, crate::config::OutputMode::Console | crate::config::OutputMode::Both)
136 && config.console.as_ref().map(|c| c.enabled).unwrap_or(false);
137
138 match (has_file, has_console) {
141 (true, true) => {
142 let file_config = config.file.as_ref().unwrap();
144 let console_config = config.console.as_ref().unwrap();
145
146 if let Some(parent) = std::path::Path::new(&file_config.path).parent() {
148 std::fs::create_dir_all(parent)
149 .with_context(|| format!("Failed to create log directory: {}", parent.display()))?;
150 }
151
152 let file = OpenOptions::new()
154 .create(true)
155 .append(true)
156 .open(&file_config.path)
157 .with_context(|| format!("Failed to open log file: {}", file_config.path))?;
158
159 let (non_blocking, guard) = non_blocking(file);
160
161 let file_layer = fmt::layer()
163 .with_writer(non_blocking)
164 .json()
165 .with_target(true)
166 .with_file(true)
167 .with_line_number(true)
168 .with_ansi(false);
169
170 use std::io;
172 let console_layer = fmt::layer()
173 .with_writer(io::stdout)
174 .json()
175 .with_target(true)
176 .with_file(true)
177 .with_line_number(true)
178 .with_ansi(console_config.ansi);
179
180 let subscriber = registry
181 .with(file_layer)
182 .with(console_layer);
183
184 Ok((Box::new(subscriber), Some(guard)))
185 }
186 (true, false) => {
187 let file_config = config.file.as_ref().unwrap();
189
190 if let Some(parent) = std::path::Path::new(&file_config.path).parent() {
192 std::fs::create_dir_all(parent)
193 .with_context(|| format!("Failed to create log directory: {}", parent.display()))?;
194 }
195
196 let file = OpenOptions::new()
198 .create(true)
199 .append(true)
200 .open(&file_config.path)
201 .with_context(|| format!("Failed to open log file: {}", file_config.path))?;
202
203 let (non_blocking, guard) = non_blocking(file);
204
205 let file_layer = fmt::layer()
207 .with_writer(non_blocking)
208 .json()
209 .with_target(true)
210 .with_file(true)
211 .with_line_number(true)
212 .with_ansi(false);
213
214 let subscriber = registry.with(file_layer);
215 Ok((Box::new(subscriber), Some(guard)))
216 }
217 (false, true) => {
218 let console_config = config.console.as_ref().unwrap();
220
221 use std::io;
222 let console_layer = fmt::layer()
223 .with_writer(io::stdout)
224 .json()
225 .with_target(true)
226 .with_file(true)
227 .with_line_number(true)
228 .with_ansi(console_config.ansi);
229
230 let subscriber = registry.with(console_layer);
231 Ok((Box::new(subscriber), None))
232 }
233 (false, false) => {
234 Ok((Box::new(registry), None))
236 }
237 }
238}