intent_engine/
logging.rs

1//! Intent-Engine Logging System
2//!
3//! Provides structured logging with configurable levels and output formats.
4//! Uses tracing crate for structured logging with spans and events.
5
6use 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/// Logging configuration options
16#[derive(Debug, Clone)]
17pub struct LoggingConfig {
18    /// Minimum log level to output
19    pub level: Level,
20    /// Enable colored output
21    pub color: bool,
22    /// Show timestamps
23    pub show_timestamps: bool,
24    /// Show target/module name
25    pub show_target: bool,
26    /// Enable JSON format for machine parsing
27    pub json_format: bool,
28    /// Enable span events for tracing
29    pub enable_spans: bool,
30    /// Output to file instead of stdout (for daemon mode)
31    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    /// Create config for different application modes
50    pub fn for_mode(mode: ApplicationMode) -> Self {
51        match mode {
52            ApplicationMode::McpServer => Self {
53                level: Level::DEBUG,
54                color: false, // MCP output should be clean
55                show_timestamps: true,
56                show_target: true,
57                json_format: true,   // Machine-readable for MCP
58                enable_spans: false, // Avoid noise in JSON-RPC
59                file_output: None,
60            },
61            ApplicationMode::Dashboard => Self {
62                level: Level::INFO,
63                color: false, // Background service
64                show_timestamps: true,
65                show_target: true,
66                json_format: false,
67                enable_spans: true, // Good for debugging dashboard
68                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    /// Create config from CLI arguments
92    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/// Application modes with different logging requirements
114#[derive(Debug, Clone, Copy)]
115pub enum ApplicationMode {
116    /// MCP server mode - clean, structured output
117    McpServer,
118    /// Dashboard server mode - detailed for debugging
119    Dashboard,
120    /// CLI mode - user-friendly output
121    Cli,
122    /// Test mode - maximum detail for testing
123    Test,
124}
125
126/// Initialize the logging system
127pub fn init_logging(config: LoggingConfig) -> io::Result<()> {
128    let env_filter = EnvFilter::try_from_default_env()
129        .unwrap_or_else(|_| EnvFilter::new(format!("intent_engine={}", config.level)));
130
131    let registry = Registry::default().with(env_filter);
132
133    if let Some(log_file) = config.file_output {
134        let file_appender = tracing_appender::rolling::never(
135            log_file.parent().ok_or_else(|| {
136                io::Error::new(io::ErrorKind::InvalidInput, "Invalid log file path")
137            })?,
138            log_file.file_name().ok_or_else(|| {
139                io::Error::new(io::ErrorKind::InvalidInput, "Invalid log file name")
140            })?,
141        );
142
143        if config.json_format {
144            let json_layer = tracing_subscriber::fmt::layer()
145                .json()
146                .with_current_span(config.enable_spans)
147                .with_span_events(FmtSpan::CLOSE)
148                .with_writer(file_appender);
149            json_layer.with_subscriber(registry).init();
150        } else {
151            let fmt_layer = fmt::layer()
152                .with_target(config.show_target)
153                .with_level(true)
154                .with_ansi(false)
155                .with_writer(file_appender);
156
157            if config.show_timestamps {
158                fmt_layer
159                    .with_timer(fmt::time::ChronoUtc::rfc_3339())
160                    .with_subscriber(registry)
161                    .init();
162            } else {
163                fmt_layer.with_subscriber(registry).init();
164            }
165        }
166    } else if config.json_format {
167        let json_layer = tracing_subscriber::fmt::layer()
168            .json()
169            .with_current_span(config.enable_spans)
170            .with_span_events(FmtSpan::CLOSE)
171            .with_writer(io::stdout);
172        json_layer.with_subscriber(registry).init();
173    } else {
174        let fmt_layer = fmt::layer()
175            .with_target(config.show_target)
176            .with_level(true)
177            .with_ansi(config.color)
178            .with_writer(io::stdout);
179
180        if config.show_timestamps {
181            fmt_layer
182                .with_timer(fmt::time::ChronoUtc::rfc_3339())
183                .with_subscriber(registry)
184                .init();
185        } else {
186            fmt_layer.with_subscriber(registry).init();
187        }
188    }
189
190    Ok(())
191}
192
193/// Initialize logging from environment variables
194pub fn init_from_env() -> io::Result<()> {
195    let _level = match std::env::var("IE_LOG_LEVEL").as_deref() {
196        Ok("error") => Level::ERROR,
197        Ok("warn") => Level::WARN,
198        Ok("info") => Level::INFO,
199        Ok("debug") => Level::DEBUG,
200        Ok("trace") => Level::TRACE,
201        _ => Level::INFO,
202    };
203
204    let json = std::env::var("IE_LOG_JSON").as_deref() == Ok("true");
205    let verbose = std::env::var("IE_LOG_VERBOSE").as_deref() == Ok("true");
206    let quiet = std::env::var("IE_LOG_QUIET").as_deref() == Ok("true");
207
208    let config = LoggingConfig::from_args(quiet, verbose, json);
209    init_logging(config)
210}
211
212/// Clean up old log files based on retention policy
213///
214/// Scans the log directory and removes files older than the specified retention period.
215/// Only removes files matching the pattern `.log.YYYY-MM-DD` (rotated log files).
216///
217/// # Arguments
218/// * `log_dir` - Directory containing log files
219/// * `retention_days` - Number of days to retain logs (default: 7)
220///
221/// # Example
222/// ```no_run
223/// use std::path::Path;
224/// use intent_engine::logging::cleanup_old_logs;
225///
226/// let log_dir = Path::new("/home/user/.intent-engine/logs");
227/// cleanup_old_logs(log_dir, 7).ok();
228/// ```
229pub fn cleanup_old_logs(log_dir: &std::path::Path, retention_days: u32) -> io::Result<()> {
230    use std::fs;
231    use std::time::SystemTime;
232
233    if !log_dir.exists() {
234        return Ok(()); // Nothing to clean if directory doesn't exist
235    }
236
237    let now = SystemTime::now();
238    let retention_duration = std::time::Duration::from_secs(retention_days as u64 * 24 * 60 * 60);
239
240    let mut cleaned_count = 0;
241    let mut cleaned_size: u64 = 0;
242
243    for entry in fs::read_dir(log_dir)? {
244        let entry = entry?;
245        let path = entry.path();
246
247        // Only process rotated log files (containing .log. followed by a date)
248        // Examples: dashboard.log.2025-11-22, mcp-server.log.2025-11-21
249        let path_str = path.to_string_lossy();
250        if !path_str.contains(".log.") || !path.is_file() {
251            continue;
252        }
253
254        let metadata = entry.metadata()?;
255        let modified = metadata.modified()?;
256
257        if let Ok(age) = now.duration_since(modified) {
258            if age > retention_duration {
259                let size = metadata.len();
260                match fs::remove_file(&path) {
261                    Ok(_) => {
262                        cleaned_count += 1;
263                        cleaned_size += size;
264                        tracing::info!(
265                            "Cleaned up old log file: {} (age: {} days, size: {} bytes)",
266                            path.display(),
267                            age.as_secs() / 86400,
268                            size
269                        );
270                    },
271                    Err(e) => {
272                        tracing::warn!("Failed to remove old log file {}: {}", path.display(), e);
273                    },
274                }
275            }
276        }
277    }
278
279    if cleaned_count > 0 {
280        tracing::info!(
281            "Log cleanup completed: removed {} files, freed {} bytes",
282            cleaned_count,
283            cleaned_size
284        );
285    }
286
287    Ok(())
288}
289
290/// Log macros for common intent-engine operations
291#[macro_export]
292macro_rules! log_project_operation {
293    ($operation:expr, $project_path:expr) => {
294        tracing::info!(
295            operation = $operation,
296            project_path = %$project_path.display(),
297            "Project operation"
298        );
299    };
300    ($operation:expr, $project_path:expr, $details:expr) => {
301        tracing::info!(
302            operation = $operation,
303            project_path = %$project_path.display(),
304            details = $details,
305            "Project operation"
306        );
307    };
308}
309
310#[macro_export]
311macro_rules! log_mcp_operation {
312    ($operation:expr, $method:expr) => {
313        tracing::debug!(
314            operation = $operation,
315            mcp_method = $method,
316            "MCP operation"
317        );
318    };
319    ($operation:expr, $method:expr, $details:expr) => {
320        tracing::debug!(
321            operation = $operation,
322            mcp_method = $method,
323            details = $details,
324            "MCP operation"
325        );
326    };
327}
328
329#[macro_export]
330macro_rules! log_dashboard_operation {
331    ($operation:expr) => {
332        tracing::info!(operation = $operation, "Dashboard operation");
333    };
334    ($operation:expr, $details:expr) => {
335        tracing::info!(
336            operation = $operation,
337            details = $details,
338            "Dashboard operation"
339        );
340    };
341}
342
343#[macro_export]
344macro_rules! log_task_operation {
345    ($operation:expr, $task_id:expr) => {
346        tracing::info!(operation = $operation, task_id = $task_id, "Task operation");
347    };
348    ($operation:expr, $task_id:expr, $details:expr) => {
349        tracing::info!(
350            operation = $operation,
351            task_id = $task_id,
352            details = $details,
353            "Task operation"
354        );
355    };
356}
357
358#[macro_export]
359macro_rules! log_registry_operation {
360    ($operation:expr, $count:expr) => {
361        tracing::debug!(
362            operation = $operation,
363            project_count = $count,
364            "Registry operation"
365        );
366    };
367}
368
369/// Utility macro for structured error logging
370#[macro_export]
371macro_rules! log_error {
372    ($error:expr, $context:expr) => {
373        tracing::error!(
374            error = %$error,
375            context = $context,
376            "Operation failed"
377        );
378    };
379}
380
381/// Utility macro for structured warning logging
382#[macro_export]
383macro_rules! log_warning {
384    ($message:expr) => {
385        tracing::warn!($message);
386    };
387    ($message:expr, $details:expr) => {
388        tracing::warn!(message = $message, details = $details, "Warning");
389    };
390}
391
392/// Get log file path for a given application mode
393pub fn log_file_path(mode: ApplicationMode) -> std::path::PathBuf {
394    let home = dirs::home_dir().expect("Failed to get home directory");
395    let log_dir = home.join(".intent-engine").join("logs");
396
397    // Create log directory if it doesn't exist
398    std::fs::create_dir_all(&log_dir).ok();
399
400    match mode {
401        ApplicationMode::Dashboard => log_dir.join("dashboard.log"),
402        ApplicationMode::McpServer => log_dir.join("mcp-server.log"),
403        ApplicationMode::Cli => log_dir.join("cli.log"),
404        ApplicationMode::Test => log_dir.join("test.log"),
405    }
406}