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;