Skip to content

Commit 563b452

Browse files
authored
Add the ability to skip fields (#25)
* all: run `cargo fmt` * add the ahash dependency * implement skipping fields in the formatter
1 parent a0ad0cc commit 563b452

File tree

3 files changed

+111
-23
lines changed

3 files changed

+111
-23
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ serde = "1.0.106"
3232
gethostname = "0.2.1"
3333
tracing-core = "0.1.10"
3434
time = { version = "0.3", default-features = false, features = ["formatting"] }
35+
ahash = "0.8.2"
3536

3637
[dev-dependencies]
3738
claims = "0.6.0"

src/formatting_layer.rs

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use crate::storage_layer::JsonStorage;
2-
use serde::ser::{SerializeMap, Serializer};
2+
use ahash::{HashSet, HashSetExt};
3+
use serde::ser::{Serialize, SerializeMap, Serializer};
34
use serde_json::Value;
45
use std::collections::HashMap;
56
use std::fmt;
67
use std::io::Write;
8+
use time::format_description::well_known::Rfc3339;
79
use tracing::{Event, Id, Subscriber};
810
use tracing_core::metadata::Level;
911
use tracing_core::span::Attributes;
@@ -12,7 +14,6 @@ use tracing_subscriber::fmt::MakeWriter;
1214
use tracing_subscriber::layer::Context;
1315
use tracing_subscriber::registry::SpanRef;
1416
use tracing_subscriber::Layer;
15-
use time::format_description::well_known::Rfc3339;
1617

1718
/// Keys for core fields of the Bunyan format (https://github.com/trentm/node-bunyan#core-fields)
1819
const BUNYAN_VERSION: &str = "v";
@@ -48,8 +49,26 @@ pub struct BunyanFormattingLayer<W: for<'a> MakeWriter<'a> + 'static> {
4849
bunyan_version: u8,
4950
name: String,
5051
default_fields: HashMap<String, Value>,
52+
skip_fields: HashSet<String>,
53+
}
54+
55+
/// This error will be returned in [`BunyanFormattingLayer::skip_fields`] if trying to skip a core field.
56+
#[non_exhaustive]
57+
#[derive(Debug)]
58+
pub struct InvalidFieldError(String);
59+
60+
impl fmt::Display for InvalidFieldError {
61+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62+
write!(
63+
f,
64+
"{} is a core field in the bunyan log format, it can't be skipped",
65+
&self.0
66+
)
67+
}
5168
}
5269

70+
impl std::error::Error for InvalidFieldError {}
71+
5372
impl<W: for<'a> MakeWriter<'a> + 'static> BunyanFormattingLayer<W> {
5473
/// Create a new `BunyanFormattingLayer`.
5574
///
@@ -74,15 +93,36 @@ impl<W: for<'a> MakeWriter<'a> + 'static> BunyanFormattingLayer<W> {
7493
Self::with_default_fields(name, make_writer, HashMap::new())
7594
}
7695

77-
pub fn with_default_fields(name: String, make_writer: W, default_fields: HashMap<String, Value>) -> Self {
96+
pub fn with_default_fields(
97+
name: String,
98+
make_writer: W,
99+
default_fields: HashMap<String, Value>,
100+
) -> Self {
78101
Self {
79102
make_writer,
80103
name,
81104
pid: std::process::id(),
82105
hostname: gethostname::gethostname().to_string_lossy().into_owned(),
83106
bunyan_version: 0,
84107
default_fields,
108+
skip_fields: HashSet::new(),
109+
}
110+
}
111+
112+
/// Add fields to skip when formatting with this layer.
113+
pub fn skip_fields<T>(mut self, fields: &[T]) -> Result<Self, InvalidFieldError>
114+
where
115+
T: AsRef<str>,
116+
{
117+
for field in fields {
118+
let field = field.as_ref();
119+
if BUNYAN_RESERVED_FIELDS.contains(&field) {
120+
return Err(InvalidFieldError(field.to_string()));
121+
}
122+
self.skip_fields.insert(field.to_string());
85123
}
124+
125+
Ok(self)
86126
}
87127

88128
fn serialize_bunyan_core_fields(
@@ -103,6 +143,22 @@ impl<W: for<'a> MakeWriter<'a> + 'static> BunyanFormattingLayer<W> {
103143
Ok(())
104144
}
105145

146+
fn serialize_field<V>(
147+
&self,
148+
map_serializer: &mut impl SerializeMap<Error = serde_json::Error>,
149+
key: &str,
150+
value: &V,
151+
) -> Result<(), std::io::Error>
152+
where
153+
V: Serialize + ?Sized,
154+
{
155+
if !self.skip_fields.contains(key) {
156+
map_serializer.serialize_entry(key, value)?;
157+
}
158+
159+
Ok(())
160+
}
161+
106162
/// Given a span, it serialised it to a in-memory buffer (vector of bytes).
107163
fn serialize_span<S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>>(
108164
&self,
@@ -117,27 +173,27 @@ impl<W: for<'a> MakeWriter<'a> + 'static> BunyanFormattingLayer<W> {
117173
// Additional metadata useful for debugging
118174
// They should be nested under `src` (see https://github.com/trentm/node-bunyan#src )
119175
// but `tracing` does not support nested values yet
120-
map_serializer.serialize_entry("target", span.metadata().target())?;
121-
map_serializer.serialize_entry("line", &span.metadata().line())?;
122-
map_serializer.serialize_entry("file", &span.metadata().file())?;
176+
self.serialize_field(&mut map_serializer, "target", span.metadata().target())?;
177+
self.serialize_field(&mut map_serializer, "line", &span.metadata().line())?;
178+
self.serialize_field(&mut map_serializer, "file", &span.metadata().file())?;
123179

124180
// Add all default fields
125181
for (key, value) in self.default_fields.iter() {
126182
if !BUNYAN_RESERVED_FIELDS.contains(&key.as_str()) {
127-
map_serializer.serialize_entry(key, value)?;
183+
self.serialize_field(&mut map_serializer, key, value)?;
128184
} else {
129185
tracing::debug!(
130-
"{} is a reserved field in the bunyan log format. Skipping it.",
131-
key
132-
);
186+
"{} is a reserved field in the bunyan log format. Skipping it.",
187+
key
188+
);
133189
}
134190
}
135191

136192
let extensions = span.extensions();
137193
if let Some(visitor) = extensions.get::<JsonStorage>() {
138194
for (key, value) in visitor.values() {
139195
if !BUNYAN_RESERVED_FIELDS.contains(key) {
140-
map_serializer.serialize_entry(key, value)?;
196+
self.serialize_field(&mut map_serializer, key, value)?;
141197
} else {
142198
tracing::debug!(
143199
"{} is a reserved field in the bunyan log format. Skipping it.",
@@ -252,16 +308,15 @@ where
252308
// Additional metadata useful for debugging
253309
// They should be nested under `src` (see https://github.com/trentm/node-bunyan#src )
254310
// but `tracing` does not support nested values yet
255-
map_serializer.serialize_entry("target", event.metadata().target())?;
256-
map_serializer.serialize_entry("line", &event.metadata().line())?;
257-
map_serializer.serialize_entry("file", &event.metadata().file())?;
311+
self.serialize_field(&mut map_serializer, "target", event.metadata().target())?;
312+
self.serialize_field(&mut map_serializer, "line", &event.metadata().line())?;
313+
self.serialize_field(&mut map_serializer, "file", &event.metadata().file())?;
258314

259315
// Add all default fields
260-
for (key, value) in self.default_fields
261-
.iter()
262-
.filter(|(key, _)| key.as_str() != "message" && !BUNYAN_RESERVED_FIELDS.contains(&key.as_str()))
263-
{
264-
map_serializer.serialize_entry(key, value)?;
316+
for (key, value) in self.default_fields.iter().filter(|(key, _)| {
317+
key.as_str() != "message" && !BUNYAN_RESERVED_FIELDS.contains(&key.as_str())
318+
}) {
319+
self.serialize_field(&mut map_serializer, key, value)?;
265320
}
266321

267322
// Add all the other fields associated with the event, expect the message we already used.
@@ -270,7 +325,7 @@ where
270325
.iter()
271326
.filter(|(&key, _)| key != "message" && !BUNYAN_RESERVED_FIELDS.contains(&key))
272327
{
273-
map_serializer.serialize_entry(key, value)?;
328+
self.serialize_field(&mut map_serializer, key, value)?;
274329
}
275330

276331
// Add all the fields from the current span, if we have one.
@@ -279,7 +334,7 @@ where
279334
if let Some(visitor) = extensions.get::<JsonStorage>() {
280335
for (key, value) in visitor.values() {
281336
if !BUNYAN_RESERVED_FIELDS.contains(key) {
282-
map_serializer.serialize_entry(key, value)?;
337+
self.serialize_field(&mut map_serializer, key, value)?;
283338
} else {
284339
tracing::debug!(
285340
"{} is a reserved field in the bunyan log format. Skipping it.",

tests/e2e.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ lazy_static! {
2323
fn run_and_get_raw_output<F: Fn()>(action: F) -> String {
2424
let mut default_fields = HashMap::new();
2525
default_fields.insert("custom_field".to_string(), json!("custom_value"));
26-
let formatting_layer = BunyanFormattingLayer::with_default_fields("test".into(), || MockWriter::new(&BUFFER), default_fields);
26+
let formatting_layer = BunyanFormattingLayer::with_default_fields(
27+
"test".into(),
28+
|| MockWriter::new(&BUFFER),
29+
default_fields,
30+
)
31+
.skip_fields(&["skipped"])
32+
.unwrap();
2733
let subscriber = Registry::default()
2834
.with(JsonStorageLayer)
2935
.with(formatting_layer);
@@ -56,7 +62,8 @@ fn test_action() {
5662

5763
info!("pre-shaving yaks");
5864
let b = 3;
59-
let new_span = span!(Level::DEBUG, "inner shaving", b);
65+
let skipped = false;
66+
let new_span = span!(Level::DEBUG, "inner shaving", b, skipped);
6067
let _enter2 = new_span.enter();
6168

6269
info!("shaving yaks");
@@ -160,3 +167,28 @@ fn elapsed_milliseconds_are_present_on_exit_span() {
160167
}
161168
}
162169
}
170+
171+
#[test]
172+
fn skip_fields() {
173+
let tracing_output = run_and_get_output(test_action);
174+
175+
for record in tracing_output {
176+
assert!(record.get("skipped").is_none());
177+
}
178+
}
179+
180+
#[test]
181+
fn skipping_core_fields_is_not_allowed() {
182+
let result = BunyanFormattingLayer::new("test".into(), || MockWriter::new(&BUFFER))
183+
.skip_fields(&["level"]);
184+
185+
match result {
186+
Err(err) => {
187+
assert_eq!(
188+
"level is a core field in the bunyan log format, it can't be skipped",
189+
err.to_string()
190+
);
191+
}
192+
Ok(_) => panic!("skipping core fields shouldn't work"),
193+
}
194+
}

0 commit comments

Comments
 (0)