1use std::io;
7use tracing::Level;
8use tracing_subscriber::{
9 fmt::{self, format::FmtSpan},
10 layer::SubscriberExt,
11 util::SubscriberInitExt,
12 EnvFilter, Layer, Registry,
13};
14
15#[derive(Debug, Clone)]
17pub struct LoggingConfig {
18 pub level: Level,
20 pub color: bool,
22 pub show_timestamps: bool,
24 pub show_target: bool,
26 pub json_format: bool,
28 pub enable_spans: bool,
30 pub file_output: Option<std::path::PathBuf>,
32}
33
34impl Default for LoggingConfig {
35 fn default() -> Self {
36 Self {
37 level: Level::INFO,
38 color: true,
39 show_timestamps: false,
40 show_target: false,
41 json_format: false,
42 enable_spans: false,
43 file_output: None,
44 }
45 }
46}
47
48impl LoggingConfig {
49 pub fn for_mode(mode: ApplicationMode) -> Self {
51 match mode {
52 ApplicationMode::McpServer => Self {
53 level: Level::DEBUG,
54 color: false, show_timestamps: true,
56 show_target: true,
57 json_format: true, enable_spans: false, file_output: None,
60 },
61 ApplicationMode::Dashboard => Self {
62 level: Level::INFO,
63 color: false, show_timestamps: true,
65 show_target: true,
66 json_format: false,
67 enable_spans: true, file_output: None,
69 },
70 ApplicationMode::Cli => Self {
71 level: Level::INFO,
72 color: true,
73 show_timestamps: false,
74 show_target: false,
75 json_format: false,
76 enable_spans: false,
77 file_output: None,
78 },
79 ApplicationMode::Test => Self {
80 level: Level::DEBUG,
81 color: false,
82 show_timestamps: true,
83 show_target: true,
84 json_format: false,
85 enable_spans: true,
86 file_output: None,
87 },
88 }
89 }
90
91 pub fn from_args(quiet: bool, verbose: bool, json: bool) -> Self {
93 let level = if verbose {
94 Level::DEBUG
95 } else if quiet {
96 Level::ERROR
97 } else {
98 Level::INFO
99 };
100
101 Self {
102 level,
103 color: !quiet && !json && atty::is(atty::Stream::Stdout),
104 show_timestamps: verbose || json,
105 show_target: verbose,
106 json_format: json,
107 enable_spans: verbose,
108 file_output: None,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy)]
115pub enum ApplicationMode {
116 McpServer,
118 Dashboard,
120 Cli,
122 Test,
124}
125
126pub fn init_logging(config: LoggingConfig) -> io::Result<()> {
132 let env_filter = EnvFilter::try_from_default_env()
133 .unwrap_or_else(|_| EnvFilter::new(format!("intent_engine={}", config.level)));
134
135 let registry = Registry::default().with(env_filter);
136
137 if let Some(log_file) = config.file_output {
138 let log_dir = log_file
139 .parent()
140 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid log file path"))?;
141
142 let file_name = log_file
143 .file_name()
144 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid log file name"))?;
145
146 let file_appender = tracing_appender::rolling::daily(log_dir, file_name);
148
149 if config.json_format {
150 let json_layer = tracing_subscriber::fmt::layer()
151 .json()
152 .with_current_span(config.enable_spans)
153 .with_span_events(FmtSpan::CLOSE)
154 .with_writer(file_appender);
155 json_layer.with_subscriber(registry).init();
156 } else {
157 let fmt_layer = fmt::layer()
158 .with_target(config.show_target)
159 .with_level(true)
160 .with_ansi(false)
161 .with_writer(file_appender);
162
163 if config.show_timestamps {
164 fmt_layer
165 .with_timer(fmt::time::ChronoUtc::rfc_3339())
166 .with_subscriber(registry)
167 .init();
168 } else {
169 fmt_layer.with_subscriber(registry).init();
170 }
171 }
172 } else if config.json_format {
173 let json_layer = tracing_subscriber::fmt::layer()
174 .json()
175 .with_current_span(config.enable_spans)
176 .with_span_events(FmtSpan::CLOSE)
177 .with_writer(io::stdout);
178 json_layer.with_subscriber(registry).init();
179 } else {
180 let fmt_layer = fmt::layer()
181 .with_target(config.show_target)
182 .with_level(true)
183 .with_ansi(config.color)
184 .with_writer(io::stdout);
185
186 if config.show_timestamps {
187 fmt_layer
188 .with_timer(fmt::time::ChronoUtc::rfc_3339())
189 .with_subscriber(registry)
190 .init();
191 } else {
192 fmt_layer.with_subscriber(registry).init();
193 }
194 }
195
196 Ok(())
197}
198
199pub fn init_from_env() -> io::Result<()> {
201 let _level = match std::env::var("IE_LOG_LEVEL").as_deref() {
202 Ok("error") => Level::ERROR,
203 Ok("warn") => Level::WARN,
204 Ok("info") => Level::INFO,
205 Ok("debug") => Level::DEBUG,
206 Ok("trace") => Level::TRACE,
207 _ => Level::INFO,
208 };
209
210 let json = std::env::var("IE_LOG_JSON").as_deref() == Ok("true");
211 let verbose = std::env::var("IE_LOG_VERBOSE").as_deref() == Ok("true");
212 let quiet = std::env::var("IE_LOG_QUIET").as_deref() == Ok("true");
213
214 let config = LoggingConfig::from_args(quiet, verbose, json);
215 init_logging(config)
216}
217
218pub fn cleanup_old_logs(log_dir: &std::path::Path, retention_days: u32) -> io::Result<()> {
236 use std::fs;
237 use std::time::SystemTime;
238
239 if !log_dir.exists() {
240 return Ok(()); }
242
243 let now = SystemTime::now();
244 let retention_duration = std::time::Duration::from_secs(retention_days as u64 * 24 * 60 * 60);
245
246 let mut cleaned_count = 0;
247 let mut cleaned_size: u64 = 0;
248
249 for entry in fs::read_dir(log_dir)? {
250 let entry = entry?;
251 let path = entry.path();
252
253 let path_str = path.to_string_lossy();
256 if !path_str.contains(".log.") || !path.is_file() {
257 continue;
258 }
259
260 let metadata = entry.metadata()?;
261 let modified = metadata.modified()?;
262
263 if let Ok(age) = now.duration_since(modified) {
264 if age > retention_duration {
265 let size = metadata.len();
266 match fs::remove_file(&path) {
267 Ok(_) => {
268 cleaned_count += 1;
269 cleaned_size += size;
270 tracing::info!(
271 "Cleaned up old log file: {} (age: {} days, size: {} bytes)",
272 path.display(),
273 age.as_secs() / 86400,
274 size
275 );
276 },
277 Err(e) => {
278 tracing::warn!("Failed to remove old log file {}: {}", path.display(), e);
279 },
280 }
281 }
282 }
283 }
284
285 if cleaned_count > 0 {
286 tracing::info!(
287 "Log cleanup completed: removed {} files, freed {} bytes",
288 cleaned_count,
289 cleaned_size
290 );
291 }
292
293 Ok(())
294}
295
296#[macro_export]
298macro_rules! log_project_operation {
299 ($operation:expr, $project_path:expr) => {
300 tracing::info!(
301 operation = $operation,
302 project_path = %$project_path.display(),
303 "Project operation"
304 );
305 };
306 ($operation:expr, $project_path:expr, $details:expr) => {
307 tracing::info!(
308 operation = $operation,
309 project_path = %$project_path.display(),
310 details = $details,
311 "Project operation"
312 );
313 };
314}
315
316#[macro_export]
317macro_rules! log_mcp_operation {
318 ($operation:expr, $method:expr) => {
319 tracing::debug!(
320 operation = $operation,
321 mcp_method = $method,
322 "MCP operation"
323 );
324 };
325 ($operation:expr, $method:expr, $details:expr) => {
326 tracing::debug!(
327 operation = $operation,
328 mcp_method = $method,
329 details = $details,
330 "MCP operation"
331 );
332 };
333}
334
335#[macro_export]
336macro_rules! log_dashboard_operation {
337 ($operation:expr) => {
338 tracing::info!(operation = $operation, "Dashboard operation");
339 };
340 ($operation:expr, $details:expr) => {
341 tracing::info!(
342 operation = $operation,
343 details = $details,
344 "Dashboard operation"
345 );
346 };
347}
348
349#[macro_export]
350macro_rules! log_task_operation {
351 ($operation:expr, $task_id:expr) => {
352 tracing::info!(operation = $operation, task_id = $task_id, "Task operation");
353 };
354 ($operation:expr, $task_id:expr, $details:expr) => {
355 tracing::info!(
356 operation = $operation,
357 task_id = $task_id,
358 details = $details,
359 "Task operation"
360 );
361 };
362}
363
364#[macro_export]
365macro_rules! log_registry_operation {
366 ($operation:expr, $count:expr) => {
367 tracing::debug!(
368 operation = $operation,
369 project_count = $count,
370 "Registry operation"
371 );
372 };
373}
374
375#[macro_export]
377macro_rules! log_error {
378 ($error:expr, $context:expr) => {
379 tracing::error!(
380 error = %$error,
381 context = $context,
382 "Operation failed"
383 );
384 };
385}
386
387#[macro_export]
389macro_rules! log_warning {
390 ($message:expr) => {
391 tracing::warn!($message);
392 };
393 ($message:expr, $details:expr) => {
394 tracing::warn!(message = $message, details = $details, "Warning");
395 };
396}
397
398pub fn log_file_path(mode: ApplicationMode) -> std::path::PathBuf {
400 let home = dirs::home_dir().expect("Failed to get home directory");
401 let log_dir = home.join(".intent-engine").join("logs");
402
403 std::fs::create_dir_all(&log_dir).ok();
405
406 match mode {
407 ApplicationMode::Dashboard => log_dir.join("dashboard.log"),
408 ApplicationMode::McpServer => log_dir.join("mcp-server.log"),
409 ApplicationMode::Cli => log_dir.join("cli.log"),
410 ApplicationMode::Test => log_dir.join("test.log"),
411 }
412}