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}