use std::io;
use std::sync::OnceLock;
use tracing_subscriber::{
EnvFilter, Registry, filter::ParseError, layer::SubscriberExt, reload, util::SubscriberInitExt,
};
static RELOAD_HANDLE: OnceLock<reload::Handle<EnvFilter, Registry>> = OnceLock::new();
const RUDOF_CRATES: &[&str] = &[
"rudof_cli",
"rudof_lib",
"rudof_rdf",
"rudof_config",
"rudof_iri",
"rudof_generate",
"rudof_mcp",
"shex_ast",
"shex_validation",
"shacl",
"shapes_comparator",
"shapes_converter",
"sparql_service",
"dctap",
"prefixmap",
"rbe",
"rdf_config",
"mie",
"pgschema",
];
pub const LEVEL_NAMES: &[&str] = &["error", "warn", "info", "debug", "trace"];
fn is_bare_level(directives: &str) -> bool {
LEVEL_NAMES.contains(&directives.trim().to_ascii_lowercase().as_str())
}
fn scoped_directives(level: &str) -> String {
let level = level.trim();
let mut directives = String::from("warn");
for crate_name in RUDOF_CRATES {
directives.push(',');
directives.push_str(crate_name);
directives.push('=');
directives.push_str(level);
}
directives
}
fn build_filter(directives: &str) -> Result<EnvFilter, ParseError> {
if is_bare_level(directives) {
EnvFilter::try_new(scoped_directives(directives))
} else {
EnvFilter::try_new(directives)
}
}
pub fn init() {
let requested = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
let env_filter =
build_filter(&requested).unwrap_or_else(|_| EnvFilter::try_new("info").expect("\"info\" is always valid"));
let (reload_layer, handle) = reload::Layer::new(env_filter.clone());
let fmt_layer = tracing_subscriber::fmt::layer()
.with_file(true)
.with_target(false)
.with_line_number(true)
.with_writer(io::stderr)
.without_time();
tracing_subscriber::registry().with(reload_layer).with(fmt_layer).init();
let _ = RELOAD_HANDLE.set(handle);
tracing::trace!("rudof running with tracing filter {}", env_filter);
}
pub fn set_level(directives: &str) -> Result<(), ParseError> {
let filter = build_filter(directives)?;
if let Some(handle) = RELOAD_HANDLE.get() {
let _ = handle.reload(filter);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_level_is_scoped_to_rudof_crates_with_a_warn_baseline() {
let directives = scoped_directives("debug");
assert!(directives.starts_with("warn,"));
assert!(directives.contains("rudof_cli=debug"));
assert!(directives.contains("shex_validation=debug"));
}
#[test]
fn bare_level_detection_is_case_insensitive_and_trims_whitespace() {
assert!(is_bare_level("debug"));
assert!(is_bare_level("DEBUG"));
assert!(is_bare_level(" Trace "));
assert!(!is_bare_level("rudof_rdf=debug,info"));
assert!(!is_bare_level("debug,reqwest=trace"));
}
#[test]
fn scoped_and_multi_directive_filters_pass_through_unscoped() {
assert!(build_filter("rudof_rdf=debug,info").is_ok());
assert!(build_filter("debug,reqwest=trace").is_ok());
}
#[test]
fn every_bare_level_name_builds_a_valid_filter() {
for level in LEVEL_NAMES {
assert!(build_filter(level).is_ok(), "expected {level} to build a valid filter");
}
}
}