use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::Level;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{
Layer, Registry,
filter::{EnvFilter, Targets},
fmt,
layer::SubscriberExt,
reload,
util::SubscriberInitExt,
};
use crate::state::LogLevelController;
#[derive(Debug, Clone)]
pub struct LoggingConfig {
pub default_env_filter: String,
pub pretty_stdout: bool,
pub enable_hft_layer: bool,
pub hft_log_dir: PathBuf,
pub hft_log_prefix: String,
pub hft_target: String,
pub hft_level: Level,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
default_env_filter: "info,janus=debug".to_string(),
pretty_stdout: true,
enable_hft_layer: true,
hft_log_dir: PathBuf::from("./logs/hft"),
hft_log_prefix: "hft.log".to_string(),
hft_target: "janus::hft".to_string(),
hft_level: Level::TRACE,
}
}
}
impl LoggingConfig {
pub fn for_tests() -> Self {
Self {
default_env_filter: "warn".to_string(),
pretty_stdout: false,
enable_hft_layer: false,
..Default::default()
}
}
pub fn with_env_filter(mut self, filter: impl Into<String>) -> Self {
self.default_env_filter = filter.into();
self
}
pub fn with_hft_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.hft_log_dir = dir.into();
self
}
pub fn without_hft(mut self) -> Self {
self.enable_hft_layer = false;
self
}
pub fn with_json_stdout(mut self) -> Self {
self.pretty_stdout = false;
self
}
}
pub struct LoggingGuard {
_hft_guard: Option<WorkerGuard>,
ops_reload_handle: Option<reload::Handle<EnvFilter, Registry>>,
}
impl LoggingGuard {
pub fn ops_reload_handle(&self) -> Option<&reload::Handle<EnvFilter, Registry>> {
self.ops_reload_handle.as_ref()
}
pub fn set_log_level(&self, filter_str: &str) -> anyhow::Result<()> {
let handle = self
.ops_reload_handle
.as_ref()
.ok_or_else(|| anyhow::anyhow!("no reload handle available"))?;
let new_filter = EnvFilter::try_new(filter_str)
.map_err(|e| anyhow::anyhow!("invalid filter '{}': {}", filter_str, e))?;
handle
.reload(new_filter)
.map_err(|e| anyhow::anyhow!("reload failed: {}", e))?;
tracing::info!(filter = filter_str, "log level reloaded at runtime");
Ok(())
}
pub fn create_controller(&self) -> Option<Box<dyn LogLevelController>> {
self.ops_reload_handle
.as_ref()
.map(|handle| -> Box<dyn LogLevelController> {
Box::new(ReloadHandleController {
handle: handle.clone(),
current_filter: Arc::new(std::sync::RwLock::new(None)),
})
})
}
}
struct ReloadHandleController {
handle: reload::Handle<EnvFilter, Registry>,
current_filter: Arc<std::sync::RwLock<Option<String>>>,
}
impl LogLevelController for ReloadHandleController {
fn set_log_level(&self, filter_str: &str) -> Result<(), String> {
let new_filter = EnvFilter::try_new(filter_str)
.map_err(|e| format!("invalid filter '{}': {}", filter_str, e))?;
self.handle
.reload(new_filter)
.map_err(|e| format!("reload failed: {}", e))?;
if let Ok(mut current) = self.current_filter.write() {
*current = Some(filter_str.to_string());
}
tracing::info!(filter = filter_str, "log level changed via API");
Ok(())
}
fn current_filter(&self) -> Option<String> {
self.current_filter
.read()
.ok()
.and_then(|guard| guard.clone())
}
}
impl std::fmt::Debug for LoggingGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LoggingGuard")
.field("has_hft_guard", &self._hft_guard.is_some())
.field("has_reload_handle", &self.ops_reload_handle.is_some())
.finish()
}
}
pub fn init_logging(config: LoggingConfig) -> anyhow::Result<LoggingGuard> {
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&config.default_env_filter));
let (env_filter_layer, reload_handle) = reload::Layer::new(env_filter);
let stdout_layer = if config.pretty_stdout {
fmt::layer()
.pretty()
.with_thread_ids(true)
.with_thread_names(true)
.with_target(true)
.with_filter(env_filter_layer)
.boxed()
} else {
fmt::layer()
.json()
.with_thread_ids(true)
.with_target(true)
.with_current_span(true)
.with_filter(env_filter_layer)
.boxed()
};
let (hft_guard, hft_layer) = if config.enable_hft_layer {
ensure_dir_exists(&config.hft_log_dir)?;
let file_appender =
tracing_appender::rolling::daily(&config.hft_log_dir, &config.hft_log_prefix);
let (non_blocking_writer, guard) = tracing_appender::non_blocking(file_appender);
let hft_filter = Targets::new().with_target(&config.hft_target, config.hft_level);
let layer = fmt::layer()
.with_writer(non_blocking_writer)
.with_ansi(false) .with_target(true)
.with_thread_ids(false)
.json() .with_filter(hft_filter)
.boxed();
(Some(guard), Some(layer))
} else {
(None, None)
};
let registry = Registry::default().with(stdout_layer).with(hft_layer);
registry
.try_init()
.map_err(|e| anyhow::anyhow!("failed to initialize tracing subscriber: {}", e))?;
tracing::info!(
pretty = config.pretty_stdout,
hft_enabled = config.enable_hft_layer,
hft_dir = %config.hft_log_dir.display(),
"logging initialized"
);
Ok(LoggingGuard {
_hft_guard: hft_guard,
ops_reload_handle: Some(reload_handle),
})
}
fn ensure_dir_exists(path: &Path) -> anyhow::Result<()> {
if !path.exists() {
std::fs::create_dir_all(path)
.map_err(|e| anyhow::anyhow!("failed to create log directory {:?}: {}", path, e))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let cfg = LoggingConfig::default();
assert_eq!(cfg.default_env_filter, "info,janus=debug");
assert!(cfg.pretty_stdout);
assert!(cfg.enable_hft_layer);
assert_eq!(cfg.hft_log_dir, PathBuf::from("./logs/hft"));
assert_eq!(cfg.hft_log_prefix, "hft.log");
assert_eq!(cfg.hft_target, "janus::hft");
assert_eq!(cfg.hft_level, Level::TRACE);
}
#[test]
fn test_config_for_tests() {
let cfg = LoggingConfig::for_tests();
assert!(!cfg.enable_hft_layer);
assert!(!cfg.pretty_stdout);
assert_eq!(cfg.default_env_filter, "warn");
}
#[test]
fn test_config_builder() {
let cfg = LoggingConfig::default()
.with_env_filter("debug,hyper=warn")
.with_hft_dir("/tmp/hft-logs")
.with_json_stdout();
assert_eq!(cfg.default_env_filter, "debug,hyper=warn");
assert_eq!(cfg.hft_log_dir, PathBuf::from("/tmp/hft-logs"));
assert!(!cfg.pretty_stdout);
assert!(cfg.enable_hft_layer);
}
#[test]
fn test_config_without_hft() {
let cfg = LoggingConfig::default().without_hft();
assert!(!cfg.enable_hft_layer);
}
}