use std::{env, path::Path};
use derive_more::FromStr;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use tracing_appender::{non_blocking, rolling};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, Layer};
pub(crate) const DEFAULT_FILE_RUST_LOG: &str = "rustyline=warn,debug";
pub fn env_file_rust_log(bin: &'static str) -> Option<anyhow::Result<EnvFilter>> {
let env_file_rust_log = format!("{}_FILE_RUST_LOG", bin.to_uppercase());
match env::var(env_file_rust_log) {
Ok(s) => Some(crate::logging::EnvFilter::from_str(&s).map_err(Into::into)),
Err(e) => match e {
env::VarError::NotPresent => None,
e @ env::VarError::NotUnicode(_) => Some(Err(e.into())),
},
}
}
pub fn init_terminal_and_file_logging(
file_log_config: &FileLogging,
logs_root: &Path,
) -> anyhow::Result<non_blocking::WorkerGuard> {
let terminal_layer = fmt::layer()
.with_writer(std::io::stderr)
.with_filter(tracing_subscriber::EnvFilter::from_default_env());
let (file_layer, guard) = {
let FileLogging {
rust_log,
max_files,
rotation,
dir,
} = file_log_config;
let filter = rust_log.layer();
let (file_logger, guard) = {
let file_appender = if *max_files == 0 || &filter.to_string() == "off" {
fmt::writer::OptionalWriter::none()
} else {
let rotation = match rotation {
Rotation::Hourly => rolling::Rotation::HOURLY,
Rotation::Daily => rolling::Rotation::DAILY,
Rotation::Never => rolling::Rotation::NEVER,
};
let logs_path = dir.clone().unwrap_or_else(|| logs_root.join("logs"));
let file_appender = rolling::Builder::new()
.rotation(rotation)
.max_log_files(*max_files)
.filename_prefix("iroh")
.filename_suffix("log")
.build(logs_path)?;
fmt::writer::OptionalWriter::some(file_appender)
};
non_blocking(file_appender)
};
let layer = fmt::Layer::new()
.with_ansi(false)
.with_line_number(true)
.with_writer(file_logger)
.with_filter(filter);
(layer, guard)
};
tracing_subscriber::registry()
.with(file_layer)
.with(terminal_layer)
.try_init()?;
Ok(guard)
}
pub fn init_terminal_logging() -> anyhow::Result<()> {
let terminal_layer = fmt::layer()
.with_writer(std::io::stderr)
.with_filter(tracing_subscriber::EnvFilter::from_default_env());
tracing_subscriber::registry()
.with(terminal_layer)
.try_init()?;
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct FileLogging {
pub rust_log: EnvFilter,
pub max_files: usize,
pub rotation: Rotation,
pub dir: Option<std::path::PathBuf>,
}
impl Default for FileLogging {
fn default() -> Self {
Self {
rust_log: EnvFilter::default(),
max_files: 4,
rotation: Rotation::default(),
dir: None,
}
}
}
#[derive(
Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr, derive_more::Display,
)]
#[display("{_0}")]
pub struct EnvFilter(String);
impl FromStr for EnvFilter {
type Err = <tracing_subscriber::EnvFilter as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let _valid_env = tracing_subscriber::EnvFilter::from_str(s)?;
Ok(EnvFilter(s.into()))
}
}
impl Default for EnvFilter {
fn default() -> Self {
Self(DEFAULT_FILE_RUST_LOG.into())
}
}
impl EnvFilter {
pub(crate) fn layer(&self) -> tracing_subscriber::EnvFilter {
tracing_subscriber::EnvFilter::from_str(&self.0).expect("validated RUST_LOG statement")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
#[serde(rename_all = "lowercase")]
#[allow(missing_docs)]
pub enum Rotation {
#[default]
Hourly,
Daily,
Never,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_file_rust_log() {
let _ = EnvFilter::default().layer();
}
}