diff --git a/tracing-logstash/Cargo.toml b/tracing-logstash/Cargo.toml index 1ffd87e..c0589d1 100644 --- a/tracing-logstash/Cargo.toml +++ b/tracing-logstash/Cargo.toml @@ -17,5 +17,6 @@ serde_json = "1" time = { version = "0.3", default-features = false, features = [ "std", "formatting" ] } [dev-dependencies] +serde = { version = "1", features = [ "derive" ] } tracing = { version = "0" } time = { version = "0.3", features = [ "macros", "parsing" ] } diff --git a/tracing-logstash/src/logstash.rs b/tracing-logstash/src/logstash.rs index af1edab..ef70fa6 100644 --- a/tracing-logstash/src/logstash.rs +++ b/tracing-logstash/src/logstash.rs @@ -27,7 +27,7 @@ use tracing_subscriber::registry::LookupSpan; /// # /// # let collector = tracing_subscriber::Registry::default().with(logger); /// ``` -pub struct LogstashFormat { +pub struct LogstashFormat { display_version: bool, display_timestamp: bool, display_logger_name: Option, @@ -39,6 +39,7 @@ pub struct LogstashFormat { span_format: SF, span_fields: Arc, constants: Vec<(&'static str, String)>, + field_contributor: FC, } /// Converts a `Level` to a numeric value. @@ -52,7 +53,7 @@ const fn level_value(level: &Level) -> u64 { } } -impl LogstashFormat { +impl LogstashFormat { pub fn with_timestamp(self, display_timestamp: bool) -> Self { Self { display_timestamp, @@ -112,6 +113,48 @@ impl LogstashFormat { } } + /// Add dynamically generated fields to every event + /// + /// # Example + /// ``` + /// # use tracing_subscriber::prelude::*; + /// # use tracing_logstash::logstash::{LogFieldReceiver, LogFieldContributor}; + /// # + /// struct DynamicFields; + /// impl LogFieldContributor for DynamicFields { + /// fn add_fields(&self, serializer: &mut F) + /// where + /// F: LogFieldReceiver, + /// { + /// serializer.add_field("string_field", "fnord"); + /// serializer.add_field("number_field", &42); + /// } + /// } + /// + /// let logger = tracing_logstash::Layer::default().event_format( + /// tracing_logstash::logstash::LogstashFormat::default() + /// .with_field_contributor(DynamicFields), + /// ); + /// # + /// # let collector = tracing_subscriber::Registry::default().with(logger); + /// ``` + pub fn with_field_contributor(self, field_contributor: FC2) -> LogstashFormat { + LogstashFormat { + display_version: self.display_version, + display_timestamp: self.display_timestamp, + display_logger_name: self.display_logger_name, + display_thread_name: self.display_thread_name, + display_level: self.display_level, + display_stack_trace: self.display_stack_trace, + display_level_value: self.display_level_value, + display_span_list: self.display_span_list, + span_format: self.span_format, + span_fields: self.span_fields, + constants: self.constants, + field_contributor, + } + } + /// Add a constant field to every event. /// /// # Example @@ -130,7 +173,7 @@ impl LogstashFormat { Self { constants, ..self } } - pub fn span_format(self, span_format: FS2) -> LogstashFormat { + pub fn span_format(self, span_format: FS2) -> LogstashFormat { LogstashFormat { display_version: self.display_version, display_timestamp: self.display_timestamp, @@ -143,6 +186,7 @@ impl LogstashFormat { span_format, span_fields: self.span_fields, constants: self.constants, + field_contributor: self.field_contributor, } } } @@ -161,6 +205,7 @@ impl Default for LogstashFormat { span_format: Default::default(), span_fields: Default::default(), constants: Default::default(), + field_contributor: (), } } } @@ -227,9 +272,25 @@ where } } -impl FormatEvent for LogstashFormat +pub trait LogFieldContributor { + fn add_fields(&self, serializer: &mut F) + where + F: LogFieldReceiver; +} + +impl LogFieldContributor for () { + #[inline(always)] + fn add_fields(&self, _serializer: &mut F) + where + F: LogFieldReceiver, + { + } +} + +impl FormatEvent for LogstashFormat where FS: FormatSpan, + DFN: LogFieldContributor, { type R = DefaultSpanRecorder; @@ -257,51 +318,53 @@ where }; if self.display_version { - field_visitor.serialize_field("@version", "1"); + field_visitor.add_field("@version", "1"); } if self.display_timestamp { - field_visitor.serialize_field("@timestamp", &LogTimestamp::default()); + field_visitor.add_field("@timestamp", &LogTimestamp::default()); } if self.display_thread_name { let thread = std::thread::current(); if let Some(name) = thread.name() { - field_visitor.serialize_field("thread_name", name); + field_visitor.add_field("thread_name", name); } } if let Some(l) = self.display_logger_name { match l { LoggerName::Event => { - field_visitor.serialize_field("logger_name", event_metadata.target()) + field_visitor.add_field("logger_name", event_metadata.target()) } LoggerName::Span => { - field_visitor.serialize_field("logger_name", &SerializeSpanName(event, &ctx)) + field_visitor.add_field("logger_name", &SerializeSpanName(event, &ctx)) } }; } if self.display_level { - field_visitor.serialize_field("level", event_level.as_str()); + field_visitor.add_field("level", event_level.as_str()); } if self.display_level_value { - field_visitor.serialize_field("level_value", &level_value(event_level)); + field_visitor.add_field("level_value", &level_value(event_level)); } if let Some((event_filter, span_filter)) = self.display_stack_trace { if let Some(stack_trace) = format_stack_trace(event, &ctx, event_filter, span_filter) { - field_visitor.serialize_field("stack_trace", &stack_trace); + field_visitor.add_field("stack_trace", &stack_trace); } } for (key, value) in &self.constants { - field_visitor.serialize_field(key, value); + field_visitor.add_field(key, value); } + self.field_contributor.add_fields(&mut field_visitor); + if let Some(filter) = self.display_span_list { - field_visitor.serialize_field( + field_visitor.add_field( "spans", &SerializableSpanList(&self.span_format, event, &ctx, filter), ); @@ -323,7 +386,11 @@ where } } -struct SerializingFieldVisitor<'a, F, S, E> { +pub trait LogFieldReceiver { + fn add_field(&mut self, field: &'static str, value: &V); +} + +pub struct SerializingFieldVisitor<'a, F, S, E> { field_name_filter: F, serializer: &'a mut S, status: Option, @@ -334,10 +401,14 @@ impl<'a, S: SerializeMap, F: FnMut(&'static str) -> bool> { #[inline] fn record_field(&mut self, field: &Field, value: &V) { - self.serialize_field(field.name(), value) + self.add_field(field.name(), value) } +} - fn serialize_field(&mut self, field: &'static str, value: &V) { +impl<'a, S: SerializeMap, F: FnMut(&'static str) -> bool> LogFieldReceiver + for SerializingFieldVisitor<'a, F, S, S::Error> +{ + fn add_field(&mut self, field: &'static str, value: &V) { if self.status.is_none() && (self.field_name_filter)(field) { if let Err(e) = self.serializer.serialize_entry(field, &value) { self.status = Some(e) diff --git a/tracing-logstash/tests/output.rs b/tracing-logstash/tests/output.rs index 30ab33d..1085dd9 100644 --- a/tracing-logstash/tests/output.rs +++ b/tracing-logstash/tests/output.rs @@ -1,8 +1,10 @@ +use serde::Serialize; use std::{ io::{self, Read, Write}, sync::{Arc, RwLock}, }; use time::format_description::well_known::Rfc3339; +use tracing_logstash::logstash::{LogFieldContributor, LogFieldReceiver}; use tracing_subscriber::{ fmt::writer::BoxMakeWriter, prelude::__tracing_subscriber_SubscriberExt, Registry, }; @@ -55,7 +57,7 @@ fn simple_log_format() { let collector = Registry::default().with(logger); - tracing::subscriber::set_global_default(collector).unwrap(); + let _guard = tracing::subscriber::set_default(collector); tracing::info!("test"); @@ -79,3 +81,67 @@ fn simple_log_format() { // assert that output_json["@timestamp"] is a valid timestamp time::OffsetDateTime::parse(output_json["@timestamp"].as_str().unwrap(), &Rfc3339).unwrap(); } + +#[test] +fn simple_log_format_with_dynamic_fields() { + let shared = Arc::new(RwLock::new(Vec::new())); + let cloned = shared.clone(); + let writer = BoxMakeWriter::new(move || Buffer::new(cloned.clone())); + + #[derive(Serialize)] + struct DynObj { + text: String, + } + + struct DynamicFields; + impl LogFieldContributor for DynamicFields { + fn add_fields(&self, serializer: &mut F) + where + F: LogFieldReceiver, + { + serializer.add_field("dyn_string", "fnord"); + serializer.add_field("dyn_string", "should_be_ignored"); + serializer.add_field("dyn_int", &42); + serializer.add_field( + "dyn_obj", + &DynObj { + text: "text".to_string(), + }, + ); + } + } + + let logger = tracing_logstash::Layer::default() + .event_format( + tracing_logstash::logstash::LogstashFormat::default() + .with_field_contributor(DynamicFields), + ) + .with_writer(writer); + + let collector = Registry::default().with(logger); + + let _guard = tracing::subscriber::set_default(collector); + + tracing::info!("test"); + + let output = String::from_utf8(shared.read().unwrap().to_vec()).unwrap(); + let output_json: serde_json::Value = serde_json::from_str(&output).unwrap(); + + let expected_json = serde_json::json!({ + "@version": "1", + "@timestamp": output_json["@timestamp"], + "thread_name": "simple_log_format_with_dynamic_fields", + "logger_name": "output", + "level": "INFO", + "level_value": 5, + "dyn_string": "fnord", + "dyn_int": 42, + "dyn_obj": { "text": "text" }, + "message": "test", + }); + + assert_eq!(output_json, expected_json); + + // assert that output_json["@timestamp"] is a valid timestamp + time::OffsetDateTime::parse(output_json["@timestamp"].as_str().unwrap(), &Rfc3339).unwrap(); +}