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 3083219
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 6 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" ] }
56 changes: 51 additions & 5 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<DFN = (), 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)>,
dynamics: DFN,
}

/// 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,23 @@ impl<SF> LogstashFormat<SF> {
}
}

pub fn with_dynamics<DFN2>(self, dynamics: DFN2) -> LogstashFormat<DFN2, 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,
dynamics,
}
}

/// Add a constant field to every event.
///
/// # Example
Expand All @@ -130,7 +148,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 +161,7 @@ impl<SF> LogstashFormat<SF> {
span_format,
span_fields: self.span_fields,
constants: self.constants,
dynamics: self.dynamics,
}
}
}
Expand All @@ -161,6 +180,7 @@ impl Default for LogstashFormat {
span_format: Default::default(),
span_fields: Default::default(),
constants: Default::default(),
dynamics: (),
}
}
}
Expand Down Expand Up @@ -227,9 +247,25 @@ where
}
}

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

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

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

Expand Down Expand Up @@ -300,6 +336,8 @@ where
field_visitor.serialize_field(key, value);
}

self.dynamics.add_fields(&mut field_visitor);

if let Some(filter) = self.display_span_list {
field_visitor.serialize_field(
"spans",
Expand All @@ -323,7 +361,11 @@ where
}
}

struct SerializingFieldVisitor<'a, F, S, E> {
pub trait FieldSerializer {
fn serialize_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 @@ -336,7 +378,11 @@ impl<'a, S: SerializeMap, F: FnMut(&'static str) -> bool>
fn record_field<V: ?Sized + Serialize>(&mut self, field: &Field, value: &V) {
self.serialize_field(field.name(), value)
}
}

impl<'a, S: SerializeMap, F: FnMut(&'static str) -> bool> FieldSerializer
for SerializingFieldVisitor<'a, F, S, S::Error>
{
fn serialize_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) {
Expand Down
67 changes: 66 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::{FieldSerializer, LogFieldContributor};
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,66 @@ 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: FieldSerializer,
{
serializer.serialize_field("dyn_string", "fnord");
serializer.serialize_field("dyn_string", "should_be_ignored");
serializer.serialize_field("dyn_int", &42);
serializer.serialize_field(
"dyn_obj",
&DynObj {
text: "text".to_string(),
},
);
}
}

let logger = tracing_logstash::Layer::default()
.event_format(
tracing_logstash::logstash::LogstashFormat::default().with_dynamics(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 3083219

Please sign in to comment.