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_auto_cfg))]
18
19use std::fmt::Arguments;
20
21use jiff::Timestamp;
22use jiff::TimestampDisplayWithOffset;
23use jiff::tz::TimeZone;
24use logforth_core::Diagnostic;
25use logforth_core::Error;
26use logforth_core::kv::Key;
27use logforth_core::kv::Value;
28use logforth_core::kv::Visitor;
29use logforth_core::layout::Layout;
30use logforth_core::record::Record;
31use serde::Serialize;
32use serde_json::Map;
33
34/// A JSON layout for formatting log records.
35///
36/// Output format:
37///
38/// ```json
39/// {"timestamp":"2024-08-11T22:44:57.172051+08:00","level":"ERROR","module_path":"file","file":"examples/file.rs","line":51,"message":"Hello error!"}
40/// {"timestamp":"2024-08-11T22:44:57.172187+08:00","level":"WARN","module_path":"file","file":"examples/file.rs","line":52,"message":"Hello warn!"}
41/// {"timestamp":"2024-08-11T22:44:57.172246+08:00","level":"INFO","module_path":"file","file":"examples/file.rs","line":53,"message":"Hello info!"}
42/// {"timestamp":"2024-08-11T22:44:57.172300+08:00","level":"DEBUG","module_path":"file","file":"examples/file.rs","line":54,"message":"Hello debug!"}
43/// {"timestamp":"2024-08-11T22:44:57.172353+08:00","level":"TRACE","module_path":"file","file":"examples/file.rs","line":55,"message":"Hello trace!"}
44/// ```
45///
46/// # Examples
47///
48/// ```
49/// use logforth_layout_json::JsonLayout;
50///
51/// let json_layout = JsonLayout::default();
52/// ```
53#[derive(Default, Debug, Clone)]
54pub struct JsonLayout {
55    tz: Option<TimeZone>,
56}
57
58impl JsonLayout {
59    /// Set the timezone for timestamps.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use jiff::tz::TimeZone;
65    /// use logforth_layout_json::JsonLayout;
66    ///
67    /// let layout = JsonLayout::default().timezone(TimeZone::UTC);
68    /// ```
69    pub fn timezone(mut self, tz: TimeZone) -> Self {
70        self.tz = Some(tz);
71        self
72    }
73}
74
75struct KvCollector<'a> {
76    kvs: &'a mut Map<String, serde_json::Value>,
77}
78
79impl Visitor for KvCollector<'_> {
80    fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> {
81        let key = key.into_string();
82        match serde_json::to_value(&value) {
83            Ok(value) => self.kvs.insert(key, value),
84            Err(_) => self.kvs.insert(key, value.to_string().into()),
85        };
86        Ok(())
87    }
88}
89
90#[derive(Debug, Clone, Serialize)]
91struct RecordLine<'a> {
92    #[serde(serialize_with = "serialize_timestamp")]
93    timestamp: TimestampDisplayWithOffset,
94    level: &'a str,
95    target: &'a str,
96    file: &'a str,
97    line: u32,
98    #[serde(serialize_with = "serialize_args")]
99    message: &'a Arguments<'a>,
100    #[serde(skip_serializing_if = "Map::is_empty")]
101    kvs: Map<String, serde_json::Value>,
102    #[serde(skip_serializing_if = "Map::is_empty")]
103    diags: Map<String, serde_json::Value>,
104}
105
106fn serialize_timestamp<S>(
107    timestamp: &TimestampDisplayWithOffset,
108    serializer: S,
109) -> Result<S::Ok, S::Error>
110where
111    S: serde::Serializer,
112{
113    serializer.collect_str(&format_args!("{timestamp:.6}"))
114}
115
116fn serialize_args<S>(args: &Arguments, serializer: S) -> Result<S::Ok, S::Error>
117where
118    S: serde::Serializer,
119{
120    serializer.collect_str(args)
121}
122
123impl Layout for JsonLayout {
124    fn format(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<Vec<u8>, Error> {
125        let diagnostics = diags;
126
127        // SAFETY: jiff::Timestamp::try_from only fails if the time is out of range, which is
128        // very unlikely if the system clock is correct.
129        let ts = Timestamp::try_from(record.time()).unwrap();
130        let tz = self.tz.clone().unwrap_or_else(TimeZone::system);
131        let offset = tz.to_offset(ts);
132        let timestamp = ts.display_with_offset(offset);
133
134        let mut kvs = Map::new();
135        let mut kvs_visitor = KvCollector { kvs: &mut kvs };
136        record.key_values().visit(&mut kvs_visitor)?;
137
138        let mut diags = Map::new();
139        let mut diags_visitor = KvCollector { kvs: &mut diags };
140        for d in diagnostics {
141            d.visit(&mut diags_visitor)?;
142        }
143
144        let record_line = RecordLine {
145            timestamp,
146            level: record.level().as_str(),
147            target: record.target(),
148            file: record.file().unwrap_or_default(),
149            line: record.line().unwrap_or_default(),
150            message: record.args(),
151            kvs,
152            diags,
153        };
154
155        // SAFETY: RecordLine is serializable.
156        Ok(serde_json::to_vec(&record_line).unwrap())
157    }
158}