use crate::engine::ENGINE;
use crate::log_format::LogFormat;
use crate::log_level::LogLevel;
use crate::logger::{RlgLogger, to_log_level_filter};
use std::fmt;
use std::sync::OnceLock;
fn detect_default_format() -> LogFormat {
if std::env::var("RLG_ENV")
.map(|v| v == "production")
.unwrap_or(false)
{
return LogFormat::JSON;
}
if atty_stdout() {
LogFormat::Logfmt
} else {
LogFormat::JSON
}
}
fn atty_stdout() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
fn parse_rust_log() -> Option<LogLevel> {
let val = std::env::var("RUST_LOG").ok()?;
let mut most_permissive: Option<LogLevel> = None;
for directive in val.split(',') {
let level_str = directive
.split('=')
.next_back()
.unwrap_or(directive)
.trim();
if let Ok(level) = level_str.parse::<LogLevel>() {
match most_permissive {
None => most_permissive = Some(level),
Some(current)
if level.to_numeric() < current.to_numeric() =>
{
most_permissive = Some(level);
}
_ => {}
}
}
}
most_permissive
}
static INIT_GUARD: OnceLock<()> = OnceLock::new();
static LOGGER: OnceLock<RlgLogger> = OnceLock::new();
#[derive(Debug, Clone, Copy)]
pub enum InitError {
LoggerAlreadySet,
SubscriberAlreadySet,
AlreadyInitialized,
}
impl fmt::Display for InitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LoggerAlreadySet => {
f.write_str("a log crate logger was already set")
}
Self::SubscriberAlreadySet => {
f.write_str("a tracing subscriber was already set")
}
Self::AlreadyInitialized => {
f.write_str("rlg was already initialized")
}
}
}
}
impl std::error::Error for InitError {}
#[derive(Debug, Clone, Copy)]
pub struct RlgBuilder {
level: LogLevel,
format: LogFormat,
install_log: bool,
install_tracing: bool,
}
impl Default for RlgBuilder {
fn default() -> Self {
Self {
level: LogLevel::INFO,
format: detect_default_format(),
install_log: true,
install_tracing: true,
}
}
}
impl RlgBuilder {
#[must_use]
pub const fn level(mut self, level: LogLevel) -> Self {
self.level = level;
self
}
#[must_use]
pub const fn format(mut self, format: LogFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub const fn without_log(mut self) -> Self {
self.install_log = false;
self
}
#[must_use]
pub const fn without_tracing(mut self) -> Self {
self.install_tracing = false;
self
}
pub(crate) fn install_log_facade(
format: LogFormat,
level: LogLevel,
) -> Result<(), InitError> {
let logger = LOGGER.get_or_init(|| RlgLogger::new(format));
log::set_logger(logger)
.map_err(|_| InitError::LoggerAlreadySet)?;
log::set_max_level(to_log_level_filter(level));
Ok(())
}
pub(crate) fn install_tracing_subscriber() -> Result<(), InitError>
{
let subscriber = crate::tracing::RlgSubscriber::new();
let dispatch =
tracing_core::dispatcher::Dispatch::new(subscriber);
tracing_core::dispatcher::set_global_default(dispatch)
.map_err(|_| InitError::SubscriberAlreadySet)?;
Ok(())
}
pub fn init(mut self) -> Result<FlushGuard, InitError> {
if INIT_GUARD.set(()).is_err() {
return Err(InitError::AlreadyInitialized);
}
if let Some(env_level) = parse_rust_log() {
self.level = env_level;
}
ENGINE.set_filter(self.level.to_numeric());
if self.install_log {
Self::install_log_facade(self.format, self.level)?;
}
if self.install_tracing {
Self::install_tracing_subscriber()?;
}
Ok(FlushGuard { _private: () })
}
}
#[must_use]
pub fn builder() -> RlgBuilder {
RlgBuilder::default()
}
#[derive(Debug)]
pub struct FlushGuard {
_private: (),
}
impl Drop for FlushGuard {
fn drop(&mut self) {
ENGINE.shutdown();
}
}
pub fn init() -> Result<FlushGuard, InitError> {
builder().init()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init_error_display_logger_already_set() {
let err = InitError::LoggerAlreadySet;
assert_eq!(
err.to_string(),
"a log crate logger was already set"
);
}
#[test]
fn test_init_error_display_subscriber_already_set() {
let err = InitError::SubscriberAlreadySet;
assert_eq!(
err.to_string(),
"a tracing subscriber was already set"
);
}
#[test]
fn test_init_error_display_already_initialized() {
let err = InitError::AlreadyInitialized;
assert_eq!(err.to_string(), "rlg was already initialized");
}
#[test]
fn test_init_error_debug() {
let err = InitError::LoggerAlreadySet;
assert_eq!(format!("{err:?}"), "LoggerAlreadySet");
}
#[test]
fn test_init_error_clone_copy() {
let err = InitError::AlreadyInitialized;
let cloned = err;
assert_eq!(format!("{err:?}"), format!("{cloned:?}"));
}
#[test]
fn test_init_error_is_error() {
let err = InitError::LoggerAlreadySet;
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_builder_defaults() {
let b = RlgBuilder::default();
assert_eq!(b.level, LogLevel::INFO);
assert!(b.install_log);
assert!(b.install_tracing);
assert!(
b.format == LogFormat::JSON
|| b.format == LogFormat::Logfmt
);
}
#[test]
fn test_builder_level() {
let b = builder().level(LogLevel::DEBUG);
assert_eq!(b.level, LogLevel::DEBUG);
}
#[test]
fn test_builder_format() {
let b = builder().format(LogFormat::JSON);
assert_eq!(b.format, LogFormat::JSON);
}
#[test]
fn test_builder_without_log() {
let b = builder().without_log();
assert!(!b.install_log);
assert!(b.install_tracing);
}
#[test]
fn test_builder_without_tracing() {
let b = builder().without_tracing();
assert!(b.install_log);
assert!(!b.install_tracing);
}
#[test]
fn test_builder_chaining() {
let b = builder()
.level(LogLevel::TRACE)
.format(LogFormat::ECS)
.without_log()
.without_tracing();
assert_eq!(b.level, LogLevel::TRACE);
assert_eq!(b.format, LogFormat::ECS);
assert!(!b.install_log);
assert!(!b.install_tracing);
}
#[test]
fn test_builder_clone_copy() {
let b = builder().level(LogLevel::WARN);
let b2 = b;
assert_eq!(b.level, b2.level);
assert_eq!(b.format, b2.format);
}
#[test]
fn test_builder_without_facades_configuration() {
let b = builder().without_log().without_tracing();
assert!(!b.install_log);
assert!(!b.install_tracing);
}
#[test]
fn test_builder_fn() {
let b = builder();
assert_eq!(b.level, LogLevel::INFO);
assert!(
b.format == LogFormat::JSON
|| b.format == LogFormat::Logfmt
);
assert!(b.install_log);
assert!(b.install_tracing);
}
#[test]
fn test_init_error_source() {
let err = InitError::LoggerAlreadySet;
assert!(std::error::Error::source(&err).is_none());
}
#[test]
fn test_builder_default_impl() {
let b1 = RlgBuilder::default();
let b2 = builder();
assert_eq!(b1.level, b2.level);
assert_eq!(b1.format, b2.format);
assert_eq!(b1.install_log, b2.install_log);
assert_eq!(b1.install_tracing, b2.install_tracing);
}
#[test]
fn test_init_error_all_display_variants() {
let msgs: Vec<String> = vec![
InitError::LoggerAlreadySet,
InitError::SubscriberAlreadySet,
InitError::AlreadyInitialized,
]
.into_iter()
.map(|e| e.to_string())
.collect();
assert_eq!(msgs.len(), 3);
assert!(msgs[0].contains("log"));
assert!(msgs[1].contains("tracing"));
assert!(msgs[2].contains("already initialized"));
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_init_guard_static() {
let _ = INIT_GUARD.set(());
assert!(INIT_GUARD.set(()).is_err());
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_logger_static() {
let logger =
LOGGER.get_or_init(|| RlgLogger::new(LogFormat::JSON));
assert!(format!("{logger:?}").contains("RlgLogger"));
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_install_log_facade() {
let r1 = RlgBuilder::install_log_facade(
LogFormat::JSON,
LogLevel::INFO,
);
assert!(
r1.is_ok()
|| matches!(r1, Err(InitError::LoggerAlreadySet))
);
let r2 = RlgBuilder::install_log_facade(
LogFormat::MCP,
LogLevel::DEBUG,
);
assert!(matches!(r2, Err(InitError::LoggerAlreadySet)));
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_install_tracing_subscriber() {
let r1 = RlgBuilder::install_tracing_subscriber();
assert!(
r1.is_ok()
|| matches!(r1, Err(InitError::SubscriberAlreadySet))
);
let r2 = RlgBuilder::install_tracing_subscriber();
assert!(matches!(r2, Err(InitError::SubscriberAlreadySet)));
}
}