figurehead/core/
logging.rs

1//! Logging infrastructure for diagram processing
2//!
3//! This module provides structured logging using the `tracing` crate.
4//! It supports configurable log levels and formats, and is designed to be
5//! WASM-compatible for future browser support.
6//!
7//! # Usage
8//!
9//! ```rust
10//! use figurehead::core::logging::init_logging;
11//!
12//! // Initialize with default settings (may fail if already initialized)
13//! let _ = init_logging(None, None);
14//!
15//! // Or with custom level and format
16//! let _ = init_logging(Some("debug"), Some("pretty"));
17//! ```
18//!
19//! # Log Levels
20//!
21//! - `trace`: Very detailed information, typically only interesting when debugging
22//! - `debug`: Detailed information for debugging
23//! - `info`: General informational messages (default)
24//! - `warn`: Warning messages
25//! - `error`: Error messages
26//!
27//! # Log Formats
28//!
29//! - `compact`: Single-line format, good for production
30//! - `pretty`: Multi-line format with colors, good for development
31//! - `json`: JSON format, good for log aggregation systems
32//!
33//! # Environment Variables
34//!
35//! Logging can be configured via environment variables:
36//! - `FIGUREHEAD_LOG_LEVEL`: Set log level (trace|debug|info|warn|error)
37//! - `RUST_LOG`: Alternative way to set log level (tracing-subscriber standard)
38//!
39//! # WASM Compatibility
40//!
41//! The logging infrastructure is designed to work in WASM environments.
42//! For WASM builds, use `tracing-wasm` instead of `tracing-subscriber`.
43//!
44//! # Adding Tracing to Custom Plugins
45//!
46//! When implementing a new diagram type plugin, add tracing spans and events
47//! to provide visibility into the processing pipeline:
48//!
49//! ```rust
50//! use tracing::{debug, info, span, trace, Level};
51//!
52//! // Example: Adding tracing to a parser implementation
53//! fn parse_with_tracing(input: &str) -> Result<(), Box<dyn std::error::Error>> {
54//!     let parse_span = span!(Level::INFO, "parse_mydiagram", input_len = input.len());
55//!     let _enter = parse_span.enter();
56//!
57//!     trace!("Starting parsing");
58//!
59//!     // Parse stages
60//!     let stage_span = span!(Level::DEBUG, "parse_stage");
61//!     let _stage_enter = stage_span.enter();
62//!     // ... parsing logic ...
63//!     debug!(input_len = input.len(), "Parsed input");
64//!     drop(_stage_enter);
65//!
66//!     info!("Parsing completed");
67//!     Ok(())
68//! }
69//!
70//! // Note: This example doesn't actually parse anything, just demonstrates tracing usage
71//! # parse_with_tracing("example input").unwrap();
72//! ```
73//!
74//! # Filtering Logs
75//!
76//! You can filter logs by component using the log level syntax:
77//!
78//! ```bash
79//! # Show only parser logs at debug level
80//! RUST_LOG="figurehead::plugins::flowchart::parser=debug" figurehead convert input.mmd
81//!
82//! # Show all logs at info level, but layout at trace level
83//! RUST_LOG="info,figurehead::plugins::flowchart::layout=trace" figurehead convert input.mmd
84//! ```
85
86use std::str::FromStr;
87
88#[cfg(not(target_arch = "wasm32"))]
89use tracing_subscriber::{
90    fmt::{self, format::FmtSpan},
91    layer::SubscriberExt,
92    util::SubscriberInitExt,
93    EnvFilter, Registry,
94};
95
96#[cfg(target_arch = "wasm32")]
97use tracing_wasm::WASMLayerConfig;
98
99/// Log format options
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum LogFormat {
102    /// Compact single-line format
103    Compact,
104    /// Pretty multi-line format with colors
105    Pretty,
106    /// JSON format for log aggregation
107    Json,
108}
109
110impl FromStr for LogFormat {
111    type Err = String;
112
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        match s.to_lowercase().as_str() {
115            "compact" => Ok(LogFormat::Compact),
116            "pretty" => Ok(LogFormat::Pretty),
117            "json" => Ok(LogFormat::Json),
118            _ => Err(format!("Unknown log format: {}", s)),
119        }
120    }
121}
122
123impl LogFormat {
124    /// Get all valid format names
125    pub fn variants() -> &'static [&'static str] {
126        &["compact", "pretty", "json"]
127    }
128}
129
130/// Initialize the tracing subscriber with the given log level and format
131///
132/// # Arguments
133///
134/// * `level` - Optional log level string (trace|debug|info|warn|error).
135///   If None, uses environment variable `FIGUREHEAD_LOG_LEVEL` or `RUST_LOG`,
136///   or defaults to `info`.
137/// * `format` - Optional log format (compact|pretty|json).
138///   If None, uses environment variable `FIGUREHEAD_LOG_FORMAT`,
139///   or defaults to `compact`.
140///
141/// # Returns
142///
143/// Returns an error if initialization fails (e.g., subscriber already initialized).
144///
145/// # Example
146///
147/// ```rust
148/// use figurehead::core::logging::init_logging;
149///
150/// // Initialize with defaults (may fail if already initialized)
151/// let _ = init_logging(None, None);
152///
153/// // Initialize with custom settings
154/// let _ = init_logging(Some("debug"), Some("pretty"));
155/// ```
156pub fn init_logging(
157    level: Option<&str>,
158    format: Option<&str>,
159) -> Result<(), Box<dyn std::error::Error>> {
160    #[cfg(target_arch = "wasm32")]
161    {
162        // WASM builds use tracing-wasm which logs to browser console
163        // tracing-wasm v0.1.0 doesn't support level filtering in config,
164        // but we can use EnvFilter if needed. For now, use default config.
165        // Log level filtering can be done via RUST_LOG env var in browser console.
166        // Format parameter is ignored in WASM (always logs to console)
167        let _ = format; // Suppress unused warning
168        tracing_wasm::set_as_global_default_with_config(WASMLayerConfig::default());
169
170        Ok(())
171    }
172
173    #[cfg(not(target_arch = "wasm32"))]
174    {
175        // Native builds use tracing-subscriber
176        // Determine log level from parameter, env var, or default
177        let log_level = level
178            .map(|s| s.to_string())
179            .or_else(|| std::env::var("FIGUREHEAD_LOG_LEVEL").ok())
180            .or_else(|| std::env::var("RUST_LOG").ok())
181            .unwrap_or_else(|| "info".to_string());
182
183        // Determine format from parameter, env var, or default
184        let log_format = format
185            .map(|s| s.to_string())
186            .or_else(|| std::env::var("FIGUREHEAD_LOG_FORMAT").ok())
187            .unwrap_or_else(|| "compact".to_string());
188
189        // Parse log level
190        let filter = if log_level == "off" {
191            EnvFilter::new("off")
192        } else {
193            EnvFilter::try_from_default_env()
194                .or_else(|_| EnvFilter::try_new(&log_level))
195                .unwrap_or_else(|_| EnvFilter::new("info"))
196        };
197
198        // Parse format
199        let format =
200            LogFormat::from_str(&log_format).map_err(|e| format!("Invalid log format: {}", e))?;
201
202        // Build subscriber based on format
203        match format {
204            LogFormat::Compact => {
205                Registry::default()
206                    .with(filter)
207                    .with(
208                        fmt::Layer::default()
209                            .with_target(false)
210                            .with_level(true)
211                            .with_file(false)
212                            .with_line_number(false)
213                            .with_span_events(FmtSpan::NONE),
214                    )
215                    .try_init()?;
216            }
217            LogFormat::Pretty => {
218                Registry::default()
219                    .with(filter)
220                    .with(
221                        fmt::Layer::default()
222                            .with_target(true)
223                            .with_level(true)
224                            .with_file(true)
225                            .with_line_number(true)
226                            .with_span_events(FmtSpan::ACTIVE)
227                            .pretty(),
228                    )
229                    .try_init()?;
230            }
231            LogFormat::Json => {
232                Registry::default()
233                    .with(filter)
234                    .with(
235                        fmt::Layer::default()
236                            .with_target(true)
237                            .with_level(true)
238                            .with_file(true)
239                            .with_line_number(true)
240                            .with_span_events(FmtSpan::ACTIVE)
241                            .json(),
242                    )
243                    .try_init()?;
244            }
245        }
246
247        Ok(())
248    }
249}
250
251/// Initialize logging with default settings (info level, compact format)
252///
253/// This is a convenience function that calls `init_logging(None, None)`.
254pub fn init_default_logging() -> Result<(), Box<dyn std::error::Error>> {
255    init_logging(None, None)
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_log_format_parsing() {
264        assert_eq!(LogFormat::from_str("compact").unwrap(), LogFormat::Compact);
265        assert_eq!(LogFormat::from_str("pretty").unwrap(), LogFormat::Pretty);
266        assert_eq!(LogFormat::from_str("json").unwrap(), LogFormat::Json);
267        assert_eq!(LogFormat::from_str("COMPACT").unwrap(), LogFormat::Compact);
268        assert!(LogFormat::from_str("invalid").is_err());
269    }
270
271    #[test]
272    fn test_log_format_variants() {
273        let variants = LogFormat::variants();
274        assert!(variants.contains(&"compact"));
275        assert!(variants.contains(&"pretty"));
276        assert!(variants.contains(&"json"));
277    }
278}