Skip to main content

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#![deny(missing_docs)]
19
20pub extern crate jiff;
21
22use jiff::Timestamp;
23use jiff::tz::TimeZone;
24use logforth_core::Diagnostic;
25use logforth_core::Error;
26use logforth_core::kv::KeyView;
27use logforth_core::kv::ValueView;
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(Debug, Clone)]
54pub struct JsonLayout {
55    timezone: TimeZone,
56    timestamp_format: Option<fn(Timestamp, &TimeZone) -> String>,
57}
58
59impl Default for JsonLayout {
60    fn default() -> Self {
61        Self {
62            timezone: TimeZone::system(),
63            timestamp_format: None,
64        }
65    }
66}
67
68impl JsonLayout {
69    /// Set the timezone for timestamps.
70    ///
71    /// Defaults to the system timezone.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use jiff::tz::TimeZone;
77    /// use logforth_layout_json::JsonLayout;
78    ///
79    /// let layout = JsonLayout::default().timezone(TimeZone::UTC);
80    /// ```
81    pub fn timezone(mut self, tz: TimeZone) -> Self {
82        self.timezone = tz;
83        self
84    }
85
86    /// Set a user-defined timestamp format function.
87    ///
88    /// Default to formatting the timestamp with offset as ISO 8601. See the example below.
89    ///
90    /// For other formatting options, refer to the [jiff::fmt::strtime] documentation.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use jiff::Timestamp;
96    /// use jiff::tz::TimeZone;
97    /// use logforth_layout_json::JsonLayout;
98    ///
99    /// // This is equivalent to the default timestamp format.
100    /// let layout = JsonLayout::default()
101    ///     .timestamp_format(|ts, tz| format!("{:.6}", ts.display_with_offset(tz.to_offset(ts))));
102    /// ```
103    pub fn timestamp_format(mut self, format: fn(Timestamp, &TimeZone) -> String) -> Self {
104        self.timestamp_format = Some(format);
105        self
106    }
107}
108
109struct KvCollector<'a> {
110    kvs: &'a mut Map<String, serde_json::Value>,
111}
112
113impl Visitor for KvCollector<'_> {
114    fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> {
115        let key = key.to_string();
116        match serde_json::to_value(&value) {
117            Ok(value) => self.kvs.insert(key, value),
118            Err(_) => self.kvs.insert(key, value.to_string().into()),
119        };
120        Ok(())
121    }
122}
123
124fn default_timestamp_format(ts: Timestamp, tz: &TimeZone) -> String {
125    let offset = tz.to_offset(ts);
126    format!("{:.6}", ts.display_with_offset(offset))
127}
128
129#[derive(Debug, Clone, Serialize)]
130struct RecordLine<'a> {
131    timestamp: String,
132    level: &'a str,
133    target: &'a str,
134    file: &'a str,
135    line: u32,
136    message: std::fmt::Arguments<'a>,
137    #[serde(skip_serializing_if = "Map::is_empty")]
138    kvs: Map<String, serde_json::Value>,
139    #[serde(skip_serializing_if = "Map::is_empty")]
140    diags: Map<String, serde_json::Value>,
141}
142
143impl Layout for JsonLayout {
144    fn format(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<Vec<u8>, Error> {
145        let diagnostics = diags;
146
147        // SAFETY: jiff::Timestamp::try_from only fails if the time is out of range, which is
148        // very unlikely if the system clock is correct.
149        let ts = Timestamp::try_from(record.time()).unwrap();
150        let timestamp = if let Some(format) = self.timestamp_format {
151            format(ts, &self.timezone)
152        } else {
153            default_timestamp_format(ts, &self.timezone)
154        };
155
156        let mut kvs = Map::new();
157        let mut kvs_visitor = KvCollector { kvs: &mut kvs };
158        record.key_values().visit(&mut kvs_visitor)?;
159
160        let mut diags = Map::new();
161        let mut diags_visitor = KvCollector { kvs: &mut diags };
162        for d in diagnostics {
163            d.visit(&mut diags_visitor)?;
164        }
165
166        let record_line = RecordLine {
167            timestamp,
168            level: record.level().name(),
169            target: record.target(),
170            file: record.file().unwrap_or_default(),
171            line: record.line().unwrap_or_default(),
172            message: record.payload(),
173            kvs,
174            diags,
175        };
176
177        // SAFETY: RecordLine is serializable.
178        Ok(serde_json::to_vec(&record_line).unwrap())
179    }
180}