Skip to main content

agent_diva_core/
logging.rs

1use std::path::Path;
2use tracing_appender::non_blocking::WorkerGuard;
3use tracing_subscriber::{
4    fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry,
5};
6
7use crate::config::schema::LoggingConfig;
8
9/// Initialize the logging system
10pub fn init_logging(config: &LoggingConfig) -> WorkerGuard {
11    init_logging_with_terminal_output(config, true)
12}
13
14/// Initialize the logging system and optionally write logs to the current terminal.
15pub fn init_logging_with_terminal_output(
16    config: &LoggingConfig,
17    enable_terminal_output: bool,
18) -> WorkerGuard {
19    // 1. Log Level
20    let log_level_str = std::env::var("RUST_LOG").unwrap_or_else(|_| config.level.clone());
21
22    // Build the EnvFilter
23    let mut filter =
24        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&log_level_str));
25
26    // Apply module overrides from config
27    for (module, level) in &config.overrides {
28        // Directives must be valid
29        if let Ok(directive) = format!("{}={}", module, level).parse() {
30            filter = filter.add_directive(directive);
31        } else {
32            eprintln!("Invalid log directive: {}={}", module, level);
33        }
34    }
35
36    // 2. Log Format
37    let format_str = std::env::var("LOG_FORMAT").unwrap_or_else(|_| config.format.clone());
38    let is_json = format_str.to_lowercase() == "json";
39
40    // 3. File Appender
41    // We use rolling::daily.
42    // Requirement: gateway-{date}.log
43    // tracing_appender::rolling::daily(dir, "gateway.log") produces gateway.log.YYYY-MM-DD
44    // tracing_appender::rolling::daily(dir, "gateway") produces gateway.YYYY-MM-DD
45    // We'll use "gateway.log" as prefix to get gateway.log.YYYY-MM-DD which is standard.
46    let file_appender = tracing_appender::rolling::daily(&config.dir, "gateway.log");
47    let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
48
49    // 4. Layers
50    // We need to use Box<dyn Layer<S>> to unify types for conditional compilation
51    // But since is_json is runtime, we can't easily change the Layer type in the subscriber type chain
52    // without boxing.
53
54    let stdout_layer = enable_terminal_output.then(|| {
55        if is_json {
56            fmt::layer()
57                .json()
58                .with_target(true)
59                .with_thread_ids(true)
60                .with_file(true)
61                .with_line_number(true)
62                .boxed()
63        } else {
64            fmt::layer()
65                .with_target(true)
66                .with_thread_ids(true)
67                .with_file(true)
68                .with_line_number(true)
69                // .pretty() // Optional: make text output pretty
70                .boxed()
71        }
72    });
73
74    let file_layer = if is_json {
75        fmt::layer()
76            .json()
77            .with_writer(non_blocking)
78            .with_target(true)
79            .with_thread_ids(true)
80            .with_file(true)
81            .with_line_number(true)
82            .with_ansi(false)
83            .boxed()
84    } else {
85        fmt::layer()
86            .with_writer(non_blocking)
87            .with_ansi(false)
88            .with_target(true)
89            .with_thread_ids(true)
90            .with_file(true)
91            .with_line_number(true)
92            .boxed()
93    };
94
95    // 5. Init Subscriber
96    Registry::default()
97        .with(filter)
98        .with(stdout_layer)
99        .with(file_layer)
100        .init();
101
102    // 6. Cleanup old logs
103    if let Err(e) = cleanup_old_logs(&config.dir, 7) {
104        eprintln!("Failed to clean up old logs: {}", e);
105    }
106
107    guard
108}
109
110/// Clean up log files older than `days` days
111fn cleanup_old_logs(dir: &str, days: u64) -> std::io::Result<()> {
112    let path = Path::new(dir);
113    if !path.exists() {
114        return Ok(());
115    }
116
117    let now = std::time::SystemTime::now();
118    let threshold = std::time::Duration::from_secs(days * 24 * 3600);
119
120    for entry in std::fs::read_dir(path)? {
121        let entry = entry?;
122        let path = entry.path();
123
124        if path.is_file() {
125            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
126                // Match standard patterns
127                if name.starts_with("gateway.log") || name.starts_with("gateway-") {
128                    if let Ok(metadata) = entry.metadata() {
129                        if let Ok(modified) = metadata.modified() {
130                            if let Ok(age) = now.duration_since(modified) {
131                                if age > threshold {
132                                    if let Err(e) = std::fs::remove_file(&path) {
133                                        eprintln!(
134                                            "Failed to remove old log file {:?}: {}",
135                                            path, e
136                                        );
137                                    } else {
138                                        // Use println here as logger might not be fully ready or to avoid recursion loop if we log to file?
139                                        // Actually logger is initializing, so we can use eprintln for internal errors.
140                                    }
141                                }
142                            }
143                        }
144                    }
145                }
146            }
147        }
148    }
149    Ok(())
150}