1
use std::io;
2
use std::io::Write;
3

            
4
use env_logger::fmt::Formatter;
5
use log::kv::Key;
6
use log::kv::Source;
7
use log::kv::Value;
8
use log::kv::VisitSource;
9

            
10
/// Formats key-value pairs from the given source as a JSON object.
11
///
12
/// # Details
13
///
14
/// Can be used to configure `env_logger` to format log key-values as JSON.
15
///
16
/// ```rust
17
/// use merc_tools::format_key_values_json;
18
///
19
/// env_logger::Builder::new()
20
///     .format_key_values(|formatter, source| {
21
///         Ok(format_key_values_json(formatter, source)?)
22
///     })
23
///     .init();
24
///
25
/// ```
26
pub fn format_key_values_json(formatter: &mut Formatter, source: &dyn Source) -> io::Result<()> {
27
    if source.count() > 0 {
28
        // If there are key-values, format them as a JSON object.
29
        formatter.write_all(b"\n{ ")?;
30
        let mut json_printer = JsonPrinter { formatter, first: true };
31
        if let Err(e) = source.visit(&mut json_printer) {
32
            return Err(io::Error::new(io::ErrorKind::InvalidData, e));
33
        }
34
        formatter.write_all(b" }")?;
35
    }
36

            
37
    Ok(())
38
}
39

            
40
/// A visitor that formats key-value pairs as JSON on a single line.
41
struct JsonPrinter<'a> {
42
    formatter: &'a mut Formatter,
43
    /// Whether the next pair is the first; used to place comma separators.
44
    first: bool,
45
}
46

            
47
impl<'a, 'kvs> VisitSource<'kvs> for JsonPrinter<'a> {
48
    fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), log::kv::Error> {
49
        write_json_pair(self.formatter, &mut self.first, key, value)?;
50
        Ok(())
51
    }
52
}
53

            
54
/// Writes a single `"key": value` pair as JSON, prefixing a `, ` separator for
55
/// every pair after the first (tracked through `first`).
56
///
57
/// Booleans and finite numbers are emitted as bare JSON tokens; everything else
58
/// (including strings, which `Value`'s `Display` renders unquoted) is written as
59
/// a quoted, escaped JSON string so the result is always valid JSON.
60
5
fn write_json_pair<W: Write>(writer: &mut W, first: &mut bool, key: Key<'_>, value: Value<'_>) -> io::Result<()> {
61
5
    if *first {
62
4
        *first = false;
63
4
    } else {
64
1
        writer.write_all(b", ")?;
65
    }
66

            
67
5
    write_json_string(writer, key.as_str())?;
68
5
    writer.write_all(b": ")?;
69

            
70
5
    if let Some(b) = value.to_bool() {
71
1
        write!(writer, "{b}")
72
4
    } else if let Some(i) = value.to_i64() {
73
3
        write!(writer, "{i}")
74
1
    } else if let Some(u) = value.to_u64() {
75
        write!(writer, "{u}")
76
1
    } else if let Some(f) = value.to_f64().filter(|f| f.is_finite()) {
77
        write!(writer, "{f}")
78
    } else {
79
1
        write_json_string(writer, &value.to_string())
80
    }
81
5
}
82

            
83
/// Writes `s` as a double-quoted, escaped JSON string.
84
7
fn write_json_string<W: Write>(writer: &mut W, s: &str) -> io::Result<()> {
85
7
    writer.write_all(b"\"")?;
86
19
    for c in s.chars() {
87
15
        match c {
88
1
            '"' => writer.write_all(b"\\\"")?,
89
1
            '\\' => writer.write_all(b"\\\\")?,
90
1
            '\n' => writer.write_all(b"\\n")?,
91
            '\r' => writer.write_all(b"\\r")?,
92
1
            '\t' => writer.write_all(b"\\t")?,
93
15
            c if (c as u32) < 0x20 => write!(writer, "\\u{:04x}", c as u32)?,
94
14
            c => write!(writer, "{c}")?,
95
        }
96
    }
97
7
    writer.write_all(b"\"")
98
7
}
99

            
100
#[cfg(test)]
101
mod tests {
102
    use log::kv::Key;
103
    use log::kv::Value;
104

            
105
    use crate::format_key_values::write_json_pair;
106
    use crate::format_key_values::write_json_string;
107

            
108
5
    fn render_pair(key: &str, value: Value<'_>, first: &mut bool) -> String {
109
5
        let mut buffer = Vec::new();
110
5
        write_json_pair(&mut buffer, first, Key::from_str(key), value).unwrap();
111
5
        String::from_utf8(buffer).unwrap()
112
5
    }
113

            
114
    #[test]
115
1
    fn test_separator_between_pairs() {
116
1
        let mut first = true;
117
1
        let a = render_pair("a", Value::from(1), &mut first);
118
1
        let b = render_pair("b", Value::from(2), &mut first);
119
1
        assert_eq!(a, r#""a": 1"#);
120
        // The second pair is prefixed with a comma separator.
121
1
        assert_eq!(b, r#", "b": 2"#);
122
1
    }
123

            
124
    #[test]
125
1
    fn test_string_values_are_quoted_and_escaped() {
126
1
        let mut first = true;
127
1
        let rendered = render_pair("path", Value::from("c:\\a\"b"), &mut first);
128
1
        assert_eq!(rendered, r#""path": "c:\\a\"b""#);
129
1
    }
130

            
131
    #[test]
132
1
    fn test_numbers_and_bools_are_bare() {
133
1
        let mut first = true;
134
1
        assert_eq!(render_pair("x", Value::from(true), &mut first), r#""x": true"#);
135
1
        let mut first = true;
136
1
        assert_eq!(render_pair("y", Value::from(-7i64), &mut first), r#""y": -7"#);
137
1
    }
138

            
139
    #[test]
140
1
    fn test_control_characters_escaped() {
141
1
        let mut buffer = Vec::new();
142
1
        write_json_string(&mut buffer, "a\nb\t\u{1}").unwrap();
143
1
        assert_eq!(String::from_utf8(buffer).unwrap(), "\"a\\nb\\t\\u0001\"");
144
1
    }
145
}