Skip to main content

thread_flow/monitoring/
logging.rs

1// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! # Structured Logging for Thread Flow
5//!
6//! Production-ready logging infrastructure with multiple output formats and log levels.
7//!
8//! ## Features
9//!
10//! - **Multiple Formats**: JSON (production) and human-readable (development)
11//! - **Contextual Logging**: Automatic span tracking with tracing
12//! - **Performance Tracking**: Built-in duration tracking for operations
13//! - **Error Context**: Rich error context with backtraces
14//!
15//! ## Usage
16//!
17//! ```rust,ignore
18//! use thread_flow::monitoring::logging::{init_logging, LogConfig, LogLevel, LogFormat};
19//!
20//! // Initialize logging (call once at startup)
21//! init_logging(LogConfig {
22//!     level: LogLevel::Info,
23//!     format: LogFormat::Json,
24//!     ..Default::default()
25//! })?;
26//!
27//! // Use macros for logging
28//! info!("Processing file", file = "src/main.rs");
29//! warn!("Cache miss", hash = "abc123...");
30//! error!("Database connection failed", error = %err);
31//!
32//! // Structured logging with spans
33//! let span = info_span!("analyze_file", file = "src/main.rs");
34//! let _guard = span.enter();
35//! // All logs within this scope will include file context
36//! ```
37
38use std::env;
39use std::fmt;
40
41/// Log level configuration
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum LogLevel {
44    /// Trace-level logging (very verbose)
45    Trace,
46    /// Debug-level logging (verbose)
47    Debug,
48    /// Info-level logging (normal)
49    Info,
50    /// Warning-level logging
51    Warn,
52    /// Error-level logging
53    Error,
54}
55
56impl LogLevel {
57    /// Parse from environment variable (RUST_LOG format)
58    pub fn from_env() -> Self {
59        env::var("RUST_LOG")
60            .ok()
61            .and_then(|s| s.parse().ok())
62            .unwrap_or(LogLevel::Info)
63    }
64}
65
66impl fmt::Display for LogLevel {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            LogLevel::Trace => write!(f, "trace"),
70            LogLevel::Debug => write!(f, "debug"),
71            LogLevel::Info => write!(f, "info"),
72            LogLevel::Warn => write!(f, "warn"),
73            LogLevel::Error => write!(f, "error"),
74        }
75    }
76}
77
78impl std::str::FromStr for LogLevel {
79    type Err = String;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        match s.to_lowercase().as_str() {
83            "trace" => Ok(LogLevel::Trace),
84            "debug" => Ok(LogLevel::Debug),
85            "info" => Ok(LogLevel::Info),
86            "warn" | "warning" => Ok(LogLevel::Warn),
87            "error" => Ok(LogLevel::Error),
88            _ => Err(format!("Invalid log level: {}", s)),
89        }
90    }
91}
92
93/// Log output format
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum LogFormat {
96    /// Human-readable format (for development)
97    Text,
98    /// JSON format (for production)
99    Json,
100    /// Compact format (for CLI)
101    Compact,
102}
103
104impl LogFormat {
105    /// Parse from environment variable
106    pub fn from_env() -> Self {
107        env::var("LOG_FORMAT")
108            .ok()
109            .and_then(|s| s.parse().ok())
110            .unwrap_or(LogFormat::Text)
111    }
112}
113
114impl fmt::Display for LogFormat {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            LogFormat::Text => write!(f, "text"),
118            LogFormat::Json => write!(f, "json"),
119            LogFormat::Compact => write!(f, "compact"),
120        }
121    }
122}
123
124impl std::str::FromStr for LogFormat {
125    type Err = String;
126
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        match s.to_lowercase().as_str() {
129            "text" | "pretty" | "human" => Ok(LogFormat::Text),
130            "json" => Ok(LogFormat::Json),
131            "compact" => Ok(LogFormat::Compact),
132            _ => Err(format!("Invalid log format: {}", s)),
133        }
134    }
135}
136
137/// Logging configuration
138#[derive(Debug, Clone)]
139pub struct LogConfig {
140    /// Log level threshold
141    pub level: LogLevel,
142    /// Output format
143    pub format: LogFormat,
144    /// Whether to include timestamps
145    pub timestamps: bool,
146    /// Whether to include file/line information
147    pub source_location: bool,
148    /// Whether to include thread IDs
149    pub thread_ids: bool,
150}
151
152impl Default for LogConfig {
153    fn default() -> Self {
154        Self {
155            level: LogLevel::Info,
156            format: LogFormat::Text,
157            timestamps: true,
158            source_location: false,
159            thread_ids: false,
160        }
161    }
162}
163
164impl LogConfig {
165    /// Load configuration from environment variables
166    pub fn from_env() -> Self {
167        Self {
168            level: LogLevel::from_env(),
169            format: LogFormat::from_env(),
170            timestamps: env::var("LOG_TIMESTAMPS")
171                .ok()
172                .and_then(|v| v.parse().ok())
173                .unwrap_or(true),
174            source_location: env::var("LOG_SOURCE_LOCATION")
175                .ok()
176                .and_then(|v| v.parse().ok())
177                .unwrap_or(false),
178            thread_ids: env::var("LOG_THREAD_IDS")
179                .ok()
180                .and_then(|v| v.parse().ok())
181                .unwrap_or(false),
182        }
183    }
184}
185
186/// Initialize logging infrastructure
187///
188/// This should be called once at application startup.
189///
190/// # Example
191///
192/// ```rust,ignore
193/// use thread_flow::monitoring::logging::{init_logging, LogConfig};
194///
195/// fn main() -> Result<(), Box<dyn std::error::Error>> {
196///     init_logging(LogConfig::default())?;
197///
198///     // Application code...
199///
200///     Ok(())
201/// }
202/// ```
203pub fn init_logging(config: LogConfig) -> Result<(), LoggingError> {
204    // Simple logging setup for now
205    // In production, this would integrate with tracing-subscriber
206
207    // Set RUST_LOG if not already set
208    if env::var("RUST_LOG").is_err() {
209        unsafe {
210            env::set_var("RUST_LOG", format!("thread_flow={}", config.level));
211        }
212    }
213
214    // Initialize env_logger (simple implementation)
215    let mut builder = env_logger::builder();
216    builder.parse_env("RUST_LOG");
217
218    if let Some(precision) = if config.timestamps {
219        Some(env_logger::fmt::TimestampPrecision::Millis)
220    } else {
221        None
222    } {
223        builder.format_timestamp(Some(precision));
224    } else {
225        builder.format_timestamp(None);
226    }
227
228    builder.format_module_path(config.source_location);
229
230    builder
231        .try_init()
232        .map_err(|e| LoggingError::InitializationFailed(e.to_string()))?;
233
234    Ok(())
235}
236
237/// Initialize logging for CLI applications
238///
239/// Convenience function that sets up human-readable logging.
240pub fn init_cli_logging() -> Result<(), LoggingError> {
241    init_logging(LogConfig {
242        level: LogLevel::from_env(),
243        format: LogFormat::Text,
244        timestamps: true,
245        source_location: false,
246        thread_ids: false,
247    })
248}
249
250/// Initialize logging for production/edge deployments
251///
252/// Convenience function that sets up JSON logging for production.
253pub fn init_production_logging() -> Result<(), LoggingError> {
254    init_logging(LogConfig {
255        level: LogLevel::Info,
256        format: LogFormat::Json,
257        timestamps: true,
258        source_location: true,
259        thread_ids: true,
260    })
261}
262
263/// Logging errors
264#[derive(Debug, thiserror::Error)]
265pub enum LoggingError {
266    #[error("Failed to initialize logging: {0}")]
267    InitializationFailed(String),
268
269    #[error("Invalid log configuration: {0}")]
270    InvalidConfiguration(String),
271}
272
273/// Macro for structured logging with performance tracking
274///
275/// # Example
276///
277/// ```rust,ignore
278/// use thread_flow::monitoring::logging::timed_operation;
279///
280/// timed_operation!("parse_file", file = "src/main.rs", {
281///     // Operation code here
282///     parse_rust_file(file)?;
283/// });
284/// // Automatically logs duration when complete
285/// ```
286#[macro_export]
287macro_rules! timed_operation {
288    ($name:expr, $($key:ident = $value:expr),*, $block:block) => {{
289        let _start = std::time::Instant::now();
290        $(
291            println!("[DEBUG] {}: {} = {:?}", $name, stringify!($key), $value);
292        )*
293        let result = $block;
294        let _duration = _start.elapsed();
295        println!("[INFO] {} completed in {:?}", $name, _duration);
296        result
297    }};
298}
299
300/// Structured logging helpers
301pub mod structured {
302    use thread_utilities::RapidMap;
303
304    /// Build a structured log context
305    pub struct LogContext {
306        fields: RapidMap<String, String>,
307    }
308
309    impl LogContext {
310        /// Create a new log context
311        pub fn new() -> Self {
312            Self {
313                fields: thread_utilities::get_map(),
314            }
315        }
316
317        /// Add a field to the context
318        pub fn field(mut self, key: impl Into<String>, value: impl ToString) -> Self {
319            self.fields.insert(key.into(), value.to_string());
320            self
321        }
322
323        /// Log at info level with context
324        pub fn info(self, message: &str) {
325            // Use println for now until log crate is properly integrated
326            println!("[INFO] {} {:?}", message, self.fields);
327        }
328
329        /// Log at warn level with context
330        pub fn warn(self, message: &str) {
331            eprintln!("[WARN] {} {:?}", message, self.fields);
332        }
333
334        /// Log at error level with context
335        pub fn error(self, message: &str) {
336            eprintln!("[ERROR] {} {:?}", message, self.fields);
337        }
338    }
339
340    impl Default for LogContext {
341        fn default() -> Self {
342            Self::new()
343        }
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_log_level_parsing() {
353        assert_eq!("trace".parse::<LogLevel>().unwrap(), LogLevel::Trace);
354        assert_eq!("debug".parse::<LogLevel>().unwrap(), LogLevel::Debug);
355        assert_eq!("info".parse::<LogLevel>().unwrap(), LogLevel::Info);
356        assert_eq!("warn".parse::<LogLevel>().unwrap(), LogLevel::Warn);
357        assert_eq!("error".parse::<LogLevel>().unwrap(), LogLevel::Error);
358    }
359
360    #[test]
361    fn test_log_format_parsing() {
362        assert_eq!("text".parse::<LogFormat>().unwrap(), LogFormat::Text);
363        assert_eq!("json".parse::<LogFormat>().unwrap(), LogFormat::Json);
364        assert_eq!("compact".parse::<LogFormat>().unwrap(), LogFormat::Compact);
365    }
366
367    #[test]
368    fn test_log_config_default() {
369        let config = LogConfig::default();
370        assert_eq!(config.level, LogLevel::Info);
371        assert_eq!(config.format, LogFormat::Text);
372        assert!(config.timestamps);
373        assert!(!config.source_location);
374        assert!(!config.thread_ids);
375    }
376}