use std::fmt;
use std::io::IsTerminal as _;
use thiserror::Error;
use tracing::field::{Field, Visit};
use tracing::{Event, Level};
use tracing_subscriber::EnvFilter;
use tracing_subscriber::filter::{Directive, ParseError};
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::time::{FormatTime as _, SystemTime};
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::util::SubscriberInitExt as _;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum LoggingInitError {
#[error("invalid log filter directive: {0}")]
Filter(#[from] ParseError),
#[error("a tracing subscriber is already installed")]
AlreadyInitialized(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
}
#[derive(Debug, Clone)]
pub struct Logging {
default_filter: String,
ansi: Option<bool>,
target: bool,
}
impl Default for Logging {
fn default() -> Self {
Self {
default_filter: "info".to_owned(),
ansi: None,
target: true,
}
}
}
impl Logging {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_default_filter(mut self, filter: impl Into<String>) -> Self {
self.default_filter = filter.into();
self
}
#[must_use]
pub const fn with_ansi(mut self, ansi: bool) -> Self {
self.ansi = Some(ansi);
self
}
#[must_use]
pub const fn with_target(mut self, target: bool) -> Self {
self.target = target;
self
}
pub fn try_init(self) -> Result<(), LoggingInitError> {
let directive: Directive = self.default_filter.parse()?;
let filter = EnvFilter::builder()
.with_default_directive(directive)
.with_env_var("RUST_LOG")
.from_env_lossy();
let ansi = self.ansi.unwrap_or_else(|| std::io::stderr().is_terminal());
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.event_format(ColorFormatter {
ansi,
target: self.target,
})
.finish()
.try_init()
.map_err(|err| LoggingInitError::AlreadyInitialized(err.into()))
}
}
pub fn init() -> Result<(), LoggingInitError> {
Logging::new().try_init()
}
#[derive(Debug, Clone, Copy)]
struct ColorFormatter {
ansi: bool,
target: bool,
}
impl ColorFormatter {
fn paint(self, w: &mut Writer<'_>, codes: &str, text: &str) -> fmt::Result {
if self.ansi {
write!(w, "\x1b[{codes}m{text}\x1b[0m")
} else {
write!(w, "{text}")
}
}
}
impl<S, N> FormatEvent<S, N> for ColorFormatter
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
_ctx: &FmtContext<'_, S, N>,
mut writer: Writer<'_>,
event: &Event<'_>,
) -> fmt::Result {
let meta = event.metadata();
let level = *meta.level();
if self.ansi {
write!(writer, "\x1b[2m")?;
SystemTime.format_time(&mut writer)?;
write!(writer, "\x1b[0m ")?;
} else {
SystemTime.format_time(&mut writer)?;
write!(writer, " ")?;
}
let color = level_color(level);
self.paint(
&mut writer,
&format!("1;{color}"),
&format!("{:>5}", meta.level()),
)?;
write!(writer, " ")?;
if self.target {
self.paint(&mut writer, "2;36", meta.target())?;
write!(writer, " ")?;
}
let mut visitor = EventVisitor::default();
event.record(&mut visitor);
if matches!(level, Level::WARN | Level::ERROR) {
self.paint(&mut writer, color, visitor.message.trim_end())?;
} else {
write!(writer, "{}", visitor.message.trim_end())?;
}
if !visitor.fields.is_empty() {
self.paint(&mut writer, "2", &visitor.fields)?;
}
writeln!(writer)
}
}
const fn level_color(level: Level) -> &'static str {
match level {
Level::ERROR => "31",
Level::WARN => "33",
Level::INFO => "32",
Level::DEBUG => "34",
Level::TRACE => "35",
}
}
#[derive(Debug, Default)]
struct EventVisitor {
message: String,
fields: String,
}
impl Visit for EventVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
use std::fmt::Write as _;
if field.name() == "message" {
let _ = write!(self.message, "{value:?}");
} else {
let _ = write!(self.fields, " {}={value:?}", field.name());
}
}
}
#[cfg(test)]
mod tests {
use super::Logging;
#[test]
fn defaults_are_info_auto_color_with_targets() {
let cfg = Logging::new();
assert_eq!(cfg.default_filter, "info");
assert!(cfg.ansi.is_none());
assert!(cfg.target);
}
#[test]
fn setters_override_defaults() {
let cfg = Logging::new()
.with_default_filter("ruststream=debug,warn")
.with_ansi(false)
.with_target(false);
assert_eq!(cfg.default_filter, "ruststream=debug,warn");
assert_eq!(cfg.ansi, Some(false));
assert!(!cfg.target);
}
#[test]
fn invalid_default_filter_is_rejected_at_init() {
let err = Logging::new()
.with_default_filter("=:not a directive:=")
.try_init();
assert!(err.is_err());
}
}