use tracing_subscriber::filter::{LevelFilter, ParseError, Targets};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
const TRANSPORT_TARGETS: [&str; 6] = ["h2", "hyper", "hyper_util", "tonic", "tower", "rustls"];
pub fn configure(debug: bool) {
Builder::new(debug).init();
}
#[derive(Debug)]
pub struct Builder {
debug: bool,
quiet: Vec<String>,
}
impl Builder {
pub fn new(debug: bool) -> Self {
Self {
debug,
quiet: Vec::new(),
}
}
pub fn quiet<I, S>(mut self, targets: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.quiet.extend(targets.into_iter().map(Into::into));
self
}
pub fn init(self) {
let rust_log = std::env::var("RUST_LOG").ok();
let (filter, ignored) = self.filter(rust_log.as_deref());
let builder = tracing_subscriber::fmt()
.with_max_level(LevelFilter::TRACE)
.with_writer(std::io::stderr)
.with_file(true)
.with_line_number(true);
if self.debug {
builder.finish().with(filter).init();
} else {
builder.json().finish().with(filter).init();
}
if let Some(error) = ignored {
tracing::warn!(%error, "ignoring RUST_LOG, which takes only target=level directives");
}
}
fn filter(&self, rust_log: Option<&str>) -> (Targets, Option<ParseError>) {
let ignored = match rust_log.map(str::trim).filter(|s| !s.is_empty()) {
None => None,
Some(set) => match set.parse() {
Ok(filter) => return (filter, None),
Err(error) => Some(error),
},
};
if !self.debug {
return (Targets::new().with_default(LevelFilter::INFO), ignored);
}
let filter = TRANSPORT_TARGETS
.iter()
.copied()
.chain(self.quiet.iter().map(String::as_str))
.fold(
Targets::new().with_default(LevelFilter::DEBUG),
|filter, target| filter.with_target(target, LevelFilter::INFO),
);
(filter, ignored)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing::Level;
fn enabled(builder: &Builder, rust_log: Option<&str>, target: &str, level: Level) -> bool {
builder.filter(rust_log).0.would_enable(target, &level)
}
#[test]
fn info_by_default() {
let builder = Builder::new(false);
for rust_log in [None, Some(" ")] {
assert!(enabled(&builder, rust_log, "my_function", Level::INFO));
assert!(!enabled(&builder, rust_log, "my_function", Level::DEBUG));
}
}
#[test]
fn debug_leaves_the_transport_at_info() {
let builder = Builder::new(true);
assert!(enabled(&builder, None, "my_function", Level::DEBUG));
for target in TRANSPORT_TARGETS {
let module = format!("{target}::proto");
assert!(!enabled(&builder, None, &module, Level::DEBUG), "{target}");
assert!(enabled(&builder, None, &module, Level::INFO), "{target}");
}
}
#[test]
fn quiet_targets_join_the_transport_under_debug() {
let builder = Builder::new(true).quiet(["wasmtime", "cranelift_codegen"]);
assert!(!enabled(&builder, None, "wasmtime::module", Level::DEBUG));
assert!(!enabled(&builder, None, "cranelift_codegen", Level::DEBUG));
assert!(enabled(&builder, None, "my_function", Level::DEBUG));
let builder = Builder::new(false).quiet(["wasmtime"]);
assert!(!enabled(&builder, None, "my_function", Level::DEBUG));
}
#[test]
fn rust_log_replaces_the_flag_and_the_quiet_targets() {
let builder = Builder::new(true).quiet(["wasmtime"]);
let rust_log = Some("info,my_function=trace,wasmtime=debug");
assert!(enabled(&builder, rust_log, "my_function", Level::TRACE));
assert!(enabled(&builder, rust_log, "wasmtime", Level::DEBUG));
assert!(!enabled(&builder, rust_log, "h2", Level::DEBUG));
assert!(builder.filter(rust_log).1.is_none());
}
#[test]
fn an_unsupported_rust_log_falls_back_to_the_flag() {
let builder = Builder::new(false);
let rust_log = Some("my_function[run{tag=x}]=debug");
let (filter, ignored) = builder.filter(rust_log);
assert!(ignored.is_some());
assert!(filter.would_enable("my_function", &Level::INFO));
assert!(!filter.would_enable("my_function", &Level::DEBUG));
}
}