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    if std::env::var("RLG_ENV")
34        .map(|v| v == "production")
35        .unwrap_or(false)
36    {
37        return LogFormat::JSON;
38    }
39    if atty_stdout() {
40        LogFormat::Logfmt
41    } else {
42        LogFormat::JSON
43    }
44}
45
46/// Returns `true` if stdout is connected to a terminal.
47fn atty_stdout() -> bool {
48    use std::io::IsTerminal;
49    std::io::stdout().is_terminal()
50}
51
52/// Parse `RUST_LOG` for a level filter (e.g., `RUST_LOG=debug`).
53///
54/// Accepts `RUST_LOG=<level>` and `RUST_LOG=<crate>=<level>`.
55/// Returns the most permissive level found. Returns `None` if unset.
56fn parse_rust_log() -> Option<LogLevel> {
57    let val = std::env::var("RUST_LOG").ok()?;
58    let mut most_permissive: Option<LogLevel> = None;
59    for directive in val.split(',') {
60        let level_str = directive
61            .split('=')
62            .next_back()
63            .unwrap_or(directive)
64            .trim();
65        if let Ok(level) = level_str.parse::<LogLevel>() {
66            match most_permissive {
67                None => most_permissive = Some(level),
68                Some(current)
69                    if level.to_numeric() < current.to_numeric() =>
70                {
71                    most_permissive = Some(level);
72                }
73                _ => {}
74            }
75        }
76    }
77    most_permissive
78}
79
80/// Prevents double initialization via `OnceLock` (set-once semantics).
81static INIT_GUARD: OnceLock<()> = OnceLock::new();
82
83/// `&'static` logger instance required by `log::set_logger`.
84static LOGGER: OnceLock<RlgLogger> = OnceLock::new();
85
86/// Initialization failures.
87#[derive(Debug, Clone, Copy)]
88pub enum InitError {
89    /// A `log` crate logger was already registered globally.
90    LoggerAlreadySet,
91    /// A `tracing` subscriber was already registered globally.
92    SubscriberAlreadySet,
93    /// `rlg::init()` or `builder().init()` was called more than once.
94    AlreadyInitialized,
95}
96
97impl fmt::Display for InitError {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::LoggerAlreadySet => {
101                f.write_str("a log crate logger was already set")
102            }
103            Self::SubscriberAlreadySet => {
104                f.write_str("a tracing subscriber was already set")
105            }
106            Self::AlreadyInitialized => {
107                f.write_str("rlg was already initialized")
108            }
109        }
110    }
111}
112
113impl std::error::Error for InitError {}
114
115/// Builder for customizing RLG initialization.
116#[derive(Debug, Clone, Copy)]
117pub struct RlgBuilder {
118    level: LogLevel,
119    format: LogFormat,
120    install_log: bool,
121    install_tracing: bool,
122}
123
124impl Default for RlgBuilder {
125    fn default() -> Self {
126        Self {
127            level: LogLevel::INFO,
128            format: detect_default_format(),
129            install_log: true,
130            install_tracing: true,
131        }
132    }
133}
134
135impl RlgBuilder {
136    /// Set the minimum severity level. Events below this are dropped.
137    #[must_use]
138    pub const fn level(mut self, level: LogLevel) -> Self {
139        self.level = level;
140        self
141    }
142
143    /// Set the default output format. Overrides auto-detection.
144    #[must_use]
145    pub const fn format(mut self, format: LogFormat) -> Self {
146        self.format = format;
147        self
148    }
149
150    /// Skip installing the `log` crate facade bridge.
151    #[must_use]
152    pub const fn without_log(mut self) -> Self {
153        self.install_log = false;
154        self
155    }
156
157    /// Skip installing the `tracing` global subscriber.
158    #[must_use]
159    pub const fn without_tracing(mut self) -> Self {
160        self.install_tracing = false;
161        self
162    }
163
164    /// Register `RlgLogger` as the global `log` facade.
165    ///
166    /// # Errors
167    ///
168    /// Returns `InitError::LoggerAlreadySet` if another logger was already registered.
169    pub(crate) fn install_log_facade(
170        format: LogFormat,
171        level: LogLevel,
172    ) -> Result<(), InitError> {
173        let logger = LOGGER.get_or_init(|| RlgLogger::new(format));
174        log::set_logger(logger)
175            .map_err(|_| InitError::LoggerAlreadySet)?;
176        log::set_max_level(to_log_level_filter(level));
177        Ok(())
178    }
179
180    /// Register `RlgSubscriber` as the global `tracing` dispatcher.
181    ///
182    /// # Errors
183    ///
184    /// Returns `InitError::SubscriberAlreadySet` if another subscriber was already registered.
185    pub(crate) fn install_tracing_subscriber() -> Result<(), InitError>
186    {
187        let subscriber = crate::tracing::RlgSubscriber::new();
188        let dispatch =
189            tracing_core::dispatcher::Dispatch::new(subscriber);
190        tracing_core::dispatcher::set_global_default(dispatch)
191            .map_err(|_| InitError::SubscriberAlreadySet)?;
192        Ok(())
193    }
194
195    /// Finalize and install RLG as the global logger and subscriber.
196    ///
197    /// Applies `RUST_LOG` overrides and auto-detects the format
198    /// (TTY → Logfmt, pipe → JSON) when none was explicitly set.
199    ///
200    /// # Errors
201    ///
202    /// Returns `InitError` if a global logger/subscriber already exists
203    /// or if `init()` was already called.
204    pub fn init(mut self) -> Result<FlushGuard, InitError> {
205        if INIT_GUARD.set(()).is_err() {
206            return Err(InitError::AlreadyInitialized);
207        }
208
209        // Apply RUST_LOG level override.
210        if let Some(env_level) = parse_rust_log() {
211            self.level = env_level;
212        }
213
214        // Set engine filter level
215        ENGINE.set_filter(self.level.to_numeric());
216
217        // Install log facade
218        if self.install_log {
219            Self::install_log_facade(self.format, self.level)?;
220        }
221
222        // Install tracing subscriber
223        if self.install_tracing {
224            Self::install_tracing_subscriber()?;
225        }
226
227        Ok(FlushGuard { _private: () })
228    }
229}
230
231/// Create a new [`RlgBuilder`] for custom initialization.
232#[must_use]
233pub fn builder() -> RlgBuilder {
234    RlgBuilder::default()
235}
236
237/// RAII guard (resource-cleanup-on-drop) that flushes pending events on drop.
238///
239/// Returned by [`init`] and [`RlgBuilder::init`]. **Hold it in `main`** —
240/// dropping it calls [`ENGINE.shutdown()`](crate::engine::LockFreeEngine::shutdown).
241///
242/// ```rust,no_run
243/// let _guard = rlg::init().unwrap();
244/// // … application code …
245/// // ← guard drops here, flushing all pending logs
246/// ```
247#[derive(Debug)]
248pub struct FlushGuard {
249    _private: (),
250}
251
252impl Drop for FlushGuard {
253    fn drop(&mut self) {
254        ENGINE.shutdown();
255    }
256}
257
258/// Initialize RLG with sensible defaults.
259///
260/// Auto-detects format (TTY → Logfmt, pipe → JSON) and respects `RUST_LOG`.
261///
262/// # Errors
263///
264/// Returns `InitError` if a global logger or subscriber already exists.
265pub fn init() -> Result<FlushGuard, InitError> {
266    builder().init()
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_init_error_display_logger_already_set() {
275        let err = InitError::LoggerAlreadySet;
276        assert_eq!(
277            err.to_string(),
278            "a log crate logger was already set"
279        );
280    }
281
282    #[test]
283    fn test_init_error_display_subscriber_already_set() {
284        let err = InitError::SubscriberAlreadySet;
285        assert_eq!(
286            err.to_string(),
287            "a tracing subscriber was already set"
288        );
289    }
290
291    #[test]
292    fn test_init_error_display_already_initialized() {
293        let err = InitError::AlreadyInitialized;
294        assert_eq!(err.to_string(), "rlg was already initialized");
295    }
296
297    #[test]
298    fn test_init_error_debug() {
299        let err = InitError::LoggerAlreadySet;
300        assert_eq!(format!("{err:?}"), "LoggerAlreadySet");
301    }
302
303    #[test]
304    fn test_init_error_clone_copy() {
305        let err = InitError::AlreadyInitialized;
306        let cloned = err;
307        assert_eq!(format!("{err:?}"), format!("{cloned:?}"));
308    }
309
310    #[test]
311    fn test_init_error_is_error() {
312        let err = InitError::LoggerAlreadySet;
313        // Verify it implements std::error::Error
314        let _: &dyn std::error::Error = &err;
315    }
316
317    #[test]
318    fn test_builder_defaults() {
319        let b = RlgBuilder::default();
320        assert_eq!(b.level, LogLevel::INFO);
321        assert!(b.install_log);
322        assert!(b.install_tracing);
323        // Format is auto-detected (Logfmt for TTY, JSON for pipe/CI)
324        assert!(
325            b.format == LogFormat::JSON
326                || b.format == LogFormat::Logfmt
327        );
328    }
329
330    #[test]
331    fn test_builder_level() {
332        let b = builder().level(LogLevel::DEBUG);
333        assert_eq!(b.level, LogLevel::DEBUG);
334    }
335
336    #[test]
337    fn test_builder_format() {
338        let b = builder().format(LogFormat::JSON);
339        assert_eq!(b.format, LogFormat::JSON);
340    }
341
342    #[test]
343    fn test_builder_without_log() {
344        let b = builder().without_log();
345        assert!(!b.install_log);
346        assert!(b.install_tracing);
347    }
348
349    #[test]
350    fn test_builder_without_tracing() {
351        let b = builder().without_tracing();
352        assert!(b.install_log);
353        assert!(!b.install_tracing);
354    }
355
356    #[test]
357    fn test_builder_chaining() {
358        let b = builder()
359            .level(LogLevel::TRACE)
360            .format(LogFormat::ECS)
361            .without_log()
362            .without_tracing();
363        assert_eq!(b.level, LogLevel::TRACE);
364        assert_eq!(b.format, LogFormat::ECS);
365        assert!(!b.install_log);
366        assert!(!b.install_tracing);
367    }
368
369    #[test]
370    fn test_builder_clone_copy() {
371        let b = builder().level(LogLevel::WARN);
372        let b2 = b;
373        // Both usable since RlgBuilder is Copy
374        assert_eq!(b.level, b2.level);
375        assert_eq!(b.format, b2.format);
376    }
377
378    #[test]
379    fn test_builder_without_facades_configuration() {
380        let b = builder().without_log().without_tracing();
381        assert!(!b.install_log);
382        assert!(!b.install_tracing);
383    }
384
385    #[test]
386    fn test_builder_fn() {
387        let b = builder();
388        assert_eq!(b.level, LogLevel::INFO);
389        // Format is auto-detected based on output context
390        assert!(
391            b.format == LogFormat::JSON
392                || b.format == LogFormat::Logfmt
393        );
394        assert!(b.install_log);
395        assert!(b.install_tracing);
396    }
397
398    #[test]
399    fn test_init_error_source() {
400        let err = InitError::LoggerAlreadySet;
401        // std::error::Error::source should return None
402        assert!(std::error::Error::source(&err).is_none());
403    }
404
405    #[test]
406    fn test_builder_default_impl() {
407        let b1 = RlgBuilder::default();
408        let b2 = builder();
409        assert_eq!(b1.level, b2.level);
410        assert_eq!(b1.format, b2.format);
411        assert_eq!(b1.install_log, b2.install_log);
412        assert_eq!(b1.install_tracing, b2.install_tracing);
413    }
414
415    #[test]
416    fn test_init_error_all_display_variants() {
417        // Exercise all three Display paths
418        let msgs: Vec<String> = vec![
419            InitError::LoggerAlreadySet,
420            InitError::SubscriberAlreadySet,
421            InitError::AlreadyInitialized,
422        ]
423        .into_iter()
424        .map(|e| e.to_string())
425        .collect();
426        assert_eq!(msgs.len(), 3);
427        assert!(msgs[0].contains("log"));
428        assert!(msgs[1].contains("tracing"));
429        assert!(msgs[2].contains("already initialized"));
430    }
431
432    #[test]
433    #[cfg_attr(miri, ignore)]
434    fn test_init_guard_static() {
435        // Exercise the OnceLock guard
436        // First attempt may succeed or fail depending on test ordering
437        let _ = INIT_GUARD.set(());
438        // Second attempt should always fail
439        assert!(INIT_GUARD.set(()).is_err());
440    }
441
442    #[test]
443    #[cfg_attr(miri, ignore)]
444    fn test_logger_static() {
445        // Exercise the LOGGER OnceLock
446        let logger =
447            LOGGER.get_or_init(|| RlgLogger::new(LogFormat::JSON));
448        assert!(format!("{logger:?}").contains("RlgLogger"));
449    }
450
451    #[test]
452    #[cfg_attr(miri, ignore)]
453    fn test_install_log_facade() {
454        // First call may succeed or fail (test ordering is non-deterministic)
455        let r1 = RlgBuilder::install_log_facade(
456            LogFormat::JSON,
457            LogLevel::INFO,
458        );
459        assert!(
460            r1.is_ok()
461                || matches!(r1, Err(InitError::LoggerAlreadySet))
462        );
463        // Second call should definitely fail
464        let r2 = RlgBuilder::install_log_facade(
465            LogFormat::MCP,
466            LogLevel::DEBUG,
467        );
468        assert!(matches!(r2, Err(InitError::LoggerAlreadySet)));
469    }
470
471    #[test]
472    #[cfg_attr(miri, ignore)]
473    fn test_install_tracing_subscriber() {
474        // First call may succeed or fail (test ordering is non-deterministic)
475        let r1 = RlgBuilder::install_tracing_subscriber();
476        assert!(
477            r1.is_ok()
478                || matches!(r1, Err(InitError::SubscriberAlreadySet))
479        );
480        // Second call should definitely fail
481        let r2 = RlgBuilder::install_tracing_subscriber();
482        assert!(matches!(r2, Err(InitError::SubscriberAlreadySet)));
483    }
484}