use std::{env::var, ffi::OsStr, io::IsTerminal, path::PathBuf};
use clap::{ArgAction, Parser, ValueEnum, ValueHint};
#[cfg(all(feature = "miette-7", feature = "anyhow-1"))]
compile_error!("features `miette-7` and `anyhow-1` are mutually exclusive");
#[cfg(feature = "tracing")]
pub use tracing_appender::non_blocking::WorkerGuard;
#[cfg(feature = "miette-7")]
type Error = miette::Report;
#[cfg(feature = "anyhow-1")]
type Error = anyhow::Error;
#[cfg(not(any(feature = "miette-7", feature = "anyhow-1")))]
type Error = Box<dyn std::error::Error + Sync + Send>;
type Result<T> = std::result::Result<T, Error>;
fn string_err<T>(err: &str) -> Result<T> {
#[cfg(feature = "miette-7")]
{
Err(miette::miette!("{err}"))
}
#[cfg(feature = "anyhow-1")]
{
Err(anyhow::anyhow!("{err}"))
}
#[cfg(not(any(feature = "miette-7", feature = "anyhow-1")))]
{
Err(err.into())
}
}
fn tracing_err(err: Box<dyn std::error::Error + Send + Sync>) -> Error {
#[cfg(feature = "miette-7")]
{
miette::Report::from_err(Box::leak(err) as &_)
}
#[cfg(feature = "anyhow-1")]
{
anyhow::anyhow!(err)
}
#[cfg(not(any(feature = "miette-7", feature = "anyhow-1")))]
{
err
}
}
#[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, value_name = "COUNT", default_value = "32")]
pub log_file_keep: usize,
#[arg(long)]
pub log_timeless: bool,
}
impl LoggingArgs {
#[cfg(feature = "tracing")]
pub fn setup(&self, level_map: impl FnOnce(u8) -> &'static str) -> Result<WorkerGuard> {
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, use_rolling) = 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",)),
true,
)
} else if let (Some(parent), Some(file_name)) = (file.parent(), file.file_name()) {
(parent.into(), PathBuf::from(file_name), false)
} else {
return string_err("Failed to determine log file name");
};
let appender = if use_rolling && self.log_file_keep > 0 {
let mut builder = rolling::RollingFileAppender::builder()
.rotation(rolling::Rotation::DAILY)
.filename_prefix(filename.to_string_lossy().to_string());
if self.log_file_keep > 0 {
builder = builder.max_log_files(self.log_file_keep);
}
builder
.build(&dir)
.map_err(|e| tracing_err(Box::new(e)))?
} else {
rolling::never(&dir, &filename)
};
non_blocking(appender)
} else {
non_blocking(stderr())
};
let color = ColourMode::from_env().or_if_auto(self.color);
let timeless = is_systemd() || 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_with_env(var_name: impl AsRef<OsStr>) -> Self {
let logline = var(var_name).ok().or_else(|| {
if var("DEBUG_INVOCATION").is_ok() {
Some("debug".into())
} else {
None
}
});
let timeless = is_systemd() || var("LOG_TIMELESS").is_ok();
let color = ColourMode::from_env();
Self {
logline,
timeless,
color,
}
}
pub fn parse() -> Self {
Self::parse_with_env("RUST_LOG")
}
#[cfg(feature = "tracing")]
pub fn setup(&self) -> Result<Option<WorkerGuard>> {
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))
.map_err(tracing_err)
} else {
sub.try_init().map(|_| Some(guard)).map_err(tracing_err)
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum ColourMode {
#[default]
Auto,
Always,
Never,
}
impl ColourMode {
pub(crate) fn or_if_auto(self, value: Self) -> Self {
match self {
Self::Auto => value,
other => other,
}
}
pub fn enabled(self) -> bool {
match self {
Self::Auto => supports_color::on(supports_color::Stream::Stderr).is_some(),
Self::Always => true,
Self::Never => false,
}
}
#[deprecated(since = "1.2.0", note = "use from_env instead")]
pub fn with_env(self) -> Self {
if var("NO_COLOR").is_ok() {
Self::Never
} else {
self
}
}
pub fn from_env() -> Self {
if supports_color::on(supports_color::Stream::Stderr).is_none() {
Self::Never
} else if var("CLICOLOR_FORCE").is_ok()
|| var("FORCE_COLOR").map_or(false, |force| match force.as_ref() {
"true" | "" => true,
"false" => false,
f => f.parse().unwrap_or(1) > 0,
}) {
Self::Always
} else if enable_ansi_support::enable_ansi_support().is_err() {
Self::Never
} else {
Self::Auto
}
}
#[deprecated(since = "1.2.0", note = "use from_env instead")]
pub fn with_windows(self) -> Self {
match self {
Self::Never => Self::Never,
mode => {
if enable_ansi_support::enable_ansi_support().is_err() {
Self::Never
} else {
mode
}
}
}
}
}
fn is_terminal() -> bool {
std::io::stderr().is_terminal()
}
fn is_systemd() -> bool {
(var("JOURNAL_STREAM").is_ok() && !is_terminal()) || var("DEBUG_INVOCATION").is_ok()
}