use std::fmt;
use tracing::field::Field;
use tracing::{Event, Level, Subscriber};
use tracing_indicatif::IndicatifLayer;
use tracing_subscriber::field::{RecordFields, Visit};
use tracing_subscriber::fmt::format::{FormatFields, Writer};
use tracing_subscriber::fmt::{FmtContext, FormatEvent};
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LogFormat {
#[default]
Structured,
Humane,
}
pub fn init_logging() -> Result<(), tracing_subscriber::util::TryInitError> {
init_logging_with_format(LogFormat::Structured)
}
pub fn init_logging_with_format(format: LogFormat) -> Result<(), tracing_subscriber::util::TryInitError> {
let filter = EnvFilter::builder()
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
.from_env_lossy();
let registry = tracing_subscriber::registry();
match format {
LogFormat::Structured => {
let indicatif_layer = IndicatifLayer::new();
registry
.with(
tracing_subscriber::fmt::layer()
.with_writer(indicatif_layer.get_stderr_writer())
.with_timer(tracing_subscriber::fmt::time::uptime())
.with_target(false)
.with_ansi(true)
.fmt_fields(CleanFields)
.compact(),
)
.with(indicatif_layer)
.with(filter)
.try_init()
}
LogFormat::Humane => {
let indicatif_layer = IndicatifLayer::new();
registry
.with(
tracing_subscriber::fmt::layer()
.with_writer(indicatif_layer.get_stderr_writer())
.fmt_fields(CleanFields)
.event_format(HumaneFormatter),
)
.with(indicatif_layer)
.with(filter)
.try_init()
}
}
}
pub struct CleanFields;
impl<'w> FormatFields<'w> for CleanFields {
fn format_fields<R: RecordFields>(&self, mut writer: Writer<'w>, fields: R) -> fmt::Result {
let mut v = CleanVisitor {
writer: &mut writer,
first: true,
};
fields.record(&mut v);
Ok(())
}
}
struct CleanVisitor<'a, 'w> {
writer: &'a mut Writer<'w>,
first: bool,
}
impl Visit for CleanVisitor<'_, '_> {
fn record_debug(&mut self, field: &Field, val: &dyn fmt::Debug) {
if field.name().starts_with("indicatif.") {
return;
}
let sep = if self.first { "" } else { " " };
if field.name() == "message" {
let _ = write!(self.writer, "{sep}{val:?}");
} else {
let _ = write!(self.writer, "{sep}{}={val:?}", field.name());
}
self.first = false;
}
fn record_str(&mut self, field: &Field, val: &str) {
if field.name().starts_with("indicatif.") {
return;
}
let sep = if self.first { "" } else { " " };
if field.name() == "message" {
let _ = write!(self.writer, "{sep}{val}");
} else {
let _ = write!(self.writer, "{sep}{}={val:?}", field.name());
}
self.first = false;
}
}
pub struct HumaneFormatter;
impl<S, N> FormatEvent<S, N> for HumaneFormatter
where
S: 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 mut visitor = HumaneVisitor {
message: String::new(),
name: None,
duration_ms: None,
url: None,
};
event.record(&mut visitor);
let metadata = event.metadata();
let level = metadata.level();
let ansi = writer.has_ansi_escapes();
let bold_green = if ansi { "\x1b[1;32m" } else { "" };
let bold_yellow = if ansi { "\x1b[1;33m" } else { "" };
let bold_red = if ansi { "\x1b[1;31m" } else { "" };
let reset = if ansi { "\x1b[0m" } else { "" };
if *level == Level::ERROR {
writeln!(
writer,
" {}Error:{} {}",
bold_red, reset, visitor.message
)?;
return Ok(());
}
if *level == Level::WARN {
writeln!(
writer,
" {}Warning:{} {}",
bold_yellow, reset, visitor.message
)?;
return Ok(());
}
if *level == Level::INFO {
if visitor.message == "Finished task" {
if let Some(name) = visitor.name {
let duration = visitor.duration_ms.unwrap_or(0);
writeln!(
writer,
" {}Finished{} {} in {}ms",
bold_green, reset, name, duration
)?;
return Ok(());
}
}
if visitor.message == "Finished copying static files" {
let duration = visitor.duration_ms.unwrap_or(0);
writeln!(
writer,
" {}Finished{} copying static files in {}ms",
bold_green, reset, duration
)?;
return Ok(());
}
if visitor.message == "Build complete!" {
writeln!(
writer,
" {}Finished{} build complete!",
bold_green, reset
)?;
return Ok(());
}
if let Some(url) = visitor.url {
writeln!(
writer,
" {}Serving{} at {}",
bold_green, reset, url
)?;
return Ok(());
}
writeln!(writer, " {}", visitor.message)?;
return Ok(());
}
writeln!(writer, " {}", visitor.message)?;
Ok(())
}
}
struct HumaneVisitor {
message: String,
name: Option<String>,
duration_ms: Option<u64>,
url: Option<String>,
}
impl Visit for HumaneVisitor {
fn record_debug(&mut self, field: &Field, val: &dyn fmt::Debug) {
let name = field.name();
if name == "message" {
self.message = clean_str(&format!("{val:?}"));
} else if name == "name" {
self.name = Some(clean_str(&format!("{val:?}")));
} else if name == "url" {
self.url = Some(clean_str(&format!("{val:?}")));
}
}
fn record_str(&mut self, field: &Field, val: &str) {
let name = field.name();
if name == "message" {
self.message = val.to_string();
} else if name == "name" {
self.name = Some(val.to_string());
} else if name == "url" {
self.url = Some(val.to_string());
}
}
fn record_u64(&mut self, field: &Field, val: u64) {
if field.name() == "duration_ms" {
self.duration_ms = Some(val);
}
}
fn record_i64(&mut self, field: &Field, val: i64) {
if field.name() == "duration_ms" && val >= 0 {
self.duration_ms = Some(val as u64);
}
}
}
fn clean_str(s: &str) -> String {
let s = s.trim();
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}