use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
pub fn init_logging(filter: Option<&str>) {
let resolved = current_node_log_filter(filter, false);
install_text_subscriber(&resolved);
}
pub fn node_log_filter(
rust_log: Option<&str>,
blvm_log_level: Option<&str>,
config_filter: Option<&str>,
verbose: bool,
) -> String {
if let Some(value) = rust_log.map(str::trim).filter(|s| !s.is_empty()) {
return value.to_string();
}
if let Some(value) = blvm_log_level.map(str::trim).filter(|s| !s.is_empty()) {
return value.to_string();
}
if let Some(value) = config_filter.map(str::trim).filter(|s| !s.is_empty()) {
return value.to_string();
}
if verbose {
return "blvm=debug,blvm_node=debug".to_string();
}
"blvm=info,blvm_node=info".to_string()
}
fn current_node_log_filter(config_filter: Option<&str>, verbose: bool) -> String {
let rust_log = std::env::var("RUST_LOG").ok();
let blvm_log_level = std::env::var("BLVM_LOG_LEVEL").ok();
node_log_filter(
rust_log.as_deref(),
blvm_log_level.as_deref(),
config_filter,
verbose,
)
}
fn install_text_subscriber(filter: &str) {
let env_filter = EnvFilter::new(filter);
let _ = tracing_subscriber::registry()
.with(
fmt::layer()
.with_target(true)
.with_thread_ids(false)
.with_ansi(std::env::var("NO_COLOR").is_err()),
)
.with(env_filter)
.try_init();
}
pub fn init_node_logging(config: Option<&crate::config::LoggingConfig>, verbose: bool) {
let config_filter = config.and_then(|c| c.filter.as_deref());
let resolved = current_node_log_filter(config_filter, verbose);
let want_json = config.map(|c| c.json_format).unwrap_or(false);
if want_json {
#[cfg(feature = "json-logging")]
{
init_json_logging(Some(&resolved));
return;
}
#[cfg(not(feature = "json-logging"))]
{
install_text_subscriber(&resolved);
tracing::warn!(
"logging.json_format is set but the json-logging feature is off; using text logs"
);
return;
}
}
install_text_subscriber(&resolved);
}
pub fn init_module_logging(module_name: &str, filter: Option<&str>) {
let mut env_filter = EnvFilter::from_default_env();
let default_filter = format!("{}={},blvm_node::module=debug", module_name, "info");
if std::env::var("RUST_LOG").is_err() {
env_filter = filter
.map(EnvFilter::new)
.unwrap_or_else(|| EnvFilter::new(&default_filter));
}
let _ = tracing_subscriber::registry()
.with(
fmt::layer()
.with_target(true) .with_thread_ids(false) .with_ansi(std::env::var("NO_COLOR").is_err()), )
.with(env_filter)
.try_init();
}
#[cfg(feature = "json-logging")]
pub fn init_json_logging(filter: Option<&str>) {
let mut env_filter = EnvFilter::from_default_env();
if filter.is_some() && std::env::var("RUST_LOG").is_err() {
if let Some(f) = filter {
env_filter = EnvFilter::new(f);
}
}
if std::env::var("RUST_LOG").is_err() && filter.is_none() {
env_filter = EnvFilter::new("info");
}
let _ = tracing_subscriber::registry()
.with(fmt::layer().json().with_target(true))
.with(env_filter)
.try_init();
}
pub fn init_logging_from_config(config: Option<&crate::config::LoggingConfig>) {
init_node_logging(config, false);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn logging_filter_uses_config_when_env_unset() {
let filter = node_log_filter(None, None, Some("blvm=debug"), false);
assert_eq!(filter, "blvm=debug");
}
#[test]
fn logging_filter_prefers_rust_log_then_blvm_level() {
assert_eq!(
node_log_filter(Some("trace"), Some("warn"), Some("blvm=debug"), true),
"trace"
);
assert_eq!(
node_log_filter(None, Some("warn"), Some("blvm=debug"), true),
"warn"
);
}
#[test]
fn logging_filter_verbose_when_nothing_else_set() {
assert_eq!(
node_log_filter(None, None, None, true),
"blvm=debug,blvm_node=debug"
);
assert_eq!(
node_log_filter(None, None, None, false),
"blvm=info,blvm_node=info"
);
}
#[test]
fn test_logging_initialization() {
let _init_logging_fn: fn(Option<&str>) = init_logging;
let _init_module_logging_fn: fn(&str, Option<&str>) = init_module_logging;
let _init_logging_from_config_fn: fn(Option<&crate::config::LoggingConfig>) =
init_logging_from_config;
#[cfg(feature = "json-logging")]
{
let _init_json_logging_fn: fn(Option<&str>) = init_json_logging;
}
}
#[cfg(feature = "json-logging")]
#[test]
fn json_format_emits_a_level_field() {
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
struct Buf(Arc<Mutex<Vec<u8>>>);
impl Write for Buf {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
self.0.lock().expect("log buffer").extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let captured = Arc::new(Mutex::new(Vec::<u8>::new()));
let writer = captured.clone();
let subscriber = tracing_subscriber::fmt()
.json()
.with_ansi(false)
.with_target(true)
.with_writer(move || Buf(writer.clone()))
.finish();
tracing::subscriber::with_default(subscriber, || {
tracing::info!("json probe");
});
let bytes = captured.lock().expect("log buffer").clone();
let text = String::from_utf8(bytes).expect("log line is utf-8");
let line = text
.lines()
.find(|line| !line.is_empty())
.expect("one log line");
let value: serde_json::Value = serde_json::from_str(line).expect("json log line");
assert!(
value.get("level").is_some(),
"json log missing level: {line}"
);
}
}