logforth_layout_json/
lib.rs

1// Copyright 2024 FastLabs Developers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A JSON layout for formatting log records.
16
17#![cfg_attr(docsrs, feature(doc_cfg))]
18
19use jiff::Timestamp;
20use jiff::TimestampDisplayWithOffset;
21use jiff::tz::TimeZone;
22use logforth_core::Diagnostic;
23use logforth_core::Error;
24use logforth_core::kv::Key;
25use logforth_core::kv::Value;
26use logforth_core::kv::Visitor;
27use logforth_core::layout::Layout;
28use logforth_core::record::Record;
29use serde::Serialize;
30use serde_json::Map;
31
32/// A JSON layout for formatting log records.
33///
34/// Output format:
35///
36/// ```json
37/// {"timestamp":"2024-08-11T22:44:57.172051+08:00","level":"ERROR","module_path":"file","file":"examples/file.rs","line":51,"message":"Hello error!"}
38/// {"timestamp":"2024-08-11T22:44:57.172187+08:00","level":"WARN","module_path":"file","file":"examples/file.rs","line":52,"message":"Hello warn!"}
39/// {"timestamp":"2024-08-11T22:44:57.172246+08:00","level":"INFO","module_path":"file","file":"examples/file.rs","line":53,"message":"Hello info!"}
40/// {"timestamp":"2024-08-11T22:44:57.172300+08:00","level":"DEBUG","module_path":"file","file":"examples/file.rs","line":54,"message":"Hello debug!"}
41/// {"timestamp":"2024-08-11T22:44:57.172353+08:00","level":"TRACE","module_path":"file","file":"examples/file.rs","line":55,"message":"Hello trace!"}
42/// ```
43///
44/// # Examples
45///
46/// ```
47/// use logforth_layout_json::JsonLayout;
48///
49/// let json_layout = JsonLayout::default();
50/// ```
51#[derive(Default, Debug, Clone)]
52pub struct JsonLayout {
53    tz: Option<TimeZone>,
54}
55
56impl JsonLayout {
57    /// Set the timezone for timestamps.
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// use jiff::tz::TimeZone;
63    /// use logforth_layout_json::JsonLayout;
64    ///
65    /// let layout = JsonLayout::default().timezone(TimeZone::UTC);
66    /// ```
67    pub fn timezone(mut self, tz: TimeZone) -> Self {
68        self.tz = Some(tz);
69        self
70    }
71}
72
73struct KvCollector<'a> {
74    kvs: &'a mut Map<String, serde_json::Value>,
75}
76
77impl Visitor for KvCollector<'_> {
78    fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> {
79        let key = key.to_string();
80        match serde_json::to_value(&value) {
81            Ok(value) => self.kvs.insert(key, value),
82            Err(_) => self.kvs.insert(key, value.to_string().into()),
83        };
84        Ok(())
85    }
86}
87
88#[derive(Debug, Clone, Serialize)]
89struct RecordLine<'a> {
90    #[serde(serialize_with = "serialize_timestamp")]
91    timestamp: TimestampDisplayWithOffset,
92    level: &'a str,
93    target: &'a str,
94    file: &'a str,
95    line: u32,
96    message: &'a str,
97    #[serde(skip_serializing_if = "Map::is_empty")]
98    kvs: Map<String, serde_json::Value>,
99    #[serde(skip_serializing_if = "Map::is_empty")]
100    diags: Map<String, serde_json::Value>,
101}
102
103fn serialize_timestamp<S>(
104    timestamp: &TimestampDisplayWithOffset,
105    serializer: S,
106) -> Result<S::Ok, S::Error>
107where
108    S: serde::Serializer,
109{
110    serializer.collect_str(&format_args!("{timestamp:.6}"))
111}
112
113impl Layout for JsonLayout {
114    fn format(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<Vec<u8>, Error> {
115        let diagnostics = diags;
116
117        // SAFETY: jiff::Timestamp::try_from only fails if the time is out of range, which is
118        // very unlikely if the system clock is correct.
119        let ts = Timestamp::try_from(record.time()).unwrap();
120        let tz = self.tz.clone().unwrap_or_else(TimeZone::system);
121        let offset = tz.to_offset(ts);
122        let timestamp = ts.display_with_offset(offset);
123
124        let mut kvs = Map::new();
125        let mut kvs_visitor = KvCollector { kvs: &mut kvs };
126        record.key_values().visit(&mut kvs_visitor)?;
127
128        let mut diags = Map::new();
129        let mut diags_visitor = KvCollector { kvs: &mut diags };
130        for d in diagnostics {
131            d.visit(&mut diags_visitor)?;
132        }
133
134        let record_line = RecordLine {
135            timestamp,
136            level: record.level().as_str(),
137            target: record.target(),
138            file: record.file().unwrap_or_default(),
139            line: record.line().unwrap_or_default(),
140            message: record.payload(),
141            kvs,
142            diags,
143        };
144
145        // SAFETY: RecordLine is serializable.
146        Ok(serde_json::to_vec(&record_line).unwrap())
147    }
148}