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<()> {
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
193pub 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
212pub 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(()); }
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 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#[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#[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#[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
392pub 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 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}