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;