Skip to main content

ronn_core/
logging.rs

1//! Structured logging configuration for RONN.
2//!
3//! This module provides centralized logging setup using the `tracing` crate
4//! for structured, contextual logging throughout the RONN runtime.
5
6use tracing::Level;
7use tracing_subscriber::{
8    EnvFilter,
9    fmt::{self, format::FmtSpan},
10    layer::SubscriberExt,
11    util::SubscriberInitExt,
12};
13
14/// Logging configuration for RONN runtime.
15#[derive(Debug, Clone)]
16pub struct LoggingConfig {
17    /// Minimum log level to display
18    pub level: LogLevel,
19    /// Whether to include timestamps
20    pub with_timestamps: bool,
21    /// Whether to include thread IDs
22    pub with_thread_ids: bool,
23    /// Whether to include source code locations
24    pub with_source_location: bool,
25    /// Whether to log span events (enter/exit)
26    pub with_span_events: bool,
27    /// Whether to output in JSON format
28    pub json_format: bool,
29}
30
31/// Log level configuration.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum LogLevel {
34    /// Trace-level logging (most verbose)
35    Trace,
36    /// Debug-level logging
37    Debug,
38    /// Info-level logging
39    Info,
40    /// Warn-level logging
41    Warn,
42    /// Error-level logging (least verbose)
43    Error,
44}
45
46impl LogLevel {
47    /// Convert to tracing::Level
48    fn to_tracing_level(&self) -> Level {
49        match self {
50            LogLevel::Trace => Level::TRACE,
51            LogLevel::Debug => Level::DEBUG,
52            LogLevel::Info => Level::INFO,
53            LogLevel::Warn => Level::WARN,
54            LogLevel::Error => Level::ERROR,
55        }
56    }
57}
58
59impl Default for LoggingConfig {
60    fn default() -> Self {
61        Self {
62            level: LogLevel::Info,
63            with_timestamps: true,
64            with_thread_ids: false,
65            with_source_location: false,
66            with_span_events: false,
67            json_format: false,
68        }
69    }
70}
71
72impl LoggingConfig {
73    /// Create a new logging configuration with default settings.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Set the minimum log level.
79    pub fn with_level(mut self, level: LogLevel) -> Self {
80        self.level = level;
81        self
82    }
83
84    /// Enable or disable timestamps.
85    pub fn with_timestamps(mut self, enable: bool) -> Self {
86        self.with_timestamps = enable;
87        self
88    }
89
90    /// Enable or disable thread IDs.
91    pub fn with_thread_ids(mut self, enable: bool) -> Self {
92        self.with_thread_ids = enable;
93        self
94    }
95
96    /// Enable or disable source code locations.
97    pub fn with_source_location(mut self, enable: bool) -> Self {
98        self.with_source_location = enable;
99        self
100    }
101
102    /// Enable or disable span event logging.
103    pub fn with_span_events(mut self, enable: bool) -> Self {
104        self.with_span_events = enable;
105        self
106    }
107
108    /// Enable or disable JSON output format.
109    pub fn with_json_format(mut self, enable: bool) -> Self {
110        self.json_format = enable;
111        self
112    }
113
114    /// Create a development-friendly configuration (verbose).
115    pub fn development() -> Self {
116        Self {
117            level: LogLevel::Debug,
118            with_timestamps: true,
119            with_thread_ids: true,
120            with_source_location: true,
121            with_span_events: true,
122            json_format: false,
123        }
124    }
125
126    /// Create a production-friendly configuration (minimal).
127    pub fn production() -> Self {
128        Self {
129            level: LogLevel::Info,
130            with_timestamps: true,
131            with_thread_ids: false,
132            with_source_location: false,
133            with_span_events: false,
134            json_format: true, // JSON for log aggregation
135        }
136    }
137}
138
139/// Initialize the global logger with the given configuration.
140///
141/// This should be called once at the start of the application.
142///
143/// # Example
144///
145/// ```no_run
146/// use ronn_core::logging::{init_logging, LoggingConfig};
147///
148/// init_logging(LoggingConfig::development());
149/// ```
150pub fn init_logging(config: LoggingConfig) {
151    let env_filter = EnvFilter::try_from_default_env()
152        .or_else(|_| EnvFilter::try_new(config.level.to_tracing_level().as_str()))
153        .unwrap_or_else(|_| EnvFilter::new("info"));
154
155    let span_events = if config.with_span_events {
156        FmtSpan::ENTER | FmtSpan::CLOSE
157    } else {
158        FmtSpan::NONE
159    };
160
161    if config.json_format {
162        // JSON format for production
163        let fmt_layer = fmt::layer()
164            .json()
165            .with_span_events(span_events)
166            .with_current_span(true)
167            .with_thread_ids(config.with_thread_ids)
168            .with_file(config.with_source_location)
169            .with_line_number(config.with_source_location);
170
171        tracing_subscriber::registry()
172            .with(env_filter)
173            .with(fmt_layer)
174            .init();
175    } else {
176        // Human-readable format for development
177        let fmt_layer = fmt::layer()
178            .with_span_events(span_events)
179            .with_thread_ids(config.with_thread_ids)
180            .with_file(config.with_source_location)
181            .with_line_number(config.with_source_location)
182            .with_target(config.with_source_location);
183
184        tracing_subscriber::registry()
185            .with(env_filter)
186            .with(fmt_layer)
187            .init();
188    }
189}
190
191/// Initialize logging with default configuration.
192///
193/// Convenience function that uses `LoggingConfig::default()`.
194pub fn init_default_logging() {
195    init_logging(LoggingConfig::default());
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_logging_config_default() {
204        let config = LoggingConfig::default();
205        assert_eq!(config.level, LogLevel::Info);
206        assert!(config.with_timestamps);
207        assert!(!config.with_thread_ids);
208    }
209
210    #[test]
211    fn test_logging_config_development() {
212        let config = LoggingConfig::development();
213        assert_eq!(config.level, LogLevel::Debug);
214        assert!(config.with_span_events);
215        assert!(!config.json_format);
216    }
217
218    #[test]
219    fn test_logging_config_production() {
220        let config = LoggingConfig::production();
221        assert_eq!(config.level, LogLevel::Info);
222        assert!(config.json_format);
223        assert!(!config.with_source_location);
224    }
225
226    #[test]
227    fn test_logging_config_builder() {
228        let config = LoggingConfig::new()
229            .with_level(LogLevel::Trace)
230            .with_timestamps(false)
231            .with_thread_ids(true)
232            .with_json_format(true);
233
234        assert_eq!(config.level, LogLevel::Trace);
235        assert!(!config.with_timestamps);
236        assert!(config.with_thread_ids);
237        assert!(config.json_format);
238    }
239}