feagi_observability/
init.rs1use anyhow::anyhow;
9#[cfg(feature = "file-logging")]
10use anyhow::Context;
11use anyhow::Result;
12#[cfg(feature = "file-logging")]
13use chrono::Utc;
14use std::path::{Path, PathBuf};
15#[cfg(feature = "file-logging")]
16use tracing_appender::rolling;
17use tracing_subscriber::layer::SubscriberExt;
18use tracing_subscriber::util::SubscriberInitExt;
19use tracing_subscriber::{EnvFilter, Layer, Registry};
20
21use crate::cli::CrateDebugFlags;
22
23fn resolve_env_filter(debug_flags: &CrateDebugFlags) -> Result<EnvFilter> {
28 if let Ok(rust_log) = std::env::var("RUST_LOG") {
29 return EnvFilter::try_new(rust_log.clone())
30 .map_err(|e| anyhow!("Invalid RUST_LOG '{}': {}", rust_log, e));
31 }
32
33 let filter = debug_flags.to_filter_string();
34 Ok(EnvFilter::new(&filter))
35}
36
37pub struct LoggingGuard {
39 #[cfg(feature = "file-logging")]
40 _file_guards: Vec<tracing_appender::non_blocking::WorkerGuard>,
41 #[cfg(feature = "file-logging")]
42 log_dir: PathBuf,
43}
44
45impl LoggingGuard {
46 #[cfg(feature = "file-logging")]
48 pub fn log_dir(&self) -> &Path {
49 &self.log_dir
50 }
51
52 #[cfg(not(feature = "file-logging"))]
53 pub fn log_dir(&self) -> &Path {
54 Path::new(".")
56 }
57}
58
59#[cfg(feature = "file-logging")]
77pub fn init_logging(
78 debug_flags: &CrateDebugFlags,
79 log_dir: Option<PathBuf>,
80 retention_days: Option<u64>,
81 retention_runs: Option<usize>,
82) -> Result<LoggingGuard> {
83 let base_log_dir = log_dir.unwrap_or_else(|| PathBuf::from("./logs"));
84
85 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
87 let run_folder = base_log_dir.join(format!("run_{}", timestamp));
88 std::fs::create_dir_all(&run_folder)
89 .with_context(|| format!("Failed to create log directory: {}", run_folder.display()))?;
90
91 cleanup_old_logs(&base_log_dir, retention_days, retention_runs)?;
93
94 let env_filter = resolve_env_filter(debug_flags)?;
95
96 let mut layers = Vec::new();
98 let mut file_guards = Vec::new();
99
100 let console_layer = tracing_subscriber::fmt::layer()
102 .with_target(false)
103 .with_file(false)
104 .with_line_number(false)
105 .with_filter(env_filter.clone());
106 layers.push(console_layer.boxed());
107
108 for crate_name in crate::KNOWN_CRATES {
110 let file_appender = rolling::daily(&run_folder, format!("{}.log", crate_name));
112
113 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
114 file_guards.push(guard);
115
116 let file_layer = tracing_subscriber::fmt::layer()
118 .with_writer(non_blocking)
119 .with_target(true)
120 .with_file(true)
121 .with_line_number(true)
122 .json()
123 .with_filter(EnvFilter::new(format!("{}=debug,info", crate_name)))
125 .boxed();
126
127 layers.push(file_layer);
128 }
129
130 let combined_appender = rolling::daily(&run_folder, "feagi.log");
132 let (combined_non_blocking, combined_guard) = tracing_appender::non_blocking(combined_appender);
133
134 let combined_layer = tracing_subscriber::fmt::layer()
135 .with_writer(combined_non_blocking)
136 .with_target(true)
137 .with_file(true)
138 .with_line_number(true)
139 .json()
140 .with_filter(env_filter.clone())
141 .boxed();
142
143 layers.push(combined_layer);
144
145 Registry::default().with(layers).init();
147
148 file_guards.push(combined_guard);
150
151 Ok(LoggingGuard {
152 _file_guards: file_guards,
153 log_dir: run_folder,
154 })
155}
156
157#[cfg(not(feature = "file-logging"))]
162pub fn init_logging(
163 debug_flags: &CrateDebugFlags,
164 _log_dir: Option<PathBuf>,
165 _retention_days: Option<u64>,
166 _retention_runs: Option<usize>,
167) -> Result<LoggingGuard> {
168 let env_filter = resolve_env_filter(debug_flags)?;
169
170 let console_layer = tracing_subscriber::fmt::layer()
172 .with_target(false)
173 .with_file(false)
174 .with_line_number(false)
175 .with_filter(env_filter);
176
177 Registry::default().with(console_layer.boxed()).init();
179
180 Ok(LoggingGuard {})
181}
182
183#[cfg(feature = "file-logging")]
185fn cleanup_old_logs(
186 base_log_dir: &Path,
187 retention_days: Option<u64>,
188 retention_runs: Option<usize>,
189) -> Result<()> {
190 if !base_log_dir.exists() {
191 return Ok(());
192 }
193
194 let retention_days = retention_days.unwrap_or(30);
195 let retention_runs = retention_runs.unwrap_or(10);
196 let cutoff_date = Utc::now() - chrono::Duration::days(retention_days as i64);
197
198 let mut runs: Vec<(PathBuf, DateTime<Utc>)> = Vec::new();
200
201 for entry in std::fs::read_dir(base_log_dir)? {
202 let entry = entry?;
203 let path = entry.path();
204
205 if path.is_dir() {
206 if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
207 if dir_name.starts_with("run_") {
208 if let Some(timestamp_str) = dir_name.strip_prefix("run_") {
210 if let Ok(dt) = DateTime::parse_from_str(timestamp_str, "%Y%m%d_%H%M%S") {
211 runs.push((path, dt.with_timezone(&Utc)));
212 }
213 }
214 }
215 }
216 }
217 }
218
219 runs.sort_by_key(|(_, dt)| *dt);
221
222 let mut removed_count = 0;
224 for (path, dt) in &runs {
225 if *dt < cutoff_date {
226 if let Err(e) = std::fs::remove_dir_all(path) {
227 eprintln!(
228 "Warning: Failed to remove old log directory {}: {}",
229 path.display(),
230 e
231 );
232 } else {
233 removed_count += 1;
234 }
235 }
236 }
237
238 if runs.len() - removed_count > retention_runs {
240 let to_remove = runs.len() - removed_count - retention_runs;
241 for (path, dt) in runs.iter().take(to_remove) {
242 if *dt >= cutoff_date {
243 if path.exists() {
245 if let Err(e) = std::fs::remove_dir_all(path) {
246 eprintln!(
247 "Warning: Failed to remove old log directory {}: {}",
248 path.display(),
249 e
250 );
251 }
252 }
253 }
254 }
255 }
256
257 Ok(())
258}
259
260pub fn init_logging_default(debug_flags: &CrateDebugFlags) -> Result<LoggingGuard> {
262 init_logging(debug_flags, None, None, None)
263}