Skip to main content

mabi_core/logging/
init.rs

1//! Logging initialization and setup.
2//!
3//! This module handles the initialization of the tracing subscriber
4//! with various configurations including file logging with rotation.
5
6use std::path::Path;
7use std::sync::OnceLock;
8
9use tracing_appender::rolling::{RollingFileAppender, Rotation};
10use tracing_subscriber::{
11    fmt::{self, format::FmtSpan, time::UtcTime},
12    layer::SubscriberExt,
13    reload,
14    util::SubscriberInitExt,
15    EnvFilter,
16};
17
18use super::config::{LogConfig, LogFormat, LogTarget};
19use super::dynamic::LogLevelController;
20use super::rotation::RotationStrategy;
21use crate::error::Result;
22
23/// Global initialization guard to prevent double initialization.
24static LOGGING_INITIALIZED: OnceLock<()> = OnceLock::new();
25
26/// Initialize logging with the given configuration.
27///
28/// This function should be called once at application startup.
29/// Subsequent calls will be ignored (returns Ok without error).
30///
31/// # Arguments
32/// * `config` - The logging configuration
33///
34/// # Returns
35/// * `Ok(())` - Logging was initialized successfully or was already initialized
36/// * `Err(Error)` - Logging initialization failed
37///
38/// # Example
39///
40/// ```rust,no_run
41/// use mabi_core::logging::{init_logging, LogConfig};
42///
43/// init_logging(&LogConfig::default()).expect("Failed to initialize logging");
44/// ```
45pub fn init_logging(config: &LogConfig) -> Result<()> {
46    // Prevent double initialization
47    if LOGGING_INITIALIZED.get().is_some() {
48        return Ok(());
49    }
50
51    // Validate configuration
52    config.validate()?;
53
54    let result = init_logging_internal(config);
55
56    if result.is_ok() {
57        LOGGING_INITIALIZED.get_or_init(|| ());
58    }
59
60    result
61}
62
63/// Internal initialization logic.
64fn init_logging_internal(config: &LogConfig) -> Result<()> {
65    let filter = build_env_filter(config);
66    let span_events = build_span_events(config);
67
68    // Check if we need dynamic level support
69    if config.dynamic_level {
70        init_with_dynamic_level(config, filter, span_events)
71    } else {
72        init_static(config, filter, span_events)
73    }
74}
75
76/// Initialize with dynamic log level support.
77fn init_with_dynamic_level(
78    config: &LogConfig,
79    filter: EnvFilter,
80    span_events: FmtSpan,
81) -> Result<()> {
82    let (filter_layer, reload_handle) = reload::Layer::new(filter);
83
84    match &config.target {
85        LogTarget::Stdout => {
86            init_stdout_with_reload(config, filter_layer, span_events)?;
87        }
88        LogTarget::Stderr => {
89            init_stderr_with_reload(config, filter_layer, span_events)?;
90        }
91        LogTarget::File {
92            directory,
93            filename_prefix,
94            rotation,
95        } => {
96            init_file_with_reload(
97                config,
98                filter_layer,
99                span_events,
100                directory,
101                filename_prefix,
102                rotation,
103            )?;
104        }
105        LogTarget::Multi(targets) => {
106            // For multi-target, we'll initialize stdout as the primary with reload
107            // and add file as a secondary layer
108            init_multi_with_reload(config, filter_layer, span_events, targets)?;
109        }
110    }
111
112    // Register the controller
113    let controller = LogLevelController::new(reload_handle, config.level);
114    controller.register_global();
115
116    Ok(())
117}
118
119/// Initialize stdout with reload support.
120fn init_stdout_with_reload(
121    config: &LogConfig,
122    filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
123    span_events: FmtSpan,
124) -> Result<()> {
125    let registry = tracing_subscriber::registry().with(filter_layer);
126
127    match config.format {
128        LogFormat::Json => {
129            let layer = fmt::layer()
130                .json()
131                .with_timer(UtcTime::rfc_3339())
132                .with_file(config.include_location)
133                .with_line_number(config.include_location)
134                .with_target(config.include_target)
135                .with_thread_ids(config.include_thread_ids)
136                .with_thread_names(config.include_thread_names)
137                .with_span_events(span_events)
138                .with_writer(std::io::stdout);
139            registry.with(layer).init();
140        }
141        LogFormat::Pretty => {
142            let layer = fmt::layer()
143                .pretty()
144                .with_timer(UtcTime::rfc_3339())
145                .with_file(config.include_location)
146                .with_line_number(config.include_location)
147                .with_target(config.include_target)
148                .with_thread_ids(config.include_thread_ids)
149                .with_thread_names(config.include_thread_names)
150                .with_span_events(span_events)
151                .with_ansi(config.ansi_colors)
152                .with_writer(std::io::stdout);
153            registry.with(layer).init();
154        }
155        LogFormat::Compact => {
156            let layer = fmt::layer()
157                .compact()
158                .with_timer(UtcTime::rfc_3339())
159                .with_file(config.include_location)
160                .with_line_number(config.include_location)
161                .with_target(config.include_target)
162                .with_thread_ids(config.include_thread_ids)
163                .with_thread_names(config.include_thread_names)
164                .with_span_events(span_events)
165                .with_ansi(config.ansi_colors)
166                .with_writer(std::io::stdout);
167            registry.with(layer).init();
168        }
169        LogFormat::Full => {
170            let layer = fmt::layer()
171                .with_timer(UtcTime::rfc_3339())
172                .with_file(config.include_location)
173                .with_line_number(config.include_location)
174                .with_target(config.include_target)
175                .with_thread_ids(config.include_thread_ids)
176                .with_thread_names(config.include_thread_names)
177                .with_span_events(span_events)
178                .with_ansi(config.ansi_colors)
179                .with_writer(std::io::stdout);
180            registry.with(layer).init();
181        }
182    }
183
184    Ok(())
185}
186
187/// Initialize stderr with reload support.
188fn init_stderr_with_reload(
189    config: &LogConfig,
190    filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
191    span_events: FmtSpan,
192) -> Result<()> {
193    let registry = tracing_subscriber::registry().with(filter_layer);
194
195    let layer = fmt::layer()
196        .with_timer(UtcTime::rfc_3339())
197        .with_file(config.include_location)
198        .with_line_number(config.include_location)
199        .with_target(config.include_target)
200        .with_thread_ids(config.include_thread_ids)
201        .with_thread_names(config.include_thread_names)
202        .with_span_events(span_events)
203        .with_ansi(config.ansi_colors)
204        .with_writer(std::io::stderr);
205
206    registry.with(layer).init();
207    Ok(())
208}
209
210/// Initialize file logging with rotation and reload support.
211fn init_file_with_reload(
212    config: &LogConfig,
213    filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
214    span_events: FmtSpan,
215    directory: &Path,
216    filename_prefix: &str,
217    rotation_config: &super::rotation::RotationConfig,
218) -> Result<()> {
219    let rotation = match rotation_config.strategy {
220        RotationStrategy::Daily => Rotation::DAILY,
221        RotationStrategy::Hourly => Rotation::HOURLY,
222        RotationStrategy::Minutely => Rotation::MINUTELY,
223        RotationStrategy::Never => Rotation::NEVER,
224    };
225
226    let file_appender = RollingFileAppender::new(rotation, directory, filename_prefix);
227    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
228
229    // Keep the guard alive for the lifetime of the program
230    // by leaking it (it will be cleaned up when the program exits)
231    std::mem::forget(_guard);
232
233    let registry = tracing_subscriber::registry().with(filter_layer);
234
235    // File logging always uses JSON format for structured parsing
236    let layer = fmt::layer()
237        .json()
238        .with_timer(UtcTime::rfc_3339())
239        .with_file(config.include_location)
240        .with_line_number(config.include_location)
241        .with_target(config.include_target)
242        .with_thread_ids(config.include_thread_ids)
243        .with_thread_names(config.include_thread_names)
244        .with_span_events(span_events)
245        .with_ansi(false) // No ANSI codes in file output
246        .with_writer(non_blocking);
247
248    registry.with(layer).init();
249    Ok(())
250}
251
252/// Initialize multi-target logging with reload support.
253fn init_multi_with_reload(
254    config: &LogConfig,
255    filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
256    span_events: FmtSpan,
257    targets: &[LogTarget],
258) -> Result<()> {
259    // For simplicity, we handle stdout/stderr + file combination
260    // More complex combinations could be added as needed
261
262    let has_console = targets.iter().any(|t| matches!(t, LogTarget::Stdout | LogTarget::Stderr));
263    let file_target = targets.iter().find_map(|t| {
264        if let LogTarget::File { directory, filename_prefix, rotation } = t {
265            Some((directory.clone(), filename_prefix.clone(), rotation.clone()))
266        } else {
267            None
268        }
269    });
270
271    let registry = tracing_subscriber::registry().with(filter_layer);
272
273    if has_console && file_target.is_some() {
274        let (directory, filename_prefix, rotation_config) = file_target.unwrap();
275
276        let rotation = match rotation_config.strategy {
277            RotationStrategy::Daily => Rotation::DAILY,
278            RotationStrategy::Hourly => Rotation::HOURLY,
279            RotationStrategy::Minutely => Rotation::MINUTELY,
280            RotationStrategy::Never => Rotation::NEVER,
281        };
282
283        let file_appender = RollingFileAppender::new(rotation, &directory, &filename_prefix);
284        let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
285        std::mem::forget(_guard);
286
287        // Console layer (pretty format)
288        let console_layer = fmt::layer()
289            .pretty()
290            .with_timer(UtcTime::rfc_3339())
291            .with_file(config.include_location)
292            .with_line_number(config.include_location)
293            .with_target(config.include_target)
294            .with_span_events(span_events.clone())
295            .with_ansi(config.ansi_colors)
296            .with_writer(std::io::stdout);
297
298        // File layer (JSON format)
299        let file_layer = fmt::layer()
300            .json()
301            .with_timer(UtcTime::rfc_3339())
302            .with_file(config.include_location)
303            .with_line_number(config.include_location)
304            .with_target(config.include_target)
305            .with_span_events(span_events)
306            .with_ansi(false)
307            .with_writer(non_blocking);
308
309        registry.with(console_layer).with(file_layer).init();
310    } else if has_console {
311        // Just console
312        init_stdout_with_reload(config, reload::Layer::new(build_env_filter(config)).0, span_events)?;
313    } else if let Some((directory, filename_prefix, rotation_config)) = file_target {
314        // Just file
315        let reload_layer = reload::Layer::new(build_env_filter(config)).0;
316        init_file_with_reload(config, reload_layer, span_events, &directory, &filename_prefix, &rotation_config)?;
317    }
318
319    Ok(())
320}
321
322/// Initialize without dynamic level support (simpler, slightly more efficient).
323fn init_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
324    match &config.target {
325        LogTarget::Stdout => init_stdout_static(config, filter, span_events),
326        LogTarget::Stderr => init_stderr_static(config, filter, span_events),
327        LogTarget::File {
328            directory,
329            filename_prefix,
330            rotation,
331        } => init_file_static(config, filter, span_events, directory, filename_prefix, rotation),
332        LogTarget::Multi(_) => {
333            // For multi-target without dynamic, we use the same logic as with dynamic
334            // but don't register a controller
335            init_with_dynamic_level(config, filter, span_events)
336        }
337    }
338}
339
340/// Initialize stdout without reload support.
341fn init_stdout_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
342    let registry = tracing_subscriber::registry().with(filter);
343
344    match config.format {
345        LogFormat::Json => {
346            let layer = fmt::layer()
347                .json()
348                .with_timer(UtcTime::rfc_3339())
349                .with_file(config.include_location)
350                .with_line_number(config.include_location)
351                .with_target(config.include_target)
352                .with_thread_ids(config.include_thread_ids)
353                .with_thread_names(config.include_thread_names)
354                .with_span_events(span_events)
355                .with_writer(std::io::stdout);
356            registry.with(layer).init();
357        }
358        LogFormat::Pretty => {
359            let layer = fmt::layer()
360                .pretty()
361                .with_timer(UtcTime::rfc_3339())
362                .with_file(config.include_location)
363                .with_line_number(config.include_location)
364                .with_target(config.include_target)
365                .with_thread_ids(config.include_thread_ids)
366                .with_thread_names(config.include_thread_names)
367                .with_span_events(span_events)
368                .with_ansi(config.ansi_colors)
369                .with_writer(std::io::stdout);
370            registry.with(layer).init();
371        }
372        LogFormat::Compact => {
373            let layer = fmt::layer()
374                .compact()
375                .with_timer(UtcTime::rfc_3339())
376                .with_file(config.include_location)
377                .with_line_number(config.include_location)
378                .with_target(config.include_target)
379                .with_thread_ids(config.include_thread_ids)
380                .with_thread_names(config.include_thread_names)
381                .with_span_events(span_events)
382                .with_ansi(config.ansi_colors)
383                .with_writer(std::io::stdout);
384            registry.with(layer).init();
385        }
386        LogFormat::Full => {
387            let layer = fmt::layer()
388                .with_timer(UtcTime::rfc_3339())
389                .with_file(config.include_location)
390                .with_line_number(config.include_location)
391                .with_target(config.include_target)
392                .with_thread_ids(config.include_thread_ids)
393                .with_thread_names(config.include_thread_names)
394                .with_span_events(span_events)
395                .with_ansi(config.ansi_colors)
396                .with_writer(std::io::stdout);
397            registry.with(layer).init();
398        }
399    }
400
401    Ok(())
402}
403
404/// Initialize stderr without reload support.
405fn init_stderr_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
406    let registry = tracing_subscriber::registry().with(filter);
407
408    let layer = fmt::layer()
409        .with_timer(UtcTime::rfc_3339())
410        .with_file(config.include_location)
411        .with_line_number(config.include_location)
412        .with_target(config.include_target)
413        .with_thread_ids(config.include_thread_ids)
414        .with_thread_names(config.include_thread_names)
415        .with_span_events(span_events)
416        .with_ansi(config.ansi_colors)
417        .with_writer(std::io::stderr);
418
419    registry.with(layer).init();
420    Ok(())
421}
422
423/// Initialize file logging without reload support.
424fn init_file_static(
425    config: &LogConfig,
426    filter: EnvFilter,
427    span_events: FmtSpan,
428    directory: &Path,
429    filename_prefix: &str,
430    rotation_config: &super::rotation::RotationConfig,
431) -> Result<()> {
432    let rotation = match rotation_config.strategy {
433        RotationStrategy::Daily => Rotation::DAILY,
434        RotationStrategy::Hourly => Rotation::HOURLY,
435        RotationStrategy::Minutely => Rotation::MINUTELY,
436        RotationStrategy::Never => Rotation::NEVER,
437    };
438
439    let file_appender = RollingFileAppender::new(rotation, directory, filename_prefix);
440    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
441    std::mem::forget(_guard);
442
443    let registry = tracing_subscriber::registry().with(filter);
444
445    let layer = fmt::layer()
446        .json()
447        .with_timer(UtcTime::rfc_3339())
448        .with_file(config.include_location)
449        .with_line_number(config.include_location)
450        .with_target(config.include_target)
451        .with_thread_ids(config.include_thread_ids)
452        .with_thread_names(config.include_thread_names)
453        .with_span_events(span_events)
454        .with_ansi(false)
455        .with_writer(non_blocking);
456
457    registry.with(layer).init();
458    Ok(())
459}
460
461/// Build an EnvFilter from the configuration.
462fn build_env_filter(config: &LogConfig) -> EnvFilter {
463    // First, try to get filter from environment variable
464    EnvFilter::try_from_default_env().unwrap_or_else(|_| {
465        let filter_str = config.build_filter_string();
466        EnvFilter::try_new(&filter_str).unwrap_or_else(|_| {
467            EnvFilter::new(config.level.as_filter_str())
468        })
469    })
470}
471
472/// Build FmtSpan from configuration.
473fn build_span_events(config: &LogConfig) -> FmtSpan {
474    if config.include_span_events {
475        FmtSpan::ENTER | FmtSpan::EXIT
476    } else {
477        FmtSpan::NONE
478    }
479}
480
481/// Initialize logging for tests.
482///
483/// This uses a test-friendly configuration and ignores errors
484/// from multiple initializations (common in tests).
485pub fn init_test_logging() {
486    let _ = init_logging(&LogConfig::test());
487}
488
489/// Check if logging has been initialized.
490pub fn is_logging_initialized() -> bool {
491    LOGGING_INITIALIZED.get().is_some()
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use super::super::config::LogLevel;
498
499    #[test]
500    fn test_build_env_filter() {
501        let config = LogConfig::builder().level(LogLevel::Debug).build();
502        let filter = build_env_filter(&config);
503        // Just verify it doesn't panic
504        drop(filter);
505    }
506
507    #[test]
508    fn test_build_span_events() {
509        let config = LogConfig::builder().include_span_events(true).build();
510        let span_events = build_span_events(&config);
511        // FmtSpan doesn't have is_empty, check it's configured correctly
512        assert_ne!(span_events, FmtSpan::NONE);
513
514        let config = LogConfig::builder().include_span_events(false).build();
515        let span_events = build_span_events(&config);
516        assert_eq!(span_events, FmtSpan::NONE);
517    }
518}