1use 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
27fn detect_default_format() -> LogFormat {
33 detect_default_format_for(
34 std::env::var("RLG_ENV").ok().as_deref(),
35 atty_stdout(),
36 )
37}
38
39const 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
59fn atty_stdout() -> bool {
61 use std::io::IsTerminal;
62 std::io::stdout().is_terminal()
63}
64
65fn 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
93static INIT_GUARD: OnceLock<()> = OnceLock::new();
95
96static LOGGER: OnceLock<RlgLogger> = OnceLock::new();
98
99#[derive(Debug, Clone, Copy)]
101pub enum InitError {
102 LoggerAlreadySet,
104 SubscriberAlreadySet,
106 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#[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 #[must_use]
151 pub const fn level(mut self, level: LogLevel) -> Self {
152 self.level = level;
153 self
154 }
155
156 #[must_use]
158 pub const fn format(mut self, format: LogFormat) -> Self {
159 self.format = format;
160 self
161 }
162
163 #[must_use]
165 pub const fn without_log(mut self) -> Self {
166 self.install_log = false;
167 self
168 }
169
170 #[must_use]
172 pub const fn without_tracing(mut self) -> Self {
173 self.install_tracing = false;
174 self
175 }
176
177 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 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 pub fn init(mut self) -> Result<FlushGuard, InitError> {
218 if INIT_GUARD.set(()).is_err() {
219 return Err(InitError::AlreadyInitialized);
220 }
221
222 if let Some(env_level) = parse_rust_log() {
224 self.level = env_level;
225 }
226
227 ENGINE.set_filter(self.level.to_numeric());
229
230 if self.install_log {
232 Self::install_log_facade(self.format, self.level)?;
233 }
234
235 if self.install_tracing {
237 Self::install_tracing_subscriber()?;
238 }
239
240 Ok(FlushGuard { _private: () })
241 }
242}
243
244#[must_use]
246pub fn builder() -> RlgBuilder {
247 RlgBuilder::default()
248}
249
250#[derive(Debug)]
261pub struct FlushGuard {
262 _private: (),
263}
264
265impl Drop for FlushGuard {
266 fn drop(&mut self) {
267 ENGINE.shutdown();
268 }
269}
270
271pub 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 #[allow(unsafe_code)]
290 fn set_env(key: &str, val: &str) {
291 unsafe { std::env::set_var(key, val) };
294 }
295
296 #[allow(unsafe_code)]
297 fn unset_env(key: &str) {
298 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 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 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 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 assert_eq!(
382 detect_default_format_for(None, false),
383 LogFormat::JSON
384 );
385 }
386
387 #[test]
388 fn atty_stdout_runs() {
389 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 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 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 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 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 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 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 let _ = INIT_GUARD.set(());
558 assert!(INIT_GUARD.set(()).is_err());
560 }
561
562 #[test]
563 #[cfg_attr(miri, ignore)]
564 fn test_logger_static() {
565 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 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 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 let r1 = RlgBuilder::install_tracing_subscriber();
596 assert!(
597 r1.is_ok()
598 || matches!(r1, Err(InitError::SubscriberAlreadySet))
599 );
600 let r2 = RlgBuilder::install_tracing_subscriber();
602 assert!(matches!(r2, Err(InitError::SubscriberAlreadySet)));
603 }
604}