Skip to main content

agent_diva_core/
logging.rs

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