use std::fmt;
use tracing_subscriber::{
EnvFilter, Layer, Registry, fmt as tracing_fmt, fmt::MakeWriter, layer::SubscriberExt,
util::SubscriberInitExt,
};
use crate::config::Logger;
const DEFAULT_MODULES: &[&str] = &["axa", "sea_orm_migration", "tower_http", "sqlx::query"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Off,
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
#[must_use]
pub fn parse(name: &str) -> Self {
Self::from_name(name).unwrap_or(Self::Info)
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"off" => Some(Self::Off),
"trace" => Some(Self::Trace),
"debug" => Some(Self::Debug),
"info" => Some(Self::Info),
"warn" => Some(Self::Warn),
"error" => Some(Self::Error),
_ => None,
}
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Off => "off",
Self::Trace => "trace",
Self::Debug => "debug",
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Format {
#[default]
Compact,
Pretty,
Json,
}
impl Format {
#[must_use]
pub fn parse(name: &str) -> Self {
Self::from_name(name).unwrap_or(Self::Compact)
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"compact" => Some(Self::Compact),
"json" => Some(Self::Json),
"pretty" => Some(Self::Pretty),
_ => None,
}
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Compact => "compact",
Self::Pretty => "pretty",
Self::Json => "json",
})
}
}
#[must_use]
pub fn init_env_filter(
app_name: &str,
override_filter: Option<&String>,
level: &LogLevel,
) -> EnvFilter {
let rust_log = std::env::var(EnvFilter::DEFAULT_ENV).ok();
choose_filter(rust_log.as_deref(), app_name, override_filter, *level)
}
fn choose_filter(
rust_log: Option<&str>,
app_name: &str,
override_filter: Option<&String>,
level: LogLevel,
) -> EnvFilter {
let from_rust_log = rust_log
.filter(|filter| !filter.trim().is_empty())
.and_then(|filter| EnvFilter::try_new(filter).ok());
if let Some(filter) = from_rust_log {
return filter;
}
override_filter
.map_or_else(
|| {
let directives = DEFAULT_MODULES
.iter()
.copied()
.chain(std::iter::once(app_name))
.map(|module| format!("{module}={level}"))
.collect::<Vec<_>>()
.join(",");
EnvFilter::try_new(directives)
},
EnvFilter::try_new,
)
.expect("logger filter is invalid")
}
pub fn init_layer<W>(
writer: W,
format: &Format,
ansi: bool,
) -> Box<dyn Layer<Registry> + Send + Sync>
where
W: for<'writer> MakeWriter<'writer> + Send + Sync + 'static,
{
let layer = tracing_fmt::Layer::default()
.with_ansi(ansi)
.with_writer(writer);
match format {
Format::Compact => layer.compact().boxed(),
Format::Pretty => layer.pretty().boxed(),
Format::Json => layer.json().boxed(),
}
}
pub fn init(app_name: &str, config: &Logger) {
for warning in &config.warnings {
eprintln!("warning: {warning}");
}
if !config.enable {
return;
}
let filter = init_env_filter(app_name, config.override_filter.as_ref(), &config.level);
let _ = tracing_subscriber::registry()
.with(init_layer(std::io::stdout, &config.format, true))
.with(filter)
.try_init();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn levels_parse_and_print() {
for name in ["off", "trace", "debug", "info", "warn", "error"] {
assert_eq!(LogLevel::parse(name).to_string(), name);
}
assert_eq!(LogLevel::parse("nonsense"), LogLevel::Info);
assert_eq!(LogLevel::from_name("nonsense"), None);
}
#[test]
fn formats_parse() {
assert_eq!(Format::parse("JSON"), Format::Json);
assert_eq!(Format::parse("pretty"), Format::Pretty);
assert_eq!(Format::parse("nonsense"), Format::Compact);
assert_eq!(Format::from_name("nonsense"), None);
for name in ["compact", "pretty", "json"] {
assert_eq!(Format::parse(name).to_string(), name);
}
}
#[test]
fn rust_log_wins_over_the_override_and_the_level() {
let filter = choose_filter(
Some("from_rust_log=trace"),
"my_service",
Some(&"from_override=warn".to_string()),
LogLevel::Debug,
)
.to_string();
assert_eq!(filter, "from_rust_log=trace");
}
#[test]
fn a_blank_or_invalid_rust_log_is_passed_over() {
for value in ["", " ", "a=b=c=d"] {
let filter =
choose_filter(Some(value), "my_service", None, LogLevel::Debug).to_string();
assert!(filter.contains("my_service=debug"), "{value:?}: {filter}");
}
}
#[test]
fn default_filter_names_the_service_and_shared_modules() {
let filter = init_env_filter("my_service", None, &LogLevel::Debug).to_string();
assert!(filter.contains("my_service=debug"), "{filter}");
assert!(filter.contains("tower_http=debug"), "{filter}");
}
#[test]
fn an_override_replaces_the_default_filter() {
let custom = "my_service=trace".to_string();
let filter = init_env_filter("my_service", Some(&custom), &LogLevel::Info).to_string();
assert_eq!(filter, custom);
}
}