use std::io::IsTerminal;
use tracing_subscriber::EnvFilter;
use crate::cli::args::ColorChoice;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogFormat {
#[default]
Human,
Json,
}
#[must_use]
pub const fn verbosity_to_directive(verbosity: u8) -> &'static str {
match verbosity {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
}
}
pub fn init_logging(format: LogFormat, verbosity: u8, color: ColorChoice) {
let default_directive = verbosity_to_directive(verbosity);
let filter = EnvFilter::try_from_env("THOUGHTJACK_LOG_LEVEL")
.unwrap_or_else(|_| EnvFilter::new(default_directive));
let show_target = verbosity >= 2;
let use_ansi = match color {
ColorChoice::Auto => {
std::io::stderr().is_terminal() && std::env::var_os("NO_COLOR").is_none()
}
ColorChoice::Always => true,
ColorChoice::Never => false,
};
match format {
LogFormat::Human => {
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_ansi(use_ansi)
.with_target(show_target)
.with_writer(std::io::stderr)
.try_init();
}
LogFormat::Json => {
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.json()
.with_target(show_target)
.with_writer(std::io::stderr)
.try_init();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn log_format_default_is_human() {
assert_eq!(LogFormat::default(), LogFormat::Human);
}
#[test]
fn log_format_clone_copy_eq() {
let a = LogFormat::Json;
let b = a; let c = a; assert_eq!(a, b);
assert_eq!(b, c);
assert_ne!(a, LogFormat::Human);
}
#[test]
fn init_logging_does_not_panic() {
init_logging(LogFormat::Human, 0, ColorChoice::Auto);
init_logging(LogFormat::Json, 3, ColorChoice::Never);
}
#[test]
fn verbosity_0_is_warn() {
assert_eq!(verbosity_to_directive(0), "warn");
}
#[test]
fn verbosity_1_is_info() {
assert_eq!(verbosity_to_directive(1), "info");
}
#[test]
fn verbosity_2_is_debug() {
assert_eq!(verbosity_to_directive(2), "debug");
}
#[test]
fn verbosity_3_is_trace() {
assert_eq!(verbosity_to_directive(3), "trace");
}
#[test]
fn verbosity_255_is_trace() {
assert_eq!(verbosity_to_directive(255), "trace");
}
#[test]
fn test_log_format_debug() {
let human = LogFormat::Human;
let debug_str = format!("{human:?}");
assert_eq!(debug_str, "Human");
}
#[test]
fn test_log_format_json_ne_human() {
assert_ne!(LogFormat::Json, LogFormat::Human);
}
#[test]
fn test_verbosity_saturates_at_255() {
assert_eq!(verbosity_to_directive(255), "trace");
for v in 3..=255 {
assert_eq!(
verbosity_to_directive(v),
"trace",
"verbosity {v} should saturate to trace"
);
}
}
#[test]
fn test_multiple_verbosity_flags() {
assert_eq!(verbosity_to_directive(3), "trace");
}
#[test]
fn test_quiet_and_verbose_conflict() {
assert_eq!(verbosity_to_directive(0), "warn");
}
#[test]
fn test_unicode_in_log_messages() {
tracing::info!("CJK: \u{4F60}\u{597D}\u{4E16}\u{754C} Emoji: \u{1F512}");
tracing::warn!("RTL: \u{0645}\u{0631}\u{062D}\u{0628}\u{0627}");
tracing::debug!("Mixed: caf\u{00E9} na\u{00EF}ve \u{00FC}ber");
}
#[test]
fn test_invalid_log_level_fallback() {
assert_eq!(verbosity_to_directive(255), "trace");
assert_eq!(verbosity_to_directive(128), "trace");
assert_eq!(verbosity_to_directive(42), "trace");
}
}