const DEFAULT_LOG_FILTER: &str = "info";
pub(crate) fn resolve_env_filter(
config_log_level: Option<&str>,
) -> (tracing_subscriber::EnvFilter, Option<String>) {
let rust_log = std::env::var(tracing_subscriber::EnvFilter::DEFAULT_ENV).ok();
resolve_env_filter_from(rust_log.as_deref(), config_log_level)
}
fn resolve_env_filter_from(
rust_log: Option<&str>,
config_log_level: Option<&str>,
) -> (tracing_subscriber::EnvFilter, Option<String>) {
let non_blank = |s: &&str| !s.trim().is_empty();
let (source, raw) = match (
rust_log.filter(non_blank),
config_log_level.filter(non_blank),
) {
(Some(raw), _) => (tracing_subscriber::EnvFilter::DEFAULT_ENV, raw),
(None, Some(level)) => ("server.log_level", level),
(None, None) => return (tracing_subscriber::EnvFilter::new(DEFAULT_LOG_FILTER), None),
};
match parse_log_filter(raw) {
Ok(filter) => (filter, None),
Err(reason) => (
tracing_subscriber::EnvFilter::new(DEFAULT_LOG_FILTER),
Some(format!(
"invalid {source} value {raw:?}: {reason} — falling back to `{DEFAULT_LOG_FILTER}`"
)),
),
}
}
fn parse_log_filter(raw: &str) -> std::result::Result<tracing_subscriber::EnvFilter, String> {
let raw = raw.trim();
if !raw.contains('=')
&& !raw.contains(',')
&& raw
.parse::<tracing_subscriber::filter::LevelFilter>()
.is_err()
{
return Err(
"expected a level (`trace`, `debug`, `info`, `warn`, `error`, `off`) or an \
`EnvFilter` directive list such as `info,bitrouter=debug`"
.to_string(),
);
}
tracing_subscriber::EnvFilter::try_new(raw).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_log_level_is_used_when_rust_log_is_unset() {
let (filter, warning) = resolve_env_filter_from(None, Some("debug"));
assert_eq!(filter.to_string(), "debug");
assert!(warning.is_none());
}
#[test]
fn config_log_level_accepts_per_target_filter_syntax() {
let (filter, warning) = resolve_env_filter_from(None, Some("warn,bitrouter=trace"));
let rendered = filter.to_string();
assert!(rendered.contains("warn"), "{rendered}");
assert!(rendered.contains("bitrouter=trace"), "{rendered}");
assert!(warning.is_none());
}
#[test]
fn rust_log_wins_over_config_log_level() {
let (filter, warning) = resolve_env_filter_from(Some("trace"), Some("error"));
assert_eq!(filter.to_string(), "trace");
assert!(warning.is_none());
}
#[test]
fn defaults_to_info_when_neither_source_is_set() {
let (filter, warning) = resolve_env_filter_from(None, None);
assert_eq!(filter.to_string(), DEFAULT_LOG_FILTER);
assert!(warning.is_none());
}
#[test]
fn blank_sources_fall_through_rather_than_erroring() {
let (filter, warning) = resolve_env_filter_from(Some(" "), Some("debug"));
assert_eq!(filter.to_string(), "debug");
assert!(warning.is_none());
let (filter, warning) = resolve_env_filter_from(None, Some(""));
assert_eq!(filter.to_string(), DEFAULT_LOG_FILTER);
assert!(warning.is_none());
}
#[test]
fn invalid_config_log_level_warns_and_falls_back() -> anyhow::Result<()> {
let (filter, warning) = resolve_env_filter_from(None, Some("not-a-level"));
assert_eq!(filter.to_string(), DEFAULT_LOG_FILTER);
let warning =
warning.ok_or_else(|| anyhow::anyhow!("an unparseable log_level must be reported"))?;
assert!(warning.contains("server.log_level"), "{warning}");
assert!(warning.contains("not-a-level"), "{warning}");
Ok(())
}
#[test]
fn typo_level_does_not_silently_mute_the_daemon() -> anyhow::Result<()> {
assert_eq!(
tracing_subscriber::EnvFilter::try_new("dbug")?.to_string(),
"dbug=trace",
);
let (filter, warning) = resolve_env_filter_from(None, Some("dbug"));
assert_eq!(filter.to_string(), DEFAULT_LOG_FILTER);
assert!(warning.is_some(), "a typo'd level must warn, not mute");
Ok(())
}
#[test]
fn every_level_name_is_accepted() {
for level in ["trace", "debug", "info", "warn", "error", "off"] {
let (filter, warning) = resolve_env_filter_from(None, Some(level));
assert!(warning.is_none(), "{level} should be valid: {warning:?}");
assert_eq!(filter.to_string(), level);
}
}
#[test]
fn invalid_rust_log_warns_and_does_not_fall_back_to_config() -> anyhow::Result<()> {
let (filter, warning) = resolve_env_filter_from(Some("not-a-level"), Some("debug"));
assert_eq!(filter.to_string(), DEFAULT_LOG_FILTER);
let warning =
warning.ok_or_else(|| anyhow::anyhow!("an unparseable RUST_LOG must be reported"))?;
assert!(warning.contains("RUST_LOG"), "{warning}");
Ok(())
}
#[test]
fn server_config_default_log_level_is_a_valid_filter() {
let default_level = bitrouter_sdk::config::ServerConfig::default().log_level;
let (filter, warning) = resolve_env_filter_from(None, Some(&default_level));
assert_eq!(filter.to_string(), DEFAULT_LOG_FILTER);
assert!(warning.is_none());
}
}