use std::fmt;
use std::sync::Mutex;
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::layer::{Context, Layer};
use tracing_subscriber::registry::LookupSpan;
use crate::console::Console;
use crate::style::Style;
use crate::text::Text;
struct FieldVisitor {
message: Option<String>,
fields: Vec<(String, String)>,
}
impl FieldVisitor {
fn new() -> Self {
Self {
message: None,
fields: Vec::new(),
}
}
}
impl Visit for FieldVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
if field.name() == "message" {
self.message = Some(format!("{:?}", value));
} else {
self.fields
.push((field.name().to_string(), format!("{:?}", value)));
}
}
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "message" {
self.message = Some(value.to_string());
} else {
self.fields
.push((field.name().to_string(), value.to_string()));
}
}
fn record_i64(&mut self, field: &Field, value: i64) {
self.fields
.push((field.name().to_string(), value.to_string()));
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.fields
.push((field.name().to_string(), value.to_string()));
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.fields
.push((field.name().to_string(), value.to_string()));
}
fn record_f64(&mut self, field: &Field, value: f64) {
self.fields
.push((field.name().to_string(), value.to_string()));
}
}
pub struct GiltLayer {
console: Mutex<Console>,
show_time: bool,
show_target: bool,
show_level: bool,
show_span_path: bool,
}
impl GiltLayer {
pub fn new() -> Self {
Self {
console: Mutex::new(Console::new()),
show_time: true,
show_target: true,
show_level: true,
show_span_path: true,
}
}
#[must_use]
pub fn with_console(mut self, console: Console) -> Self {
self.console = Mutex::new(console);
self
}
#[must_use]
pub fn with_show_time(mut self, show: bool) -> Self {
self.show_time = show;
self
}
#[must_use]
pub fn with_show_target(mut self, show: bool) -> Self {
self.show_target = show;
self
}
#[must_use]
pub fn with_show_level(mut self, show: bool) -> Self {
self.show_level = show;
self
}
#[must_use]
pub fn with_show_span_path(mut self, show: bool) -> Self {
self.show_span_path = show;
self
}
fn level_style(level: &Level) -> Style {
let spec = match *level {
Level::ERROR => "bold red",
Level::WARN => "bold yellow",
Level::INFO => "bold blue",
Level::DEBUG => "bold green",
Level::TRACE => "dim",
};
Style::parse(spec).unwrap_or_else(|_| Style::null())
}
fn render_time() -> Text {
let now = {
use std::time::{SystemTime, UNIX_EPOCH};
let dur = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let total_secs = dur.as_secs();
let hours = (total_secs / 3600) % 24;
let minutes = (total_secs / 60) % 60;
let seconds = total_secs % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
};
let dim_style = Style::parse("dim").unwrap_or_else(|_| Style::null());
Text::styled(&now, dim_style)
}
fn render_level(level: &Level) -> Text {
let name = match *level {
Level::ERROR => "ERROR",
Level::WARN => "WARN",
Level::INFO => "INFO",
Level::DEBUG => "DEBUG",
Level::TRACE => "TRACE",
};
let padded = format!("{:<8}", name);
let style = Self::level_style(level);
Text::styled(&padded, style)
}
fn render_target(target: &str) -> Text {
let dim_style = Style::parse("dim").unwrap_or_else(|_| Style::null());
Text::styled(target, dim_style)
}
fn render_fields(fields: &[(String, String)]) -> Text {
if fields.is_empty() {
return Text::new("", Style::null());
}
let parts: Vec<String> = fields.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
let joined = parts.join(" ");
let style = Style::parse("dim italic").unwrap_or_else(|_| Style::null());
Text::styled(&joined, style)
}
fn emit<S: Subscriber + for<'a> LookupSpan<'a>>(
&self,
event: &Event<'_>,
ctx: &Context<'_, S>,
) {
let mut visitor = FieldVisitor::new();
event.record(&mut visitor);
let metadata = event.metadata();
let mut parts = Text::new("", Style::null());
if self.show_time {
let time_text = Self::render_time();
parts.append_text(&time_text);
parts.append_str(" ", None);
}
if self.show_level {
let level_text = Self::render_level(metadata.level());
parts.append_text(&level_text);
parts.append_str(" ", None);
}
if self.show_span_path {
if let Some(scope) = ctx.event_scope(event) {
let span_names: Vec<&str> = scope.from_root().map(|s| s.name()).collect();
if !span_names.is_empty() {
let path = span_names.join(":");
let span_style = Style::parse("italic cyan").unwrap_or_else(|_| Style::null());
let span_text = Text::styled(&path, span_style);
parts.append_text(&span_text);
parts.append_str(" ", None);
}
}
}
let message = visitor.message.unwrap_or_default();
parts.append_str(&message, None);
if !visitor.fields.is_empty() {
parts.append_str(" ", None);
let fields_text = Self::render_fields(&visitor.fields);
parts.append_text(&fields_text);
}
if self.show_target {
let target = metadata.target();
if !target.is_empty() {
parts.append_str(" ", None);
let target_text = Self::render_target(target);
parts.append_text(&target_text);
}
}
if let Ok(mut console) = self.console.lock() {
console.print(&parts);
}
}
}
impl Default for GiltLayer {
fn default() -> Self {
Self::new()
}
}
impl<S> Layer<S> for GiltLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
self.emit(event, &ctx);
}
}
pub fn install() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use tracing_subscriber::prelude::*;
tracing_subscriber::registry()
.with(GiltLayer::new())
.try_init()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::Console;
use tracing_subscriber::prelude::*;
#[test]
fn test_default_construction() {
let layer = GiltLayer::new();
assert!(layer.show_time);
assert!(layer.show_target);
assert!(layer.show_level);
assert!(layer.show_span_path);
}
#[test]
fn test_default_trait() {
let layer = GiltLayer::default();
assert!(layer.show_time);
assert!(layer.show_target);
assert!(layer.show_level);
assert!(layer.show_span_path);
}
#[test]
fn test_builder_show_time() {
let layer = GiltLayer::new().with_show_time(false);
assert!(!layer.show_time);
}
#[test]
fn test_builder_show_target() {
let layer = GiltLayer::new().with_show_target(false);
assert!(!layer.show_target);
}
#[test]
fn test_builder_show_level() {
let layer = GiltLayer::new().with_show_level(false);
assert!(!layer.show_level);
}
#[test]
fn test_builder_show_span_path() {
let layer = GiltLayer::new().with_show_span_path(false);
assert!(!layer.show_span_path);
}
#[test]
fn test_builder_console() {
let console = Console::builder().width(120).build();
let _layer = GiltLayer::new().with_console(console);
}
#[test]
fn test_error_style_is_bold_red() {
let style = GiltLayer::level_style(&Level::ERROR);
assert_eq!(style.bold(), Some(true));
assert!(style.color().is_some());
assert_eq!(style.color().unwrap().name, "red");
}
#[test]
fn test_warn_style_is_bold_yellow() {
let style = GiltLayer::level_style(&Level::WARN);
assert_eq!(style.bold(), Some(true));
assert_eq!(style.color().unwrap().name, "yellow");
}
#[test]
fn test_info_style_is_bold_blue() {
let style = GiltLayer::level_style(&Level::INFO);
assert_eq!(style.bold(), Some(true));
assert_eq!(style.color().unwrap().name, "blue");
}
#[test]
fn test_debug_style_is_bold_green() {
let style = GiltLayer::level_style(&Level::DEBUG);
assert_eq!(style.bold(), Some(true));
assert_eq!(style.color().unwrap().name, "green");
}
#[test]
fn test_trace_style_is_dim() {
let style = GiltLayer::level_style(&Level::TRACE);
assert_eq!(style.dim(), Some(true));
}
#[test]
fn test_render_time_format() {
let time_text = GiltLayer::render_time();
let plain = time_text.plain().to_string();
assert_eq!(plain.len(), 8);
assert_eq!(plain.as_bytes()[2], b':');
assert_eq!(plain.as_bytes()[5], b':');
}
#[test]
fn test_render_time_has_dim_style() {
let time_text = GiltLayer::render_time();
assert!(!time_text.spans().is_empty());
}
#[test]
fn test_render_level_error() {
let text = GiltLayer::render_level(&Level::ERROR);
assert_eq!(text.plain(), "ERROR ");
}
#[test]
fn test_render_level_warn() {
let text = GiltLayer::render_level(&Level::WARN);
assert_eq!(text.plain(), "WARN ");
}
#[test]
fn test_render_level_info() {
let text = GiltLayer::render_level(&Level::INFO);
assert_eq!(text.plain(), "INFO ");
}
#[test]
fn test_render_level_debug() {
let text = GiltLayer::render_level(&Level::DEBUG);
assert_eq!(text.plain(), "DEBUG ");
}
#[test]
fn test_render_level_trace() {
let text = GiltLayer::render_level(&Level::TRACE);
assert_eq!(text.plain(), "TRACE ");
}
#[test]
fn test_render_level_has_style() {
for level in &[
Level::ERROR,
Level::WARN,
Level::INFO,
Level::DEBUG,
Level::TRACE,
] {
let text = GiltLayer::render_level(level);
assert!(
!text.spans().is_empty(),
"level {:?} should have a styled span",
level
);
}
}
#[test]
fn test_render_target() {
let text = GiltLayer::render_target("my_crate::module");
assert_eq!(text.plain(), "my_crate::module");
assert!(!text.spans().is_empty());
}
#[test]
fn test_render_target_empty() {
let text = GiltLayer::render_target("");
assert_eq!(text.plain(), "");
}
#[test]
fn test_render_fields_empty() {
let text = GiltLayer::render_fields(&[]);
assert_eq!(text.plain(), "");
}
#[test]
fn test_render_fields_single() {
let fields = vec![("user".to_string(), "alice".to_string())];
let text = GiltLayer::render_fields(&fields);
assert_eq!(text.plain(), "user=alice");
assert!(!text.spans().is_empty());
}
#[test]
fn test_render_fields_multiple() {
let fields = vec![
("user".to_string(), "alice".to_string()),
("status".to_string(), "200".to_string()),
];
let text = GiltLayer::render_fields(&fields);
assert_eq!(text.plain(), "user=alice status=200");
}
#[test]
fn test_field_visitor_record_str_message() {
let mut visitor = FieldVisitor::new();
let field = tracing::field::FieldSet::new(
&["message"],
tracing::callsite::Identifier(&TestCallsite),
)
.field("message")
.unwrap();
visitor.record_str(&field, "hello world");
assert_eq!(visitor.message.as_deref(), Some("hello world"));
assert!(visitor.fields.is_empty());
}
#[test]
fn test_field_visitor_record_str_other() {
let mut visitor = FieldVisitor::new();
let field_set =
tracing::field::FieldSet::new(&["user"], tracing::callsite::Identifier(&TestCallsite));
let field = field_set.field("user").unwrap();
visitor.record_str(&field, "bob");
assert!(visitor.message.is_none());
assert_eq!(visitor.fields.len(), 1);
assert_eq!(visitor.fields[0].0, "user");
assert_eq!(visitor.fields[0].1, "bob");
}
#[test]
fn test_field_visitor_record_i64() {
let mut visitor = FieldVisitor::new();
let field_set =
tracing::field::FieldSet::new(&["count"], tracing::callsite::Identifier(&TestCallsite));
let field = field_set.field("count").unwrap();
visitor.record_i64(&field, -42);
assert_eq!(visitor.fields[0], ("count".to_string(), "-42".to_string()));
}
#[test]
fn test_field_visitor_record_u64() {
let mut visitor = FieldVisitor::new();
let field_set =
tracing::field::FieldSet::new(&["count"], tracing::callsite::Identifier(&TestCallsite));
let field = field_set.field("count").unwrap();
visitor.record_u64(&field, 99);
assert_eq!(visitor.fields[0], ("count".to_string(), "99".to_string()));
}
#[test]
fn test_field_visitor_record_bool() {
let mut visitor = FieldVisitor::new();
let field_set = tracing::field::FieldSet::new(
&["active"],
tracing::callsite::Identifier(&TestCallsite),
);
let field = field_set.field("active").unwrap();
visitor.record_bool(&field, true);
assert_eq!(
visitor.fields[0],
("active".to_string(), "true".to_string())
);
}
#[test]
fn test_field_visitor_record_f64() {
let mut visitor = FieldVisitor::new();
let field_set =
tracing::field::FieldSet::new(&["ratio"], tracing::callsite::Identifier(&TestCallsite));
let field = field_set.field("ratio").unwrap();
visitor.record_f64(&field, 1.5);
assert_eq!(visitor.fields[0], ("ratio".to_string(), "1.5".to_string()));
}
#[test]
fn test_subscriber_captures_info_event() {
let console = Console::builder()
.width(120)
.no_color(true)
.record(true)
.markup(false)
.build();
let layer = GiltLayer::new()
.with_console(console)
.with_show_time(false)
.with_show_level(true)
.with_show_target(false)
.with_show_span_path(false);
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || {
tracing::info!("hello from tracing");
});
}
#[test]
fn test_subscriber_captures_all_levels() {
let console = Console::builder()
.width(120)
.no_color(true)
.record(true)
.markup(false)
.build();
let layer = GiltLayer::new()
.with_console(console)
.with_show_time(false)
.with_show_level(true)
.with_show_target(false)
.with_show_span_path(false);
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || {
tracing::error!("error msg");
tracing::warn!("warn msg");
tracing::info!("info msg");
tracing::debug!("debug msg");
tracing::trace!("trace msg");
});
}
#[test]
fn test_subscriber_with_structured_fields() {
let console = Console::builder()
.width(120)
.no_color(true)
.record(true)
.markup(false)
.build();
let layer = GiltLayer::new()
.with_console(console)
.with_show_time(false)
.with_show_level(false)
.with_show_target(false)
.with_show_span_path(false);
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || {
tracing::info!(user = "alice", status = 200u64, "request handled");
});
}
#[test]
fn test_subscriber_with_span_context() {
let console = Console::builder()
.width(120)
.no_color(true)
.record(true)
.markup(false)
.build();
let layer = GiltLayer::new()
.with_console(console)
.with_show_time(false)
.with_show_level(false)
.with_show_target(false)
.with_show_span_path(true);
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || {
let _guard = tracing::info_span!("my_span").entered();
tracing::info!("inside span");
});
}
#[test]
fn test_emit_full_line() {
let console = Console::builder()
.width(120)
.no_color(true)
.record(true)
.markup(false)
.build();
let layer = GiltLayer::new()
.with_console(console)
.with_show_time(true)
.with_show_level(true)
.with_show_target(true)
.with_show_span_path(true);
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || {
let _guard = tracing::info_span!("server").entered();
tracing::info!(port = 8080u64, "listening");
});
}
#[test]
fn test_install_returns_result() {
let _: fn() -> Result<(), Box<dyn std::error::Error + Send + Sync>> = install;
}
struct TestCallsite;
impl tracing::callsite::Callsite for TestCallsite {
fn set_interest(&self, _: tracing::subscriber::Interest) {}
fn metadata(&self) -> &tracing::Metadata<'_> {
static META: tracing::Metadata<'static> = tracing::Metadata::new(
"test",
"test",
Level::INFO,
None,
None,
None,
tracing::field::FieldSet::new(&[], tracing::callsite::Identifier(&TestCallsite2)),
tracing::metadata::Kind::EVENT,
);
&META
}
}
struct TestCallsite2;
impl tracing::callsite::Callsite for TestCallsite2 {
fn set_interest(&self, _: tracing::subscriber::Interest) {}
fn metadata(&self) -> &tracing::Metadata<'_> {
TestCallsite.metadata()
}
}
}