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}