use std::{env::var, io::IsTerminal, path::PathBuf};
use clap::{ArgAction, Parser, ValueEnum, ValueHint};
#[cfg(feature = "tracing")]
pub use tracing_appender::non_blocking::WorkerGuard;
#[derive(Debug, Clone, Parser)]
pub struct LoggingArgs {
#[arg(long, default_value = "auto", value_name = "MODE", alias = "colour")]
pub color: ColourMode,
#[arg(
long,
short,
action = ArgAction::Count,
num_args = 0,
default_value = "0",
)]
pub verbose: u8,
#[arg(
long,
num_args = 0..=1,
default_missing_value = ".",
value_hint = ValueHint::AnyPath,
value_name = "PATH",
)]
pub log_file: Option<PathBuf>,
#[arg(long)]
pub log_timeless: bool,
}
impl LoggingArgs {
#[cfg(feature = "tracing")]
pub fn setup(
&self,
level_map: impl FnOnce(u8) -> &'static str,
) -> Result<WorkerGuard, Box<dyn std::error::Error + Sync + Send>> {
use std::{env::current_exe, fs::metadata, io::stderr};
use time::{macros::format_description, OffsetDateTime};
use tracing_appender::{non_blocking, rolling};
let (log_writer, guard) = if let Some(file) = &self.log_file {
let is_dir = metadata(file).is_ok_and(|info| info.is_dir());
let (dir, filename) = if is_dir {
let progname = current_exe()
.ok()
.and_then(|path| {
path.file_stem()
.map(|stem| stem.to_string_lossy().to_string())
})
.unwrap_or(env!("CARGO_PKG_NAME").into());
let time = OffsetDateTime::now_utc()
.format(format_description!(
"[year]-[month]-[day]T[hour]-[minute]-[second]Z"
))
.unwrap_or("debug".into());
(
file.to_owned(),
PathBuf::from(format!("{progname}.{time}.log",)),
)
} else if let (Some(parent), Some(file_name)) = (file.parent(), file.file_name()) {
(parent.into(), PathBuf::from(file_name))
} else {
return Err("Failed to determine log file name".into());
};
non_blocking(rolling::never(dir, filename))
} else {
non_blocking(stderr())
};
let color = self.color.with_env().with_windows();
let timeless =
var("JOURNAL_STREAM").is_ok() || var("DEBUG_INVOCATION").is_ok() || self.log_timeless;
let mut builder = tracing_subscriber::fmt()
.with_env_filter(level_map(self.verbose))
.with_ansi(color.enabled())
.with_writer(log_writer);
if self.verbose > 0 {
use tracing_subscriber::fmt::format::FmtSpan;
builder = builder.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
}
if self.log_file.is_some() {
builder.json().init();
} else if timeless {
builder.without_time().init();
} else {
builder.init();
}
Ok(guard)
}
}
#[derive(Debug, Clone)]
pub struct PreArgs {
pub logline: Option<String>,
pub timeless: bool,
pub color: ColourMode,
}
impl PreArgs {
pub fn parse() -> Self {
let logline = var("RUST_LOG").ok().or_else(|| {
if var("DEBUG_INVOCATION").is_ok() {
Some("debug".into())
} else {
None
}
});
let timeless = var("JOURNAL_STREAM").is_ok()
|| var("DEBUG_INVOCATION").is_ok()
|| var("LOG_TIMELESS").is_ok();
let color = ColourMode::default().with_env().with_windows();
Self {
logline,
timeless,
color,
}
}
#[cfg(feature = "tracing")]
pub fn setup(&self) -> Result<Option<WorkerGuard>, Box<dyn std::error::Error + Sync + Send>> {
use std::io::stderr;
use tracing_appender::non_blocking;
use tracing_subscriber::EnvFilter;
let Some(logline) = self.logline.as_ref() else {
return Ok(None);
};
let (writer, guard) = non_blocking(stderr());
let sub = tracing_subscriber::fmt()
.with_ansi(self.color.enabled())
.with_env_filter(EnvFilter::new(logline))
.with_writer(writer);
if self.timeless {
sub.without_time().try_init().map(|_| Some(guard))
} else {
sub.try_init().map(|_| Some(guard))
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum ColourMode {
#[default]
Auto,
Always,
Never,
}
impl ColourMode {
pub fn enabled(self) -> bool {
match self {
ColourMode::Auto => std::io::stderr().is_terminal(),
ColourMode::Always => true,
ColourMode::Never => false,
}
}
pub fn with_env(self) -> Self {
if var("NO_COLOR").is_ok() {
ColourMode::Never
} else {
self
}
}
pub fn with_windows(self) -> Self {
match self {
ColourMode::Never => ColourMode::Never,
mode => {
if enable_ansi_support::enable_ansi_support().is_err() {
ColourMode::Never
} else {
mode
}
}
}
}
}