Skip to content

Commit 945a2bb

Browse files
ellttBenestk
andauthored
Add support for Key-Value pairs (#362)
* Add log::kv support * Update log version * Switch to a direct deserialize implementation for json kv attributes encoding. Factor pattern parsing code between MDC and key_value targets --------- Co-authored-by: estk <esims89@gmail.com>
1 parent 771a6f1 commit 945a2bb

File tree

3 files changed

+208
-49
lines changed

3 files changed

+208
-49
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ console_writer = ["ansi_writer", "libc", "winapi"]
4848
simple_writer = []
4949
threshold_filter = []
5050
background_rotation = []
51+
log_kv = ["log/kv"]
5152

5253
all_components = [
5354
"console_appender",
@@ -78,7 +79,7 @@ chrono = { version = "0.4.23", optional = true, features = [
7879
flate2 = { version = "1.0", optional = true }
7980
fnv = "1.0"
8081
humantime = { version = "2.1", optional = true }
81-
log = { version = "0.4.20", features = ["std"] }
82+
log = { version = "0.4.21", features = ["std"] }
8283
log-mdc = { version = "0.1", optional = true }
8384
serde = { version = "1.0.196", optional = true, features = ["derive"] }
8485
serde-value = { version = "0.7", optional = true }

src/encode/json.rs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
//! }
2525
//! }
2626
//! ```
27+
//! If the `log_kv` feature is enabled, an additional `attributes` field will
28+
//! contain a map of the record's [log::kv][log_kv] structured logging
29+
//! attributes.
30+
//!
31+
//! [log_kv]: https://docs.rs/log/latest/log/kv/index.html
2732
2833
use chrono::{
2934
format::{DelayedFormat, Fixed, Item},
@@ -76,6 +81,8 @@ impl JsonEncoder {
7681
thread: thread.name(),
7782
thread_id: thread_id::get(),
7883
mdc: Mdc,
84+
#[cfg(feature = "log_kv")]
85+
attributes: kv::Attributes(record.key_values()),
7986
};
8087
message.serialize(&mut serde_json::Serializer::new(&mut *w))?;
8188
w.write_all(NEWLINE.as_bytes())?;
@@ -106,6 +113,8 @@ struct Message<'a> {
106113
thread: Option<&'a str>,
107114
thread_id: usize,
108115
mdc: Mdc,
116+
#[cfg(feature = "log_kv")]
117+
attributes: kv::Attributes<'a>,
109118
}
110119

111120
fn ser_display<T, S>(v: &T, s: S) -> Result<S::Ok, S::Error>
@@ -162,6 +171,40 @@ impl Deserialize for JsonEncoderDeserializer {
162171
Ok(Box::<JsonEncoder>::default())
163172
}
164173
}
174+
#[cfg(feature = "log_kv")]
175+
mod kv {
176+
use log::kv::VisitSource;
177+
use serde::ser::{self, Error, SerializeMap};
178+
179+
pub(crate) struct Attributes<'a>(pub &'a dyn log::kv::Source);
180+
181+
pub(crate) struct SerializerVisitor<T: ser::SerializeMap>(pub T);
182+
183+
impl<'kvs, T: ser::SerializeMap> VisitSource<'kvs> for SerializerVisitor<T> {
184+
fn visit_pair(
185+
&mut self,
186+
key: log::kv::Key<'kvs>,
187+
value: log::kv::Value<'kvs>,
188+
) -> Result<(), log::kv::Error> {
189+
self.0
190+
.serialize_entry(key.as_str(), &value.to_string())
191+
.map_err(|e| log::kv::Error::boxed(e.to_string()))?;
192+
Ok(())
193+
}
194+
}
195+
196+
impl<'a> ser::Serialize for Attributes<'a> {
197+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
198+
where
199+
S: serde::Serializer,
200+
{
201+
let map = serializer.serialize_map(Some(self.0.count()))?;
202+
let mut visitor = SerializerVisitor(map);
203+
self.0.visit(&mut visitor).map_err(S::Error::custom)?;
204+
visitor.0.end()
205+
}
206+
}
207+
}
165208

166209
#[cfg(test)]
167210
#[cfg(feature = "simple_writer")]
@@ -189,26 +232,35 @@ mod test {
189232

190233
let encoder = JsonEncoder::new();
191234

235+
let mut record_builder = Record::builder();
236+
record_builder
237+
.level(level)
238+
.target(target)
239+
.module_path(Some(module_path))
240+
.file(Some(file))
241+
.line(Some(line));
242+
243+
#[cfg(feature = "log_kv")]
244+
record_builder.key_values(&[("log_foo", "log_bar")]);
245+
192246
let mut buf = vec![];
193247
encoder
194248
.encode_inner(
195249
&mut SimpleWriter(&mut buf),
196250
time,
197-
&Record::builder()
198-
.level(level)
199-
.target(target)
200-
.module_path(Some(module_path))
201-
.file(Some(file))
202-
.line(Some(line))
203-
.args(format_args!("{}", message))
204-
.build(),
251+
&record_builder.args(format_args!("{}", message)).build(),
205252
)
206253
.unwrap();
207254

255+
#[cfg(feature = "log_kv")]
256+
let expected_attributes = ",\"attributes\":{\"log_foo\":\"log_bar\"}";
257+
#[cfg(not(feature = "log_kv"))]
258+
let expected_attributes = "";
259+
208260
let expected = format!(
209261
"{{\"time\":\"{}\",\"level\":\"{}\",\"message\":\"{}\",\"module_path\":\"{}\",\
210262
\"file\":\"{}\",\"line\":{},\"target\":\"{}\",\
211-
\"thread\":\"{}\",\"thread_id\":{},\"mdc\":{{\"foo\":\"bar\"}}}}",
263+
\"thread\":\"{}\",\"thread_id\":{},\"mdc\":{{\"foo\":\"bar\"}}{}}}",
212264
time.to_rfc3339(),
213265
level,
214266
message,
@@ -218,6 +270,7 @@ mod test {
218270
target,
219271
thread,
220272
thread_id::get(),
273+
expected_attributes
221274
);
222275
assert_eq!(expected, String::from_utf8(buf).unwrap().trim());
223276
}

src/encode/pattern/mod.rs

Lines changed: 144 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@
7373
//! defaults to the empty string.
7474
//! * `{X(user_id)}` - `123e4567-e89b-12d3-a456-426655440000`
7575
//! * `{X(nonexistent_key)(no mapping)}` - `no mapping`
76+
//! * `K`, `key_value` - A value from a [log::kv][log_kv] structured logging
77+
//! record attributes. The first argument specifies the key, and the second
78+
//! argument specifies the default value if the key is not present in the
79+
//! log record's attributes. The second argument is optional, and defaults
80+
//! to the empty string. This formatter requires the `log_kv` feature to be
81+
//! enabled.
82+
//! * `{K(user_id)}` - `123e4567-e89b-12d3-a456-426655440000`
83+
//! * `{K(nonexistent_key)(no mapping)}` - `no mapping`
7684
//! * An "unnamed" formatter simply formats its argument, applying the format
7785
//! specification.
7886
//! * `{({l} {m})}` - `INFO hello`
@@ -120,6 +128,7 @@
120128
//! level `DEBUG` will be truncated to `DEBUG hello, wo`.
121129
//!
122130
//! [MDC]: https://crates.io/crates/log-mdc
131+
//! [log_kv]: https://docs.rs/log/latest/log/kv/index.html
123132
124133
use chrono::{Local, Utc};
125134
use derivative::Derivative;
@@ -135,6 +144,8 @@ use crate::encode::{
135144
#[cfg(feature = "config_parsing")]
136145
use crate::config::{Deserialize, Deserializers};
137146

147+
use self::parser::Formatter;
148+
138149
mod parser;
139150

140151
thread_local!(
@@ -496,46 +507,25 @@ impl<'a> From<Piece<'a>> for Chunk {
496507
"P" | "pid" => no_args(&formatter.args, parameters, FormattedChunk::ProcessId),
497508
"i" | "tid" => no_args(&formatter.args, parameters, FormattedChunk::SystemThreadId),
498509
"t" | "target" => no_args(&formatter.args, parameters, FormattedChunk::Target),
499-
"X" | "mdc" => {
500-
if formatter.args.len() > 2 {
501-
return Chunk::Error("expected at most two arguments".to_owned());
502-
}
503-
504-
let key = match formatter.args.first() {
505-
Some(arg) => {
506-
if let Some(arg) = arg.first() {
507-
match arg {
508-
Piece::Text(key) => key.to_owned(),
509-
Piece::Error(ref e) => return Chunk::Error(e.clone()),
510-
_ => return Chunk::Error("invalid MDC key".to_owned()),
511-
}
512-
} else {
513-
return Chunk::Error("invalid MDC key".to_owned());
514-
}
515-
}
516-
None => return Chunk::Error("missing MDC key".to_owned()),
517-
};
518-
519-
let default = match formatter.args.get(1) {
520-
Some(arg) => {
521-
if let Some(arg) = arg.first() {
522-
match arg {
523-
Piece::Text(key) => key.to_owned(),
524-
Piece::Error(ref e) => return Chunk::Error(e.clone()),
525-
_ => return Chunk::Error("invalid MDC default".to_owned()),
526-
}
527-
} else {
528-
return Chunk::Error("invalid MDC default".to_owned());
529-
}
530-
}
531-
None => "",
532-
};
533-
534-
Chunk::Formatted {
535-
chunk: FormattedChunk::Mdc(key.into(), default.into()),
510+
"X" | "mdc" => match kv_parsing(&formatter) {
511+
Err(e) => Chunk::Error(format!("MDC: {e}")),
512+
Ok((key, default)) => Chunk::Formatted {
513+
chunk: FormattedChunk::Mdc(key, default),
536514
params: parameters,
537-
}
538-
}
515+
},
516+
},
517+
#[cfg(feature = "log_kv")]
518+
"K" | "key_value" => match kv_parsing(&formatter) {
519+
Err(e) => Chunk::Error(format!("key_value: {e}")),
520+
Ok((key, default)) => Chunk::Formatted {
521+
chunk: FormattedChunk::Kv(key, default),
522+
params: parameters,
523+
},
524+
},
525+
#[cfg(not(feature = "log_kv"))]
526+
"K" | "key_value" => Chunk::Error(
527+
"The log_kv feature is required to parse the key_value argument".to_owned(),
528+
),
539529
"" => {
540530
if formatter.args.len() != 1 {
541531
return Chunk::Error("expected exactly one argument".to_owned());
@@ -568,6 +558,43 @@ fn no_args(arg: &[Vec<Piece>], params: Parameters, chunk: FormattedChunk) -> Chu
568558
}
569559
}
570560

561+
fn kv_parsing<'a>(formatter: &'a Formatter) -> Result<(String, String), &'a str> {
562+
if formatter.args.len() > 2 {
563+
return Err("expected at most two arguments");
564+
}
565+
566+
let key = match formatter.args.first() {
567+
Some(arg) => {
568+
if let Some(arg) = arg.first() {
569+
match arg {
570+
Piece::Text(key) => key.to_owned(),
571+
Piece::Error(ref e) => return Err(e),
572+
_ => return Err("invalid key"),
573+
}
574+
} else {
575+
return Err("invalid key");
576+
}
577+
}
578+
None => return Err("missing key"),
579+
};
580+
581+
let default = match formatter.args.get(1) {
582+
Some(arg) => {
583+
if let Some(arg) = arg.first() {
584+
match arg {
585+
Piece::Text(key) => key.to_owned(),
586+
Piece::Error(ref e) => return Err(e),
587+
_ => return Err("invalid default"),
588+
}
589+
} else {
590+
return Err("invalid default");
591+
}
592+
}
593+
None => "",
594+
};
595+
Ok((key.into(), default.into()))
596+
}
597+
571598
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
572599
enum Timezone {
573600
Utc,
@@ -593,6 +620,8 @@ enum FormattedChunk {
593620
Debug(Vec<Chunk>),
594621
Release(Vec<Chunk>),
595622
Mdc(String, String),
623+
#[cfg(feature = "log_kv")]
624+
Kv(String, String),
596625
}
597626

598627
impl FormattedChunk {
@@ -666,6 +695,15 @@ impl FormattedChunk {
666695
FormattedChunk::Mdc(ref key, ref default) => {
667696
log_mdc::get(key, |v| write!(w, "{}", v.unwrap_or(default)))
668697
}
698+
#[cfg(feature = "log_kv")]
699+
FormattedChunk::Kv(ref key, ref default) => {
700+
use log::kv::ToKey;
701+
if let Some(v) = record.key_values().get(key.to_key()) {
702+
write!(w, "{v}")
703+
} else {
704+
write!(w, "{default}")
705+
}
706+
}
669707
}
670708
}
671709
}
@@ -751,6 +789,8 @@ mod tests {
751789
#[cfg(feature = "simple_writer")]
752790
use std::thread;
753791

792+
#[cfg(feature = "log_kv")]
793+
use super::Parser;
754794
use super::{Chunk, PatternEncoder};
755795
#[cfg(feature = "simple_writer")]
756796
use crate::encode::writer::simple::SimpleWriter;
@@ -1031,6 +1071,71 @@ mod tests {
10311071
assert_eq!(buf, b"missing value");
10321072
}
10331073

1074+
#[test]
1075+
#[cfg(all(feature = "simple_writer", feature = "log_kv"))]
1076+
fn test_kv() {
1077+
let pw = PatternEncoder::new("{K(user_id)}");
1078+
let kv = [("user_id", "kv value")];
1079+
1080+
let mut buf = vec![];
1081+
pw.encode(
1082+
&mut SimpleWriter(&mut buf),
1083+
&Record::builder().key_values(&kv).build(),
1084+
)
1085+
.unwrap();
1086+
1087+
assert_eq!(buf, b"kv value");
1088+
}
1089+
1090+
#[test]
1091+
#[cfg(all(feature = "simple_writer", feature = "log_kv"))]
1092+
fn test_kv_missing_default() {
1093+
let pw = PatternEncoder::new("{K(user_id)}");
1094+
1095+
let mut buf = vec![];
1096+
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
1097+
.unwrap();
1098+
1099+
assert_eq!(buf, b"");
1100+
}
1101+
1102+
#[test]
1103+
#[cfg(all(feature = "simple_writer", feature = "log_kv"))]
1104+
fn test_kv_missing_custom() {
1105+
let pw = PatternEncoder::new("{K(user_id)(missing value)}");
1106+
1107+
let mut buf = vec![];
1108+
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
1109+
.unwrap();
1110+
1111+
assert_eq!(buf, b"missing value");
1112+
}
1113+
1114+
#[test]
1115+
#[cfg(feature = "log_kv")]
1116+
fn test_kv_from_piece_to_chunk() {
1117+
let tests = vec![
1118+
(
1119+
"[{K(user_id)(foobar)(test):<5.5}]",
1120+
"expected at most two arguments",
1121+
),
1122+
("[{K({l user_id):<5.5}]", "expected '}'"),
1123+
("[{K({l} user_id):<5.5}]", "key_value: invalid key"),
1124+
("[{K:<5.5}]", "key_value: missing key"),
1125+
("[{K(user_id)({l):<5.5}]", "expected '}'"),
1126+
("[{K(user_id)({l}):<5.5}]", "key_value: invalid default"),
1127+
("[{K(user_id)():<5.5} {M}]", "key_value: invalid default"),
1128+
];
1129+
1130+
for (pattern, error_msg) in tests {
1131+
let chunks: Vec<Chunk> = Parser::new(pattern).map(From::from).collect();
1132+
match chunks.get(1).unwrap() {
1133+
Chunk::Error(err) => assert!(err.contains(error_msg)),
1134+
_ => panic!(),
1135+
}
1136+
}
1137+
}
1138+
10341139
#[test]
10351140
#[cfg(feature = "simple_writer")]
10361141
fn debug_release() {

0 commit comments

Comments
 (0)