Skip to main content

elara_runtime/observability/
logging.rs

1//! Structured logging system for ELARA Protocol
2//!
3//! This module provides structured, queryable logging with support for:
4//! - Multiple log levels (Trace, Debug, Info, Warn, Error)
5//! - Multiple output formats (JSON, Pretty, Compact)
6//! - Multiple output destinations (Stdout, Stderr, File)
7//! - Per-module log level configuration via RUST_LOG environment variable
8//! - Contextual fields (node_id, session_id, peer_id) in all log entries
9//!
10//! # Per-Module Log Levels
11//!
12//! You can configure different log levels for different modules using the `RUST_LOG`
13//! environment variable. This is useful for debugging specific components without
14//! overwhelming logs from other parts of the system.
15//!
16//! ## Examples
17//!
18//! ```bash
19//! # Set all modules to info, but elara_wire to debug
20//! RUST_LOG=info,elara_wire=debug
21//!
22//! # Set elara_crypto to trace, everything else to warn
23//! RUST_LOG=warn,elara_crypto=trace
24//!
25//! # Multiple module overrides
26//! RUST_LOG=info,elara_wire=debug,elara_state=trace,elara_transport=warn
27//! ```
28//!
29//! # Contextual Fields
30//!
31//! The logging system supports attaching contextual fields to log entries. These fields
32//! provide additional context about the operation being logged:
33//!
34//! - `node_id`: Identifier of the node generating the log
35//! - `session_id`: Current session identifier
36//! - `peer_id`: Identifier of the peer involved in the operation
37//!
38//! Use the `tracing` macros with field syntax to add contextual information:
39//!
40//! ```no_run
41//! use tracing::info;
42//!
43//! info!(
44//!     node_id = "node-1",
45//!     session_id = "session-abc",
46//!     peer_id = "peer-xyz",
47//!     "Connection established"
48//! );
49//! ```
50//!
51//! # Basic Example
52//!
53//! ```no_run
54//! use elara_runtime::observability::logging::{LoggingConfig, LogLevel, LogFormat, LogOutput, init_logging};
55//!
56//! let config = LoggingConfig {
57//!     level: LogLevel::Info,
58//!     format: LogFormat::Json,
59//!     output: LogOutput::Stdout,
60//! };
61//!
62//! init_logging(config).expect("Failed to initialize logging");
63//! ```
64
65use std::path::PathBuf;
66use std::sync::atomic::{AtomicBool, Ordering};
67use thiserror::Error;
68use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
69
70/// Global flag to track if logging has been initialized
71static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
72
73/// Reset the logging initialization flag.
74///
75/// **WARNING**: This function is only for testing purposes and should never be used
76/// in production code. It allows tests to re-initialize the logging system.
77///
78/// # Safety
79///
80/// This function is only available when running tests. Using this in production
81/// could lead to undefined behavior as it allows multiple initializations of global state.
82#[doc(hidden)]
83pub fn reset_logging_for_testing() {
84    LOGGING_INITIALIZED.store(false, Ordering::SeqCst);
85}
86
87/// Configuration for the logging system
88#[derive(Debug, Clone)]
89pub struct LoggingConfig {
90    /// Log level threshold
91    pub level: LogLevel,
92    /// Output format
93    pub format: LogFormat,
94    /// Output destination
95    pub output: LogOutput,
96}
97
98impl Default for LoggingConfig {
99    fn default() -> Self {
100        Self {
101            level: LogLevel::Info,
102            format: LogFormat::Pretty,
103            output: LogOutput::Stdout,
104        }
105    }
106}
107
108/// Log level enumeration
109#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
110pub enum LogLevel {
111    /// Trace level - most verbose
112    Trace,
113    /// Debug level - detailed information
114    Debug,
115    /// Info level - general information
116    Info,
117    /// Warn level - warnings
118    Warn,
119    /// Error level - errors only
120    Error,
121}
122
123impl LogLevel {
124    /// Convert to filter directive string for EnvFilter
125    fn to_filter_directive(&self) -> &'static str {
126        match self {
127            LogLevel::Trace => "trace",
128            LogLevel::Debug => "debug",
129            LogLevel::Info => "info",
130            LogLevel::Warn => "warn",
131            LogLevel::Error => "error",
132        }
133    }
134}
135
136/// Log output format
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum LogFormat {
139    /// Human-readable format for development
140    Pretty,
141    /// JSON format for production log aggregation
142    Json,
143    /// Compact format for high-throughput scenarios
144    Compact,
145}
146
147/// Log output destination
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum LogOutput {
150    /// Write to stdout
151    Stdout,
152    /// Write to stderr
153    Stderr,
154    /// Write to a file
155    File(PathBuf),
156}
157
158/// Errors that can occur during logging initialization
159#[derive(Debug, Error)]
160pub enum LoggingError {
161    /// Logging has already been initialized
162    #[error("Logging system has already been initialized")]
163    AlreadyInitialized,
164
165    /// Failed to set global default subscriber
166    #[error("Failed to set global default subscriber: {0}")]
167    SetGlobalDefaultFailed(String),
168
169    /// Failed to open log file
170    #[error("Failed to open log file: {0}")]
171    FileOpenFailed(#[from] std::io::Error),
172}
173
174/// Initialize the logging system with the given configuration
175///
176/// This function sets up the tracing subscriber with the specified configuration.
177/// It can only be called once - subsequent calls will return `LoggingError::AlreadyInitialized`.
178///
179/// # Per-Module Log Levels
180///
181/// The logging system respects the `RUST_LOG` environment variable for per-module
182/// log level configuration. If `RUST_LOG` is set, it takes precedence over the
183/// `config.level` parameter for fine-grained control.
184///
185/// The `config.level` serves as the default/fallback level when `RUST_LOG` is not set.
186///
187/// # Arguments
188///
189/// * `config` - Logging configuration specifying level, format, and output
190///
191/// # Returns
192///
193/// * `Ok(())` - Logging initialized successfully
194/// * `Err(LoggingError)` - Initialization failed
195///
196/// # Example
197///
198/// ```no_run
199/// use elara_runtime::observability::logging::{LoggingConfig, LogLevel, LogFormat, LogOutput, init_logging};
200///
201/// let config = LoggingConfig {
202///     level: LogLevel::Info,
203///     format: LogFormat::Json,
204///     output: LogOutput::Stdout,
205/// };
206///
207/// init_logging(config).expect("Failed to initialize logging");
208/// ```
209///
210/// # Idempotency
211///
212/// This function is idempotent in the sense that calling it multiple times will not
213/// reinitialize the logging system. The second and subsequent calls will return
214/// `Err(LoggingError::AlreadyInitialized)`.
215pub fn init_logging(config: LoggingConfig) -> Result<(), LoggingError> {
216    // Check if already initialized (atomic operation)
217    if LOGGING_INITIALIZED.swap(true, Ordering::SeqCst) {
218        return Err(LoggingError::AlreadyInitialized);
219    }
220
221    // Build EnvFilter that respects RUST_LOG environment variable
222    // Falls back to config.level if RUST_LOG is not set
223    let env_filter = EnvFilter::try_from_default_env()
224        .or_else(|_| {
225            // If RUST_LOG is not set, use the configured level as default
226            EnvFilter::try_new(format!("{}", config.level.to_filter_directive()))
227        })
228        .map_err(|e| LoggingError::SetGlobalDefaultFailed(format!("Failed to create EnvFilter: {}", e)))?;
229
230    // Build the fmt layer based on format and output configuration
231    let result = match (config.format, config.output) {
232        // JSON format to stdout
233        (LogFormat::Json, LogOutput::Stdout) => {
234            let fmt_layer = tracing_subscriber::fmt::layer()
235                .json()
236                .with_target(true)
237                .with_thread_ids(true)
238                .with_line_number(true)
239                .with_file(true);
240
241            tracing_subscriber::registry()
242                .with(env_filter)
243                .with(fmt_layer)
244                .try_init()
245        }
246
247        // JSON format to stderr
248        (LogFormat::Json, LogOutput::Stderr) => {
249            let fmt_layer = tracing_subscriber::fmt::layer()
250                .json()
251                .with_writer(std::io::stderr)
252                .with_target(true)
253                .with_thread_ids(true)
254                .with_line_number(true)
255                .with_file(true);
256
257            tracing_subscriber::registry()
258                .with(env_filter)
259                .with(fmt_layer)
260                .try_init()
261        }
262
263        // JSON format to file
264        (LogFormat::Json, LogOutput::File(path)) => {
265            let file = std::fs::OpenOptions::new()
266                .create(true)
267                .append(true)
268                .open(&path)?;
269
270            let fmt_layer = tracing_subscriber::fmt::layer()
271                .json()
272                .with_writer(file)
273                .with_target(true)
274                .with_thread_ids(true)
275                .with_line_number(true)
276                .with_file(true);
277
278            tracing_subscriber::registry()
279                .with(env_filter)
280                .with(fmt_layer)
281                .try_init()
282        }
283
284        // Pretty format to stdout
285        (LogFormat::Pretty, LogOutput::Stdout) => {
286            let fmt_layer = tracing_subscriber::fmt::layer()
287                .pretty()
288                .with_target(true)
289                .with_thread_ids(true)
290                .with_line_number(true)
291                .with_file(true);
292
293            tracing_subscriber::registry()
294                .with(env_filter)
295                .with(fmt_layer)
296                .try_init()
297        }
298
299        // Pretty format to stderr
300        (LogFormat::Pretty, LogOutput::Stderr) => {
301            let fmt_layer = tracing_subscriber::fmt::layer()
302                .pretty()
303                .with_writer(std::io::stderr)
304                .with_target(true)
305                .with_thread_ids(true)
306                .with_line_number(true)
307                .with_file(true);
308
309            tracing_subscriber::registry()
310                .with(env_filter)
311                .with(fmt_layer)
312                .try_init()
313        }
314
315        // Pretty format to file
316        (LogFormat::Pretty, LogOutput::File(path)) => {
317            let file = std::fs::OpenOptions::new()
318                .create(true)
319                .append(true)
320                .open(&path)?;
321
322            let fmt_layer = tracing_subscriber::fmt::layer()
323                .pretty()
324                .with_writer(file)
325                .with_target(true)
326                .with_thread_ids(true)
327                .with_line_number(true)
328                .with_file(true);
329
330            tracing_subscriber::registry()
331                .with(env_filter)
332                .with(fmt_layer)
333                .try_init()
334        }
335
336        // Compact format to stdout
337        (LogFormat::Compact, LogOutput::Stdout) => {
338            let fmt_layer = tracing_subscriber::fmt::layer()
339                .compact()
340                .with_target(true)
341                .with_thread_ids(true)
342                .with_line_number(true);
343
344            tracing_subscriber::registry()
345                .with(env_filter)
346                .with(fmt_layer)
347                .try_init()
348        }
349
350        // Compact format to stderr
351        (LogFormat::Compact, LogOutput::Stderr) => {
352            let fmt_layer = tracing_subscriber::fmt::layer()
353                .compact()
354                .with_writer(std::io::stderr)
355                .with_target(true)
356                .with_thread_ids(true)
357                .with_line_number(true);
358
359            tracing_subscriber::registry()
360                .with(env_filter)
361                .with(fmt_layer)
362                .try_init()
363        }
364
365        // Compact format to file
366        (LogFormat::Compact, LogOutput::File(path)) => {
367            let file = std::fs::OpenOptions::new()
368                .create(true)
369                .append(true)
370                .open(&path)?;
371
372            let fmt_layer = tracing_subscriber::fmt::layer()
373                .compact()
374                .with_writer(file)
375                .with_target(true)
376                .with_thread_ids(true)
377                .with_line_number(true);
378
379            tracing_subscriber::registry()
380                .with(env_filter)
381                .with(fmt_layer)
382                .try_init()
383        }
384    };
385
386    result.map_err(|e| {
387        let err_msg = e.to_string();
388        // If the error is that a global default is already set, and we're in test mode,
389        // we can safely ignore it since tests run serially with #[serial]
390        if err_msg.contains("global default trace dispatcher has already been set") {
391            // Don't reset the flag - logging is effectively initialized
392            return LoggingError::AlreadyInitialized;
393        }
394        // For other errors, reset flag
395        LOGGING_INITIALIZED.store(false, Ordering::SeqCst);
396        LoggingError::SetGlobalDefaultFailed(err_msg)
397    })?;
398
399    Ok(())
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    #[test]
407    fn test_log_level_ordering() {
408        assert!(LogLevel::Trace < LogLevel::Debug);
409        assert!(LogLevel::Debug < LogLevel::Info);
410        assert!(LogLevel::Info < LogLevel::Warn);
411        assert!(LogLevel::Warn < LogLevel::Error);
412    }
413
414    #[test]
415    fn test_default_config() {
416        let config = LoggingConfig::default();
417        assert_eq!(config.level, LogLevel::Info);
418        assert_eq!(config.format, LogFormat::Pretty);
419        assert_eq!(config.output, LogOutput::Stdout);
420    }
421}