diff --git a/Cargo.lock b/Cargo.lock index ee84e01..21cda83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,6 +258,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "yansi", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e6e2402..ad79e96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ rust-embed = { version = "8.0", features = ["debug-embed", "include-exclude"] } mime_guess = "2.0" clap = { version = "4.5", features = ["derive"] } rapidhash = "4.1.0" +yansi = "1.0.1" [dev-dependencies] diff --git a/src/formatter.rs b/src/formatter.rs index 39e7d6f..7d9e789 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -10,6 +10,7 @@ use tracing::{Event, Level, Subscriber}; use tracing_subscriber::fmt::format::Writer; use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields, FormattedFields}; use tracing_subscriber::registry::LookupSpan; +use yansi::Paint; /// Cached format description for timestamps /// Uses 3 subsecond digits on Emscripten, 5 otherwise for better performance @@ -26,11 +27,6 @@ const TIMESTAMP_FORMAT: &[FormatItem<'static>] = /// Re-implementation of the Full formatter with improved timestamp display. pub struct CustomPrettyFormatter; -/// A custom JSON formatter that flattens fields to root level -/// -/// Outputs logs in the format: { "message": "...", "level": "...", "customAttribute": "..." } -pub struct CustomJsonFormatter; - impl FormatEvent for CustomPrettyFormatter where S: Subscriber + for<'a> LookupSpan<'a>, @@ -63,20 +59,20 @@ where for span in scope.from_root() { write_bold(&mut writer, span.metadata().name())?; saw_any = true; + + write_dimmed(&mut writer, ":")?; + let ext = span.extensions(); - if let Some(fields) = &ext.get::>() { - if !fields.is_empty() { - write_bold(&mut writer, "{")?; - write!(writer, "{}", fields)?; - write_bold(&mut writer, "}")?; - } - } - if writer.has_ansi_escapes() { - write!(writer, "\x1b[2m:\x1b[0m")?; - } else { - writer.write_char(':')?; + if let Some(fields) = &ext.get::>() + && !fields.fields.is_empty() + { + write_bold(&mut writer, "{")?; + writer.write_str(fields.fields.as_str())?; + write_bold(&mut writer, "}")?; } + write_dimmed(&mut writer, ":")?; } + if saw_any { writer.write_char(' ')?; } @@ -84,7 +80,7 @@ where // 4) Target (dimmed), then a space if writer.has_ansi_escapes() { - write!(writer, "\x1b[2m{}\x1b[0m\x1b[2m:\x1b[0m ", meta.target())?; + write!(writer, "{}: ", Paint::new(meta.target()).dim())?; } else { write!(writer, "{}: ", meta.target())?; } @@ -97,6 +93,11 @@ where } } +/// A custom JSON formatter that flattens fields to root level +/// +/// Outputs logs in the format: { "message": "...", "level": "...", "customAttribute": "..." } +pub struct CustomJsonFormatter; + impl FormatEvent for CustomJsonFormatter where S: Subscriber + for<'a> LookupSpan<'a>, @@ -104,7 +105,7 @@ where { fn format_event( &self, - _ctx: &FmtContext<'_, S, N>, + ctx: &FmtContext<'_, S, N>, mut writer: Writer<'_>, event: &Event<'_>, ) -> fmt::Result { @@ -116,12 +117,15 @@ where level: String, target: String, #[serde(flatten)] + spans: Map, + #[serde(flatten)] fields: Map, } - let (message, fields) = { + let (message, fields, spans) = { let mut message: Option = None; let mut fields: Map = Map::new(); + let mut spans: Map = Map::new(); struct FieldVisitor<'a> { message: &'a mut Option, @@ -184,13 +188,42 @@ where }; event.record(&mut visitor); - (message, fields) + // Collect span information from the span hierarchy + if let Some(scope) = ctx.event_scope() { + for span in scope.from_root() { + let span_name = span.metadata().name().to_string(); + let mut span_fields: Map = Map::new(); + + // Try to extract fields from FormattedFields + let ext = span.extensions(); + if let Some(formatted_fields) = ext.get::>() { + // Try to parse as JSON first + if let Ok(json_fields) = serde_json::from_str::>( + formatted_fields.fields.as_str(), + ) { + span_fields.extend(json_fields); + } else { + // If not valid JSON, treat the entire field string as a single field + span_fields.insert( + "raw".to_string(), + Value::String(formatted_fields.fields.as_str().to_string()), + ); + } + } + + // Insert span as a nested object directly into the spans map + spans.insert(span_name, Value::Object(span_fields)); + } + } + + (message, fields, spans) }; let json = EventFields { message: message.unwrap_or_default(), level: meta.level().to_string(), target: meta.target().to_string(), + spans, fields, }; @@ -205,15 +238,14 @@ where /// Write the verbosity level with the same coloring/alignment as the Full formatter. fn write_colored_level(writer: &mut Writer<'_>, level: &Level) -> fmt::Result { if writer.has_ansi_escapes() { - // Basic ANSI color sequences; reset with \x1b[0m - let (color, text) = match *level { - Level::TRACE => ("\x1b[35m", "TRACE"), // purple - Level::DEBUG => ("\x1b[34m", "DEBUG"), // blue - Level::INFO => ("\x1b[32m", " INFO"), // green, note leading space - Level::WARN => ("\x1b[33m", " WARN"), // yellow, note leading space - Level::ERROR => ("\x1b[31m", "ERROR"), // red + let paint = match *level { + Level::TRACE => Paint::new("TRACE").magenta(), + Level::DEBUG => Paint::new("DEBUG").blue(), + Level::INFO => Paint::new(" INFO").green(), + Level::WARN => Paint::new(" WARN").yellow(), + Level::ERROR => Paint::new("ERROR").red(), }; - write!(writer, "{}{}\x1b[0m", color, text) + write!(writer, "{}", paint) } else { // Right-pad to width 5 like Full's non-ANSI mode match *level { @@ -228,7 +260,7 @@ fn write_colored_level(writer: &mut Writer<'_>, level: &Level) -> fmt::Result { fn write_dimmed(writer: &mut Writer<'_>, s: impl fmt::Display) -> fmt::Result { if writer.has_ansi_escapes() { - write!(writer, "\x1b[2m{}\x1b[0m", s) + write!(writer, "{}", Paint::new(s).dim()) } else { write!(writer, "{}", s) } @@ -236,7 +268,7 @@ fn write_dimmed(writer: &mut Writer<'_>, s: impl fmt::Display) -> fmt::Result { fn write_bold(writer: &mut Writer<'_>, s: impl fmt::Display) -> fmt::Result { if writer.has_ansi_escapes() { - write!(writer, "\x1b[1m{}\x1b[0m", s) + write!(writer, "{}", Paint::new(s).bold()) } else { write!(writer, "{}", s) } diff --git a/src/main.rs b/src/main.rs index c37405c..bab4545 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use num_format::{Locale, ToFormattedString}; use serenity::all::{ActivityData, ClientBuilder, GatewayIntents}; use tokio::signal; use tracing::{debug, error, info, warn}; +use tracing_subscriber::fmt::format::JsonFields; use tracing_subscriber::{EnvFilter, FmtSubscriber}; use crate::banner::BannerApi; @@ -185,6 +186,7 @@ async fn main() { FmtSubscriber::builder() .with_target(true) .event_format(formatter::CustomJsonFormatter) + .fmt_fields(JsonFields::new()) .with_env_filter(filter) .finish(), )