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 let ring_capacity = crate::ring_layer::capacity_from_env();
148 if ring_capacity > 0 {
149 let ring = crate::ring_layer::install_global_ring(ring_capacity);
150 let ring_layer = crate::ring_layer::RingBufferLayer::new(ring)
151 .with_filter(env_filter.clone())
152 .boxed();
153 layers.push(ring_layer);
154 }
155
156 Registry::default().with(layers).init();
158
159 file_guards.push(combined_guard);
161
162 Ok(LoggingGuard {
163 _file_guards: file_guards,
164 log_dir: run_folder,
165 })
166}
167
168#[cfg(not(feature = "file-logging"))]
173pub fn init_logging(
174 debug_flags: &CrateDebugFlags,
175 _log_dir: Option<PathBuf>,
176 _retention_days: Option<u64>,
177 _retention_runs: Option<usize>,
178) -> Result<LoggingGuard> {
179 let env_filter = resolve_env_filter(debug_flags)?;
180
181 let console_layer = tracing_subscriber::fmt::layer()
183 .with_target(false)
184 .with_file(false)
185 .with_line_number(false)
186 .with_filter(env_filter.clone());
187
188 let mut layers: Vec<Box<dyn tracing_subscriber::Layer<Registry> + Send + Sync>> = Vec::new();
189 layers.push(console_layer.boxed());
190
191 let ring_capacity = crate::ring_layer::capacity_from_env();
193 if ring_capacity > 0 {
194 let ring = crate::ring_layer::install_global_ring(ring_capacity);
195 let ring_layer = crate::ring_layer::RingBufferLayer::new(ring)
196 .with_filter(env_filter)
197 .boxed();
198 layers.push(ring_layer);
199 }
200
201 Registry::default().with(layers).init();
203
204 Ok(LoggingGuard {})
205}
206
207#[cfg(feature = "file-logging")]
209fn cleanup_old_logs(
210 base_log_dir: &Path,
211 retention_days: Option<u64>,
212 retention_runs: Option<usize>,
213) -> Result<()> {
214 if !base_log_dir.exists() {
215 return Ok(());
216 }
217
218 let retention_days = retention_days.unwrap_or(30);
219 let retention_runs = retention_runs.unwrap_or(10);
220 let cutoff_date = Utc::now() - chrono::Duration::days(retention_days as i64);
221
222 let mut runs: Vec<(PathBuf, DateTime<Utc>)> = Vec::new();
224
225 for entry in std::fs::read_dir(base_log_dir)? {
226 let entry = entry?;
227 let path = entry.path();
228
229 if path.is_dir() {
230 if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
231 if dir_name.starts_with("run_") {
232 if let Some(timestamp_str) = dir_name.strip_prefix("run_") {
234 if let Ok(dt) = DateTime::parse_from_str(timestamp_str, "%Y%m%d_%H%M%S") {
235 runs.push((path, dt.with_timezone(&Utc)));
236 }
237 }
238 }
239 }
240 }
241 }
242
243 runs.sort_by_key(|(_, dt)| *dt);
245
246 let mut removed_count = 0;
248 for (path, dt) in &runs {
249 if *dt < cutoff_date {
250 if let Err(e) = std::fs::remove_dir_all(path) {
251 eprintln!(
252 "Warning: Failed to remove old log directory {}: {}",
253 path.display(),
254 e
255 );
256 } else {
257 removed_count += 1;
258 }
259 }
260 }
261
262 if runs.len() - removed_count > retention_runs {
264 let to_remove = runs.len() - removed_count - retention_runs;
265 for (path, dt) in runs.iter().take(to_remove) {
266 if *dt >= cutoff_date {
267 if path.exists() {
269 if let Err(e) = std::fs::remove_dir_all(path) {
270 eprintln!(
271 "Warning: Failed to remove old log directory {}: {}",
272 path.display(),
273 e
274 );
275 }
276 }
277 }
278 }
279 }
280
281 Ok(())
282}
283
284pub fn init_logging_default(debug_flags: &CrateDebugFlags) -> Result<LoggingGuard> {
286 init_logging(debug_flags, None, None, None)
287}