Skip to content

Commit

Permalink
Add LogFieldContributor trait for generated fields
Browse files Browse the repository at this point in the history
  • Loading branch information
gsson committed Jan 8, 2024
1 parent 5861fa7 commit 052e0e2
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 18 deletions.
1 change: 1 addition & 0 deletions tracing-logstash/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" ] }
105 changes: 88 additions & 17 deletions tracing-logstash/src/logstash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use tracing_subscriber::registry::LookupSpan;
/// #
/// # let collector = tracing_subscriber::Registry::default().with(logger);
/// ```
pub struct LogstashFormat<SF = DefaultSpanFormat> {
pub struct LogstashFormat<FC = (), SF = DefaultSpanFormat> {
display_version: bool,
display_timestamp: bool,
display_logger_name: Option<LoggerName>,
Expand All @@ -39,6 +39,7 @@ pub struct LogstashFormat<SF = DefaultSpanFormat> {
span_format: SF,
span_fields: Arc<FieldConfig>,
constants: Vec<(&'static str, String)>,
field_contributor: FC,
}

/// Converts a `Level` to a numeric value.
Expand All @@ -52,7 +53,7 @@ const fn level_value(level: &Level) -> u64 {
}
}

impl<SF> LogstashFormat<SF> {
impl<DFN, SF> LogstashFormat<DFN, SF> {
pub fn with_timestamp(self, display_timestamp: bool) -> Self {
Self {
display_timestamp,
Expand Down Expand Up @@ -112,6 +113,48 @@ impl<SF> LogstashFormat<SF> {
}
}

/// 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<F>(&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<FC2>(self, field_contributor: FC2) -> LogstashFormat<FC2, SF> {
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
Expand All @@ -130,7 +173,7 @@ impl<SF> LogstashFormat<SF> {
Self { constants, ..self }
}

pub fn span_format<FS2>(self, span_format: FS2) -> LogstashFormat<FS2> {
pub fn span_format<FS2>(self, span_format: FS2) -> LogstashFormat<DFN, FS2> {
LogstashFormat {
display_version: self.display_version,
display_timestamp: self.display_timestamp,
Expand All @@ -143,6 +186,7 @@ impl<SF> LogstashFormat<SF> {
span_format,
span_fields: self.span_fields,
constants: self.constants,
field_contributor: self.field_contributor,
}
}
}
Expand All @@ -161,6 +205,7 @@ impl Default for LogstashFormat {
span_format: Default::default(),
span_fields: Default::default(),
constants: Default::default(),
field_contributor: (),
}
}
}
Expand Down Expand Up @@ -227,9 +272,25 @@ where
}
}

impl<FS> FormatEvent for LogstashFormat<FS>
pub trait LogFieldContributor {
fn add_fields<F>(&self, serializer: &mut F)
where
F: LogFieldReceiver;
}

impl LogFieldContributor for () {
#[inline(always)]
fn add_fields<F>(&self, _serializer: &mut F)
where
F: LogFieldReceiver,
{
}
}

impl<DFN, FS> FormatEvent for LogstashFormat<DFN, FS>
where
FS: FormatSpan,
DFN: LogFieldContributor,
{
type R = DefaultSpanRecorder;

Expand Down Expand Up @@ -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),
);
Expand All @@ -323,7 +386,11 @@ where
}
}

struct SerializingFieldVisitor<'a, F, S, E> {
pub trait LogFieldReceiver {
fn add_field<V: ?Sized + Serialize>(&mut self, field: &'static str, value: &V);
}

pub struct SerializingFieldVisitor<'a, F, S, E> {
field_name_filter: F,
serializer: &'a mut S,
status: Option<E>,
Expand All @@ -334,10 +401,14 @@ impl<'a, S: SerializeMap, F: FnMut(&'static str) -> bool>
{
#[inline]
fn record_field<V: ?Sized + Serialize>(&mut self, field: &Field, value: &V) {
self.serialize_field(field.name(), value)
self.add_field(field.name(), value)
}
}

fn serialize_field<V: ?Sized + Serialize>(&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<V: ?Sized + Serialize>(&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)
Expand Down
68 changes: 67 additions & 1 deletion tracing-logstash/tests/output.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -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");

Expand All @@ -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<F>(&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();
}

0 comments on commit 052e0e2

Please sign in to comment.