use std::{env::var, ffi::OsStr, io::IsTerminal, path::PathBuf};
use clap::{ArgAction, Parser, ValueEnum, ValueHint};
#[cfg(feature = "tracing")]
pub use tracing_appender::non_blocking::WorkerGuard;
#[cfg(feature = "miette-7")]
type Error = miette::Report;
#[cfg(not(feature = "miette-7"))]
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(not(feature = "miette-7"))]
{
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(not(feature = "miette-7"))]
{
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)]
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) = 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 string_err("Failed to determine log file name");
};
non_blocking(rolling::never(dir, filename))
} 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 => is_terminal(),
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 var("NO_COLOR").is_ok() {
Self::Never
} else if var("CLICOLOR_FORCE").is_ok() {
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()
}