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
263        .iter()
264        .any(|t| matches!(t, LogTarget::Stdout | LogTarget::Stderr));
265    let file_target = targets.iter().find_map(|t| {
266        if let LogTarget::File {
267            directory,
268            filename_prefix,
269            rotation,
270        } = t
271        {
272            Some((directory.clone(), filename_prefix.clone(), rotation.clone()))
273        } else {
274            None
275        }
276    });
277
278    let registry = tracing_subscriber::registry().with(filter_layer);
279
280    if has_console && file_target.is_some() {
281        let (directory, filename_prefix, rotation_config) = file_target.unwrap();
282
283        let rotation = match rotation_config.strategy {
284            RotationStrategy::Daily => Rotation::DAILY,
285            RotationStrategy::Hourly => Rotation::HOURLY,
286            RotationStrategy::Minutely => Rotation::MINUTELY,
287            RotationStrategy::Never => Rotation::NEVER,
288        };
289
290        let file_appender = RollingFileAppender::new(rotation, &directory, &filename_prefix);
291        let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
292        std::mem::forget(_guard);
293
294        // Console layer (pretty format)
295        let console_layer = fmt::layer()
296            .pretty()
297            .with_timer(UtcTime::rfc_3339())
298            .with_file(config.include_location)
299            .with_line_number(config.include_location)
300            .with_target(config.include_target)
301            .with_span_events(span_events.clone())
302            .with_ansi(config.ansi_colors)
303            .with_writer(std::io::stdout);
304
305        // File layer (JSON format)
306        let file_layer = fmt::layer()
307            .json()
308            .with_timer(UtcTime::rfc_3339())
309            .with_file(config.include_location)
310            .with_line_number(config.include_location)
311            .with_target(config.include_target)
312            .with_span_events(span_events)
313            .with_ansi(false)
314            .with_writer(non_blocking);
315
316        registry.with(console_layer).with(file_layer).init();
317    } else if has_console {
318        // Just console
319        init_stdout_with_reload(
320            config,
321            reload::Layer::new(build_env_filter(config)).0,
322            span_events,
323        )?;
324    } else if let Some((directory, filename_prefix, rotation_config)) = file_target {
325        // Just file
326        let reload_layer = reload::Layer::new(build_env_filter(config)).0;
327        init_file_with_reload(
328            config,
329            reload_layer,
330            span_events,
331            &directory,
332            &filename_prefix,
333            &rotation_config,
334        )?;
335    }
336
337    Ok(())
338}
339
340/// Initialize without dynamic level support (simpler, slightly more efficient).
341fn init_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
342    match &config.target {
343        LogTarget::Stdout => init_stdout_static(config, filter, span_events),
344        LogTarget::Stderr => init_stderr_static(config, filter, span_events),
345        LogTarget::File {
346            directory,
347            filename_prefix,
348            rotation,
349        } => init_file_static(
350            config,
351            filter,
352            span_events,
353            directory,
354            filename_prefix,
355            rotation,
356        ),
357        LogTarget::Multi(_) => {
358            // For multi-target without dynamic, we use the same logic as with dynamic
359            // but don't register a controller
360            init_with_dynamic_level(config, filter, span_events)
361        }
362    }
363}
364
365/// Initialize stdout without reload support.
366fn init_stdout_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
367    let registry = tracing_subscriber::registry().with(filter);
368
369    match config.format {
370        LogFormat::Json => {
371            let layer = fmt::layer()
372                .json()
373                .with_timer(UtcTime::rfc_3339())
374                .with_file(config.include_location)
375                .with_line_number(config.include_location)
376                .with_target(config.include_target)
377                .with_thread_ids(config.include_thread_ids)
378                .with_thread_names(config.include_thread_names)
379                .with_span_events(span_events)
380                .with_writer(std::io::stdout);
381            registry.with(layer).init();
382        }
383        LogFormat::Pretty => {
384            let layer = fmt::layer()
385                .pretty()
386                .with_timer(UtcTime::rfc_3339())
387                .with_file(config.include_location)
388                .with_line_number(config.include_location)
389                .with_target(config.include_target)
390                .with_thread_ids(config.include_thread_ids)
391                .with_thread_names(config.include_thread_names)
392                .with_span_events(span_events)
393                .with_ansi(config.ansi_colors)
394                .with_writer(std::io::stdout);
395            registry.with(layer).init();
396        }
397        LogFormat::Compact => {
398            let layer = fmt::layer()
399                .compact()
400                .with_timer(UtcTime::rfc_3339())
401                .with_file(config.include_location)
402                .with_line_number(config.include_location)
403                .with_target(config.include_target)
404                .with_thread_ids(config.include_thread_ids)
405                .with_thread_names(config.include_thread_names)
406                .with_span_events(span_events)
407                .with_ansi(config.ansi_colors)
408                .with_writer(std::io::stdout);
409            registry.with(layer).init();
410        }
411        LogFormat::Full => {
412            let layer = fmt::layer()
413                .with_timer(UtcTime::rfc_3339())
414                .with_file(config.include_location)
415                .with_line_number(config.include_location)
416                .with_target(config.include_target)
417                .with_thread_ids(config.include_thread_ids)
418                .with_thread_names(config.include_thread_names)
419                .with_span_events(span_events)
420                .with_ansi(config.ansi_colors)
421                .with_writer(std::io::stdout);
422            registry.with(layer).init();
423        }
424    }
425
426    Ok(())
427}
428
429/// Initialize stderr without reload support.
430fn init_stderr_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
431    let registry = tracing_subscriber::registry().with(filter);
432
433    let layer = fmt::layer()
434        .with_timer(UtcTime::rfc_3339())
435        .with_file(config.include_location)
436        .with_line_number(config.include_location)
437        .with_target(config.include_target)
438        .with_thread_ids(config.include_thread_ids)
439        .with_thread_names(config.include_thread_names)
440        .with_span_events(span_events)
441        .with_ansi(config.ansi_colors)
442        .with_writer(std::io::stderr);
443
444    registry.with(layer).init();
445    Ok(())
446}
447
448/// Initialize file logging without reload support.
449fn init_file_static(
450    config: &LogConfig,
451    filter: EnvFilter,
452    span_events: FmtSpan,
453    directory: &Path,
454    filename_prefix: &str,
455    rotation_config: &super::rotation::RotationConfig,
456) -> Result<()> {
457    let rotation = match rotation_config.strategy {
458        RotationStrategy::Daily => Rotation::DAILY,
459        RotationStrategy::Hourly => Rotation::HOURLY,
460        RotationStrategy::Minutely => Rotation::MINUTELY,
461        RotationStrategy::Never => Rotation::NEVER,
462    };
463
464    let file_appender = RollingFileAppender::new(rotation, directory, filename_prefix);
465    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
466    std::mem::forget(_guard);
467
468    let registry = tracing_subscriber::registry().with(filter);
469
470    let layer = fmt::layer()
471        .json()
472        .with_timer(UtcTime::rfc_3339())
473        .with_file(config.include_location)
474        .with_line_number(config.include_location)
475        .with_target(config.include_target)
476        .with_thread_ids(config.include_thread_ids)
477        .with_thread_names(config.include_thread_names)
478        .with_span_events(span_events)
479        .with_ansi(false)
480        .with_writer(non_blocking);
481
482    registry.with(layer).init();
483    Ok(())
484}
485
486/// Build an EnvFilter from the configuration.
487fn build_env_filter(config: &LogConfig) -> EnvFilter {
488    // First, try to get filter from environment variable
489    EnvFilter::try_from_default_env().unwrap_or_else(|_| {
490        let filter_str = config.build_filter_string();
491        EnvFilter::try_new(&filter_str)
492            .unwrap_or_else(|_| EnvFilter::new(config.level.as_filter_str()))
493    })
494}
495
496/// Build FmtSpan from configuration.
497fn build_span_events(config: &LogConfig) -> FmtSpan {
498    if config.include_span_events {
499        FmtSpan::ENTER | FmtSpan::EXIT
500    } else {
501        FmtSpan::NONE
502    }
503}
504
505/// Initialize logging for tests.
506///
507/// This uses a test-friendly configuration and ignores errors
508/// from multiple initializations (common in tests).
509pub fn init_test_logging() {
510    let _ = init_logging(&LogConfig::test());
511}
512
513/// Check if logging has been initialized.
514pub fn is_logging_initialized() -> bool {
515    LOGGING_INITIALIZED.get().is_some()
516}
517
518#[cfg(test)]
519mod tests {
520    use super::super::config::LogLevel;
521    use super::*;
522
523    #[test]
524    fn test_build_env_filter() {
525        let config = LogConfig::builder().level(LogLevel::Debug).build();
526        let filter = build_env_filter(&config);
527        // Just verify it doesn't panic
528        drop(filter);
529    }
530
531    #[test]
532    fn test_build_span_events() {
533        let config = LogConfig::builder().include_span_events(true).build();
534        let span_events = build_span_events(&config);
535        // FmtSpan doesn't have is_empty, check it's configured correctly
536        assert_ne!(span_events, FmtSpan::NONE);
537
538        let config = LogConfig::builder().include_span_events(false).build();
539        let span_events = build_span_events(&config);
540        assert_eq!(span_events, FmtSpan::NONE);
541    }
542}