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