feat: setup span recording for CustomJsonFormatter, use 'yansi' for better ANSI terminal colors in CustomPrettyFormatter

This commit is contained in:
2025-09-13 18:40:55 -05:00
parent b64aa41b14
commit 19b3a98f66
4 changed files with 66 additions and 30 deletions

1
Cargo.lock generated
View File

@@ -258,6 +258,7 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"url", "url",
"yansi",
] ]
[[package]] [[package]]

View File

@@ -49,6 +49,7 @@ rust-embed = { version = "8.0", features = ["debug-embed", "include-exclude"] }
mime_guess = "2.0" mime_guess = "2.0"
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
rapidhash = "4.1.0" rapidhash = "4.1.0"
yansi = "1.0.1"
[dev-dependencies] [dev-dependencies]

View File

@@ -10,6 +10,7 @@ use tracing::{Event, Level, Subscriber};
use tracing_subscriber::fmt::format::Writer; use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields, FormattedFields}; use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields, FormattedFields};
use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::registry::LookupSpan;
use yansi::Paint;
/// Cached format description for timestamps /// Cached format description for timestamps
/// Uses 3 subsecond digits on Emscripten, 5 otherwise for better performance /// 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. /// Re-implementation of the Full formatter with improved timestamp display.
pub struct CustomPrettyFormatter; 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<S, N> FormatEvent<S, N> for CustomPrettyFormatter impl<S, N> FormatEvent<S, N> for CustomPrettyFormatter
where where
S: Subscriber + for<'a> LookupSpan<'a>, S: Subscriber + for<'a> LookupSpan<'a>,
@@ -63,20 +59,20 @@ where
for span in scope.from_root() { for span in scope.from_root() {
write_bold(&mut writer, span.metadata().name())?; write_bold(&mut writer, span.metadata().name())?;
saw_any = true; saw_any = true;
write_dimmed(&mut writer, ":")?;
let ext = span.extensions(); let ext = span.extensions();
if let Some(fields) = &ext.get::<FormattedFields<N>>() { if let Some(fields) = &ext.get::<FormattedFields<N>>()
if !fields.is_empty() { && !fields.fields.is_empty()
{
write_bold(&mut writer, "{")?; write_bold(&mut writer, "{")?;
write!(writer, "{}", fields)?; writer.write_str(fields.fields.as_str())?;
write_bold(&mut writer, "}")?; write_bold(&mut writer, "}")?;
} }
write_dimmed(&mut writer, ":")?;
} }
if writer.has_ansi_escapes() {
write!(writer, "\x1b[2m:\x1b[0m")?;
} else {
writer.write_char(':')?;
}
}
if saw_any { if saw_any {
writer.write_char(' ')?; writer.write_char(' ')?;
} }
@@ -84,7 +80,7 @@ where
// 4) Target (dimmed), then a space // 4) Target (dimmed), then a space
if writer.has_ansi_escapes() { 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 { } else {
write!(writer, "{}: ", meta.target())?; 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<S, N> FormatEvent<S, N> for CustomJsonFormatter impl<S, N> FormatEvent<S, N> for CustomJsonFormatter
where where
S: Subscriber + for<'a> LookupSpan<'a>, S: Subscriber + for<'a> LookupSpan<'a>,
@@ -104,7 +105,7 @@ where
{ {
fn format_event( fn format_event(
&self, &self,
_ctx: &FmtContext<'_, S, N>, ctx: &FmtContext<'_, S, N>,
mut writer: Writer<'_>, mut writer: Writer<'_>,
event: &Event<'_>, event: &Event<'_>,
) -> fmt::Result { ) -> fmt::Result {
@@ -116,12 +117,15 @@ where
level: String, level: String,
target: String, target: String,
#[serde(flatten)] #[serde(flatten)]
spans: Map<String, Value>,
#[serde(flatten)]
fields: Map<String, Value>, fields: Map<String, Value>,
} }
let (message, fields) = { let (message, fields, spans) = {
let mut message: Option<String> = None; let mut message: Option<String> = None;
let mut fields: Map<String, Value> = Map::new(); let mut fields: Map<String, Value> = Map::new();
let mut spans: Map<String, Value> = Map::new();
struct FieldVisitor<'a> { struct FieldVisitor<'a> {
message: &'a mut Option<String>, message: &'a mut Option<String>,
@@ -184,13 +188,42 @@ where
}; };
event.record(&mut visitor); 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<String, Value> = Map::new();
// Try to extract fields from FormattedFields
let ext = span.extensions();
if let Some(formatted_fields) = ext.get::<FormattedFields<N>>() {
// Try to parse as JSON first
if let Ok(json_fields) = serde_json::from_str::<Map<String, Value>>(
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 { let json = EventFields {
message: message.unwrap_or_default(), message: message.unwrap_or_default(),
level: meta.level().to_string(), level: meta.level().to_string(),
target: meta.target().to_string(), target: meta.target().to_string(),
spans,
fields, fields,
}; };
@@ -205,15 +238,14 @@ where
/// Write the verbosity level with the same coloring/alignment as the Full formatter. /// Write the verbosity level with the same coloring/alignment as the Full formatter.
fn write_colored_level(writer: &mut Writer<'_>, level: &Level) -> fmt::Result { fn write_colored_level(writer: &mut Writer<'_>, level: &Level) -> fmt::Result {
if writer.has_ansi_escapes() { if writer.has_ansi_escapes() {
// Basic ANSI color sequences; reset with \x1b[0m let paint = match *level {
let (color, text) = match *level { Level::TRACE => Paint::new("TRACE").magenta(),
Level::TRACE => ("\x1b[35m", "TRACE"), // purple Level::DEBUG => Paint::new("DEBUG").blue(),
Level::DEBUG => ("\x1b[34m", "DEBUG"), // blue Level::INFO => Paint::new(" INFO").green(),
Level::INFO => ("\x1b[32m", " INFO"), // green, note leading space Level::WARN => Paint::new(" WARN").yellow(),
Level::WARN => ("\x1b[33m", " WARN"), // yellow, note leading space Level::ERROR => Paint::new("ERROR").red(),
Level::ERROR => ("\x1b[31m", "ERROR"), // red
}; };
write!(writer, "{}{}\x1b[0m", color, text) write!(writer, "{}", paint)
} else { } else {
// Right-pad to width 5 like Full's non-ANSI mode // Right-pad to width 5 like Full's non-ANSI mode
match *level { 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 { fn write_dimmed(writer: &mut Writer<'_>, s: impl fmt::Display) -> fmt::Result {
if writer.has_ansi_escapes() { if writer.has_ansi_escapes() {
write!(writer, "\x1b[2m{}\x1b[0m", s) write!(writer, "{}", Paint::new(s).dim())
} else { } else {
write!(writer, "{}", s) 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 { fn write_bold(writer: &mut Writer<'_>, s: impl fmt::Display) -> fmt::Result {
if writer.has_ansi_escapes() { if writer.has_ansi_escapes() {
write!(writer, "\x1b[1m{}\x1b[0m", s) write!(writer, "{}", Paint::new(s).bold())
} else { } else {
write!(writer, "{}", s) write!(writer, "{}", s)
} }

View File

@@ -4,6 +4,7 @@ use num_format::{Locale, ToFormattedString};
use serenity::all::{ActivityData, ClientBuilder, GatewayIntents}; use serenity::all::{ActivityData, ClientBuilder, GatewayIntents};
use tokio::signal; use tokio::signal;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use tracing_subscriber::fmt::format::JsonFields;
use tracing_subscriber::{EnvFilter, FmtSubscriber}; use tracing_subscriber::{EnvFilter, FmtSubscriber};
use crate::banner::BannerApi; use crate::banner::BannerApi;
@@ -185,6 +186,7 @@ async fn main() {
FmtSubscriber::builder() FmtSubscriber::builder()
.with_target(true) .with_target(true)
.event_format(formatter::CustomJsonFormatter) .event_format(formatter::CustomJsonFormatter)
.fmt_fields(JsonFields::new())
.with_env_filter(filter) .with_env_filter(filter)
.finish(), .finish(),
) )