use std::{
fs::File,
path::{Path, PathBuf},
};
use anyhow::Result;
use tracing::{Level, level_filters::LevelFilter};
use tracing_subscriber::{EnvFilter, Layer, Registry, fmt::time::UtcTime};
use tracing_subscriber_init::{Iso8601, TracingConfig, compact, try_init};
use crate::{
config::{ConfigSalusAgent, PathDefaults},
error::Error,
utils::{ensure_parent_dir, to_path_buf},
};
pub(crate) fn initialize<T, U>(
tracing_config: &T,
config: &ConfigSalusAgent,
defaults: &U,
layers_opt: Option<Vec<Box<dyn Layer<Registry> + Send + Sync>>>,
) -> Result<()>
where
T: TracingConfig,
U: PathDefaults,
{
let mut layers = layers_opt.unwrap_or_default();
if config.enable_std_output() {
let (layer, level_filter) = compact(tracing_config);
let directives = directives(config, level_filter);
let filter = EnvFilter::builder()
.with_default_directive(level_filter.into())
.parse_lossy(directives);
let stdout_layer = layer
.with_ansi(true)
.with_ansi_sanitization(false)
.with_timer(UtcTime::new(Iso8601::DEFAULT))
.with_filter(filter);
layers.push(stdout_layer.boxed());
}
let tracing_absolute_path = tracing_absolute_path(defaults)?;
ensure_parent_dir(&tracing_absolute_path)?;
let tracing_file = File::create(&tracing_absolute_path)?;
let (layer, level_filter) = compact(tracing_config);
let directives = directives(config, level_filter);
let filter = EnvFilter::builder()
.with_default_directive(level_filter.into())
.parse_lossy(directives);
let file_layer = layer
.with_ansi_sanitization(false)
.with_timer(UtcTime::new(Iso8601::DEFAULT))
.with_writer(tracing_file)
.with_filter(filter);
layers.push(file_layer.boxed());
try_init(layers)?;
Ok(())
}
fn directives(config: &ConfigSalusAgent, level_filter: LevelFilter) -> String {
let directives_base = match level_filter.into_level() {
Some(level) => match level {
Level::TRACE => "trace",
Level::DEBUG => "debug",
Level::INFO => "info",
Level::WARN => "warn",
Level::ERROR => "error",
},
None => "info",
};
if let Some(directives) = config.tracing().directives() {
format!("{directives_base},{directives}")
} else {
directives_base.to_string()
}
}
fn tracing_absolute_path<D>(defaults: &D) -> Result<PathBuf>
where
D: PathDefaults,
{
let default_fn = || -> Result<PathBuf> { default_tracing_absolute_path(defaults) };
defaults
.tracing_absolute_path()
.as_ref()
.map_or_else(default_fn, to_path_buf)
}
fn default_tracing_absolute_path<D>(defaults: &D) -> Result<PathBuf>
where
D: PathDefaults,
{
let base = dirs2::data_local_dir().ok_or(Error::LogDir)?;
Ok(log_file_in(&base, &defaults.app_name()))
}
fn log_file_in(base: &Path, app: &str) -> PathBuf {
base.join(app).join(app).with_extension("log")
}
#[cfg(test)]
mod test {
use std::path::Path;
use anyhow::Result;
use config::{Config, FileFormat};
use tracing::level_filters::LevelFilter;
use super::{directives, log_file_in};
use crate::config::ConfigSalusAgent;
#[test]
fn log_file_in_composes_app_dir_and_extension() {
let path = log_file_in(Path::new("/base"), "salus-agent");
assert_eq!(path, Path::new("/base/salus-agent/salus-agent.log"));
}
#[test]
fn directives_map_each_level_filter() {
let cfg = ConfigSalusAgent::default();
assert_eq!(directives(&cfg, LevelFilter::TRACE), "trace");
assert_eq!(directives(&cfg, LevelFilter::DEBUG), "debug");
assert_eq!(directives(&cfg, LevelFilter::INFO), "info");
assert_eq!(directives(&cfg, LevelFilter::WARN), "warn");
assert_eq!(directives(&cfg, LevelFilter::ERROR), "error");
assert_eq!(directives(&cfg, LevelFilter::OFF), "info");
}
#[test]
fn directives_appends_configured_directives() -> Result<()> {
let cfg: ConfigSalusAgent = Config::builder()
.add_source(config::File::from_str(
"[tracing]\ndirectives = \"mycrate=debug\"\n",
FileFormat::Toml,
))
.build()?
.try_deserialize()?;
assert_eq!(directives(&cfg, LevelFilter::INFO), "info,mycrate=debug");
Ok(())
}
}