arqen 0.15.0

Rust-first backend toolkit using Tokio, Axum, Tower, tracing, Serde, Reqwest, and an optional Clap CLI
Documentation
use std::io::IsTerminal;
use std::sync::OnceLock;

use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_subscriber::{EnvFilter, fmt};

use crate::config::{LogFormat, LoggingConfig};

static LOG_WRITER: OnceLock<(NonBlocking, WorkerGuard)> = OnceLock::new();

fn writer() -> NonBlocking {
    LOG_WRITER
        .get_or_init(|| tracing_appender::non_blocking(std::io::stderr()))
        .0
        .clone()
}

pub fn init_logging(log_level: &str, log_format: &str) {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level));
    let ansi = std::io::stderr().is_terminal();
    let writer = writer();

    match log_format {
        "json" => {
            fmt()
                .with_writer(writer)
                .with_env_filter(filter)
                .with_target(false)
                .with_ansi(false)
                .json()
                .init();
        }
        "pretty" => {
            fmt()
                .with_writer(writer)
                .with_env_filter(filter)
                .with_target(false)
                .with_ansi(ansi)
                .with_thread_ids(false)
                .with_thread_names(false)
                .with_timer(fmt::time::SystemTime)
                .pretty()
                .init();
        }
        _ => {
            fmt()
                .with_writer(writer)
                .with_env_filter(filter)
                .with_target(false)
                .with_ansi(ansi)
                .with_thread_ids(false)
                .with_thread_names(false)
                .with_timer(fmt::time::SystemTime)
                .compact()
                .init();
        }
    }
}

pub fn init_logging_with_config(config: &LoggingConfig) {
    let filter =
        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.level));
    let ansi = std::io::stderr().is_terminal();
    let writer = writer();

    match config.format {
        LogFormat::Json => {
            fmt()
                .with_writer(writer)
                .with_env_filter(filter)
                .with_target(false)
                .with_ansi(false)
                .json()
                .init();
        }
        LogFormat::Pretty => {
            fmt()
                .with_writer(writer)
                .with_env_filter(filter)
                .with_target(false)
                .with_ansi(ansi)
                .with_thread_ids(false)
                .with_thread_names(false)
                .with_timer(fmt::time::SystemTime)
                .pretty()
                .init();
        }
        LogFormat::Compact => {
            fmt()
                .with_writer(writer)
                .with_env_filter(filter)
                .with_target(false)
                .with_ansi(ansi)
                .with_thread_ids(false)
                .with_thread_names(false)
                .with_timer(fmt::time::SystemTime)
                .compact()
                .init();
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::LogFormat;

    #[test]
    fn test_logging_config_default() {
        let config = LoggingConfig::default();
        assert_eq!(config.level, "info");
        assert!(matches!(config.format, LogFormat::Pretty));
    }

    #[test]
    fn test_logging_config_custom() {
        let config = LoggingConfig {
            level: "debug".to_string(),
            format: LogFormat::Json,
        };
        assert_eq!(config.level, "debug");
        assert!(matches!(config.format, LogFormat::Json));
    }
}