Skip to main content

rlg/
init.rs

1// init.rs
2// Copyright © 2024-2026 RustLogs (RLG). All rights reserved.
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6//! One-call initialization for the RLG engine.
7//!
8//! ```rust,no_run
9//! // Sensible defaults — auto-detects format (TTY → Logfmt, pipe → JSON).
10//! let _guard = rlg::init().unwrap();
11//!
12//! // Custom configuration via builder.
13//! let _guard = rlg::builder()
14//!     .level(rlg::LogLevel::DEBUG)
15//!     .format(rlg::LogFormat::JSON)
16//!     .init()
17//!     .unwrap();
18//! ```
19
20use crate::engine::ENGINE;
21use crate::log_format::LogFormat;
22use crate::log_level::LogLevel;
23use crate::logger::{RlgLogger, to_log_level_filter};
24use std::fmt;
25use std::sync::OnceLock;
26
27/// Auto-detect the output format from the execution context.
28///
29/// - **TTY** → `Logfmt` (human-readable key=value)
30/// - **Pipe / file / CI** → `JSON` (machine-parseable)
31/// - **`RLG_ENV=production`** → `JSON`
32fn detect_default_format() -> LogFormat {
33    detect_default_format_for(
34        std::env::var("RLG_ENV").ok().as_deref(),
35        atty_stdout(),
36    )
37}
38
39/// Pure helper for [`detect_default_format`] — exposed to tests so the
40/// TTY/pipe branches can be exercised without an actual terminal.
41const fn detect_default_format_for(
42    rlg_env: Option<&str>,
43    is_tty: bool,
44) -> LogFormat {
45    match rlg_env {
46        Some(s) if matches!(s.as_bytes(), b"production") => {
47            LogFormat::JSON
48        }
49        _ => {
50            if is_tty {
51                LogFormat::Logfmt
52            } else {
53                LogFormat::JSON
54            }
55        }
56    }
57}
58
59/// Returns `true` if stdout is connected to a terminal.
60fn atty_stdout() -> bool {
61    use std::io::IsTerminal;
62    std::io::stdout().is_terminal()
63}
64
65/// Parse `RUST_LOG` for a level filter (e.g., `RUST_LOG=debug`).
66///
67/// Accepts `RUST_LOG=<level>` and `RUST_LOG=<crate>=<level>`.
68/// Returns the most permissive level found. Returns `None` if unset.
69fn parse_rust_log() -> Option<LogLevel> {
70    let val = std::env::var("RUST_LOG").ok()?;
71    let mut most_permissive: Option<LogLevel> = None;
72    for directive in val.split(',') {
73        let level_str = directive
74            .split('=')
75            .next_back()
76            .unwrap_or(directive)
77            .trim();
78        if let Ok(level) = level_str.parse::<LogLevel>() {
79            match most_permissive {
80                None => most_permissive = Some(level),
81                Some(current)
82                    if level.to_numeric() < current.to_numeric() =>
83                {
84                    most_permissive = Some(level);
85                }
86                _ => {}
87            }
88        }
89    }
90    most_permissive
91}
92
93/// Prevents double initialization via `OnceLock` (set-once semantics).
94static INIT_GUARD: OnceLock<()> = OnceLock::new();
95
96/// `&'static` logger instance required by `log::set_logger`.
97static LOGGER: OnceLock<RlgLogger> = OnceLock::new();
98
99/// Initialization failures.
100#[derive(Debug, Clone, Copy)]
101pub enum InitError {
102    /// A `log` crate logger was already registered globally.
103    LoggerAlreadySet,
104    /// A `tracing` subscriber was already registered globally.
105    SubscriberAlreadySet,
106    /// `rlg::init()` or `builder().init()` was called more than once.
107    AlreadyInitialized,
108}
109
110impl fmt::Display for InitError {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            Self::LoggerAlreadySet => {
114                f.write_str("a log crate logger was already set")
115            }
116            Self::SubscriberAlreadySet => {
117                f.write_str("a tracing subscriber was already set")
118            }
119            Self::AlreadyInitialized => {
120                f.write_str("rlg was already initialized")
121            }
122        }
123    }
124}
125
126impl std::error::Error for InitError {}
127
128/// Builder for customizing RLG initialization.
129#[derive(Debug, Clone, Copy)]
130pub struct RlgBuilder {
131    level: LogLevel,
132    format: LogFormat,
133    install_log: bool,
134    install_tracing: bool,
135}
136
137impl Default for RlgBuilder {
138    fn default() -> Self {
139        Self {
140            level: LogLevel::INFO,
141            format: detect_default_format(),
142            install_log: true,
143            install_tracing: true,
144        }
145    }
146}
147
148impl RlgBuilder {
149    /// Set the minimum severity level. Events below this are dropped.
150    #[must_use]
151    pub const fn level(mut self, level: LogLevel) -> Self {
152        self.level = level;
153        self
154    }
155
156    /// Set the default output format. Overrides auto-detection.
157    #[must_use]
158    pub const fn format(mut self, format: LogFormat) -> Self {
159        self.format = format;
160        self
161    }
162
163    /// Skip installing the `log` crate facade bridge.
164    #[must_use]
165    pub const fn without_log(mut self) -> Self {
166        self.install_log = false;
167        self
168    }
169
170    /// Skip installing the `tracing` global subscriber.
171    #[must_use]
172    pub const fn without_tracing(mut self) -> Self {
173        self.install_tracing = false;
174        self
175    }
176
177    /// Register `RlgLogger` as the global `log` facade.
178    ///
179    /// # Errors
180    ///
181    /// Returns `InitError::LoggerAlreadySet` if another logger was already registered.
182    pub(crate) fn install_log_facade(
183        format: LogFormat,
184        level: LogLevel,
185    ) -> Result<(), InitError> {
186        let logger = LOGGER.get_or_init(|| RlgLogger::new(format));
187        log::set_logger(logger)
188            .map_err(|_| InitError::LoggerAlreadySet)?;
189        log::set_max_level(to_log_level_filter(level));
190        Ok(())
191    }
192
193    /// Register `RlgSubscriber` as the global `tracing` dispatcher.
194    ///
195    /// # Errors
196    ///
197    /// Returns `InitError::SubscriberAlreadySet` if another subscriber was already registered.
198    pub(crate) fn install_tracing_subscriber() -> Result<(), InitError>
199    {
200        let subscriber = crate::tracing::RlgSubscriber::new();
201        let dispatch =
202            tracing_core::dispatcher::Dispatch::new(subscriber);
203        tracing_core::dispatcher::set_global_default(dispatch)
204            .map_err(|_| InitError::SubscriberAlreadySet)?;
205        Ok(())
206    }
207
208    /// Finalize and install RLG as the global logger and subscriber.
209    ///
210    /// Applies `RUST_LOG` overrides and auto-detects the format
211    /// (TTY → Logfmt, pipe → JSON) when none was explicitly set.
212    ///
213    /// # Errors
214    ///
215    /// Returns `InitError` if a global logger/subscriber already exists
216    /// or if `init()` was already called.
217    pub fn init(mut self) -> Result<FlushGuard, InitError> {
218        if INIT_GUARD.set(()).is_err() {
219            return Err(InitError::AlreadyInitialized);
220        }
221
222        // Apply RUST_LOG level override.
223        if let Some(env_level) = parse_rust_log() {
224            self.level = env_level;
225        }
226
227        // Set engine filter level
228        ENGINE.set_filter(self.level.to_numeric());
229
230        // Install log facade
231        if self.install_log {
232            Self::install_log_facade(self.format, self.level)?;
233        }
234
235        // Install tracing subscriber
236        if self.install_tracing {
237            Self::install_tracing_subscriber()?;
238        }
239
240        Ok(FlushGuard { _private: () })
241    }
242}
243
244/// Create a new [`RlgBuilder`] for custom initialization.
245#[must_use]
246pub fn builder() -> RlgBuilder {
247    RlgBuilder::default()
248}
249
250/// RAII guard (resource-cleanup-on-drop) that flushes pending events on drop.
251///
252/// Returned by [`init`] and [`RlgBuilder::init`]. **Hold it in `main`** —
253/// dropping it calls [`ENGINE.shutdown()`](crate::engine::LockFreeEngine::shutdown).
254///
255/// ```rust,no_run
256/// let _guard = rlg::init().unwrap();
257/// // … application code …
258/// // ← guard drops here, flushing all pending logs
259/// ```
260#[derive(Debug)]
261pub struct FlushGuard {
262    _private: (),
263}
264
265impl Drop for FlushGuard {
266    fn drop(&mut self) {
267        ENGINE.shutdown();
268    }
269}
270
271/// Initialize RLG with sensible defaults.
272///
273/// Auto-detects format (TTY → Logfmt, pipe → JSON) and respects `RUST_LOG`.
274///
275/// # Errors
276///
277/// Returns `InitError` if a global logger or subscriber already exists.
278pub fn init() -> Result<FlushGuard, InitError> {
279    builder().init()
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use serial_test::serial;
286
287    /// SAFETY shim: tests in this module manipulate process-wide env vars
288    /// — they must run serially.
289    #[allow(unsafe_code)]
290    fn set_env(key: &str, val: &str) {
291        // SAFETY: tests using this helper are `#[serial]`, so no other
292        // thread reads env vars concurrently.
293        unsafe { std::env::set_var(key, val) };
294    }
295
296    #[allow(unsafe_code)]
297    fn unset_env(key: &str) {
298        // SAFETY: see `set_env`.
299        unsafe { std::env::remove_var(key) };
300    }
301
302    #[test]
303    #[serial]
304    #[cfg_attr(miri, ignore)]
305    fn parse_rust_log_returns_most_permissive() {
306        set_env("RUST_LOG", "warn,my_crate=debug,other=info");
307        let level = parse_rust_log();
308        assert_eq!(level, Some(LogLevel::DEBUG));
309        unset_env("RUST_LOG");
310    }
311
312    #[test]
313    #[serial]
314    #[cfg_attr(miri, ignore)]
315    fn parse_rust_log_simple_level() {
316        set_env("RUST_LOG", "trace");
317        assert_eq!(parse_rust_log(), Some(LogLevel::TRACE));
318        unset_env("RUST_LOG");
319    }
320
321    #[test]
322    #[serial]
323    #[cfg_attr(miri, ignore)]
324    fn parse_rust_log_ignores_unparseable_directives() {
325        set_env("RUST_LOG", "garbage_xyz,info");
326        assert_eq!(parse_rust_log(), Some(LogLevel::INFO));
327        unset_env("RUST_LOG");
328    }
329
330    #[test]
331    #[serial]
332    #[cfg_attr(miri, ignore)]
333    fn parse_rust_log_returns_none_when_unset() {
334        unset_env("RUST_LOG");
335        assert!(parse_rust_log().is_none());
336    }
337
338    #[test]
339    #[serial]
340    #[cfg_attr(miri, ignore)]
341    fn detect_default_format_production_env_forces_json() {
342        set_env("RLG_ENV", "production");
343        assert_eq!(detect_default_format(), LogFormat::JSON);
344        unset_env("RLG_ENV");
345    }
346
347    #[test]
348    #[serial]
349    #[cfg_attr(miri, ignore)]
350    fn detect_default_format_outside_production_picks_via_tty() {
351        unset_env("RLG_ENV");
352        // Outside production the answer depends on stdout being a TTY.
353        // Either branch is acceptable — what matters is that the function
354        // runs without panicking and returns one of the two valid choices.
355        let fmt = detect_default_format();
356        assert!(fmt == LogFormat::JSON || fmt == LogFormat::Logfmt);
357    }
358
359    #[test]
360    fn detect_default_format_for_covers_every_branch() {
361        // production → JSON regardless of TTY.
362        assert_eq!(
363            detect_default_format_for(Some("production"), true),
364            LogFormat::JSON
365        );
366        assert_eq!(
367            detect_default_format_for(Some("production"), false),
368            LogFormat::JSON
369        );
370        // Non-production + TTY → Logfmt (the branch real-life tests
371        // cannot reach because stdout is piped in `cargo test`).
372        assert_eq!(
373            detect_default_format_for(None, true),
374            LogFormat::Logfmt
375        );
376        assert_eq!(
377            detect_default_format_for(Some("staging"), true),
378            LogFormat::Logfmt
379        );
380        // Non-production + pipe → JSON.
381        assert_eq!(
382            detect_default_format_for(None, false),
383            LogFormat::JSON
384        );
385    }
386
387    #[test]
388    fn atty_stdout_runs() {
389        // Only verifying the call is reachable.
390        let _ = atty_stdout();
391    }
392
393    #[test]
394    fn test_init_error_display_logger_already_set() {
395        let err = InitError::LoggerAlreadySet;
396        assert_eq!(
397            err.to_string(),
398            "a log crate logger was already set"
399        );
400    }
401
402    #[test]
403    fn test_init_error_display_subscriber_already_set() {
404        let err = InitError::SubscriberAlreadySet;
405        assert_eq!(
406            err.to_string(),
407            "a tracing subscriber was already set"
408        );
409    }
410
411    #[test]
412    fn test_init_error_display_already_initialized() {
413        let err = InitError::AlreadyInitialized;
414        assert_eq!(err.to_string(), "rlg was already initialized");
415    }
416
417    #[test]
418    fn test_init_error_debug() {
419        let err = InitError::LoggerAlreadySet;
420        assert_eq!(format!("{err:?}"), "LoggerAlreadySet");
421    }
422
423    #[test]
424    fn test_init_error_clone_copy() {
425        let err = InitError::AlreadyInitialized;
426        let cloned = err;
427        assert_eq!(format!("{err:?}"), format!("{cloned:?}"));
428    }
429
430    #[test]
431    fn test_init_error_is_error() {
432        let err = InitError::LoggerAlreadySet;
433        // Verify it implements std::error::Error
434        let _: &dyn std::error::Error = &err;
435    }
436
437    #[test]
438    fn test_builder_defaults() {
439        let b = RlgBuilder::default();
440        assert_eq!(b.level, LogLevel::INFO);
441        assert!(b.install_log);
442        assert!(b.install_tracing);
443        // Format is auto-detected (Logfmt for TTY, JSON for pipe/CI)
444        assert!(
445            b.format == LogFormat::JSON
446                || b.format == LogFormat::Logfmt
447        );
448    }
449
450    #[test]
451    fn test_builder_level() {
452        let b = builder().level(LogLevel::DEBUG);
453        assert_eq!(b.level, LogLevel::DEBUG);
454    }
455
456    #[test]
457    fn test_builder_format() {
458        let b = builder().format(LogFormat::JSON);
459        assert_eq!(b.format, LogFormat::JSON);
460    }
461
462    #[test]
463    fn test_builder_without_log() {
464        let b = builder().without_log();
465        assert!(!b.install_log);
466        assert!(b.install_tracing);
467    }
468
469    #[test]
470    fn test_builder_without_tracing() {
471        let b = builder().without_tracing();
472        assert!(b.install_log);
473        assert!(!b.install_tracing);
474    }
475
476    #[test]
477    fn test_builder_chaining() {
478        let b = builder()
479            .level(LogLevel::TRACE)
480            .format(LogFormat::ECS)
481            .without_log()
482            .without_tracing();
483        assert_eq!(b.level, LogLevel::TRACE);
484        assert_eq!(b.format, LogFormat::ECS);
485        assert!(!b.install_log);
486        assert!(!b.install_tracing);
487    }
488
489    #[test]
490    fn test_builder_clone_copy() {
491        let b = builder().level(LogLevel::WARN);
492        let b2 = b;
493        // Both usable since RlgBuilder is Copy
494        assert_eq!(b.level, b2.level);
495        assert_eq!(b.format, b2.format);
496    }
497
498    #[test]
499    fn test_builder_without_facades_configuration() {
500        let b = builder().without_log().without_tracing();
501        assert!(!b.install_log);
502        assert!(!b.install_tracing);
503    }
504
505    #[test]
506    fn test_builder_fn() {
507        let b = builder();
508        assert_eq!(b.level, LogLevel::INFO);
509        // Format is auto-detected based on output context
510        assert!(
511            b.format == LogFormat::JSON
512                || b.format == LogFormat::Logfmt
513        );
514        assert!(b.install_log);
515        assert!(b.install_tracing);
516    }
517
518    #[test]
519    fn test_init_error_source() {
520        let err = InitError::LoggerAlreadySet;
521        // std::error::Error::source should return None
522        assert!(std::error::Error::source(&err).is_none());
523    }
524
525    #[test]
526    fn test_builder_default_impl() {
527        let b1 = RlgBuilder::default();
528        let b2 = builder();
529        assert_eq!(b1.level, b2.level);
530        assert_eq!(b1.format, b2.format);
531        assert_eq!(b1.install_log, b2.install_log);
532        assert_eq!(b1.install_tracing, b2.install_tracing);
533    }
534
535    #[test]
536    fn test_init_error_all_display_variants() {
537        // Exercise all three Display paths
538        let msgs: Vec<String> = vec![
539            InitError::LoggerAlreadySet,
540            InitError::SubscriberAlreadySet,
541            InitError::AlreadyInitialized,
542        ]
543        .into_iter()
544        .map(|e| e.to_string())
545        .collect();
546        assert_eq!(msgs.len(), 3);
547        assert!(msgs[0].contains("log"));
548        assert!(msgs[1].contains("tracing"));
549        assert!(msgs[2].contains("already initialized"));
550    }
551
552    #[test]
553    #[cfg_attr(miri, ignore)]
554    fn test_init_guard_static() {
555        // Exercise the OnceLock guard
556        // First attempt may succeed or fail depending on test ordering
557        let _ = INIT_GUARD.set(());
558        // Second attempt should always fail
559        assert!(INIT_GUARD.set(()).is_err());
560    }
561
562    #[test]
563    #[cfg_attr(miri, ignore)]
564    fn test_logger_static() {
565        // Exercise the LOGGER OnceLock
566        let logger =
567            LOGGER.get_or_init(|| RlgLogger::new(LogFormat::JSON));
568        assert!(format!("{logger:?}").contains("RlgLogger"));
569    }
570
571    #[test]
572    #[cfg_attr(miri, ignore)]
573    fn test_install_log_facade() {
574        // First call may succeed or fail (test ordering is non-deterministic)
575        let r1 = RlgBuilder::install_log_facade(
576            LogFormat::JSON,
577            LogLevel::INFO,
578        );
579        assert!(
580            r1.is_ok()
581                || matches!(r1, Err(InitError::LoggerAlreadySet))
582        );
583        // Second call should definitely fail
584        let r2 = RlgBuilder::install_log_facade(
585            LogFormat::MCP,
586            LogLevel::DEBUG,
587        );
588        assert!(matches!(r2, Err(InitError::LoggerAlreadySet)));
589    }
590
591    #[test]
592    #[cfg_attr(miri, ignore)]
593    fn test_install_tracing_subscriber() {
594        // First call may succeed or fail (test ordering is non-deterministic)
595        let r1 = RlgBuilder::install_tracing_subscriber();
596        assert!(
597            r1.is_ok()
598                || matches!(r1, Err(InitError::SubscriberAlreadySet))
599        );
600        // Second call should definitely fail
601        let r2 = RlgBuilder::install_tracing_subscriber();
602        assert!(matches!(r2, Err(InitError::SubscriberAlreadySet)));
603    }
604}