logforth_layout_text/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 layout that formats log record as optionally colored text.
16
17#![cfg_attr(docsrs, feature(doc_cfg))]
18#![deny(missing_docs)]
19
20pub extern crate colored;
21pub extern crate jiff;
22
23use std::fmt::Write;
24
25use colored::Color;
26use colored::ColoredString;
27use colored::Colorize;
28use jiff::Timestamp;
29use jiff::tz::TimeZone;
30use logforth_core::Diagnostic;
31use logforth_core::Error;
32use logforth_core::kv::KeyView;
33use logforth_core::kv::ValueView;
34use logforth_core::kv::Visitor;
35use logforth_core::layout::Layout;
36use logforth_core::record::Level;
37use logforth_core::record::Record;
38
39/// A layout that formats log record as optionally colored text.
40///
41/// Output format:
42///
43/// ```text
44/// 2024-08-11T22:44:57.172105+08:00 ERROR file: examples/file.rs:51 Hello error!
45/// 2024-08-11T22:44:57.172219+08:00 WARN file: examples/file.rs:52 Hello warn!
46/// 2024-08-11T22:44:57.172276+08:00 INFO file: examples/file.rs:53 Hello info!
47/// 2024-08-11T22:44:57.172329+08:00 DEBUG file: examples/file.rs:54 Hello debug!
48/// 2024-08-11T22:44:57.172382+08:00 TRACE file: examples/file.rs:55 Hello trace!
49/// ```
50///
51/// By default, log levels are colored. You can set the `no_color` field to `true` to disable
52/// coloring.
53///
54/// You can also customize the color of each log level with [`error_color`](TextLayout::error_color)
55/// and so on.
56///
57/// You can customize the timezone of the timestamp by setting the `tz` field with a [`TimeZone`]
58/// instance. Otherwise, the system timezone is used.
59///
60/// # Examples
61///
62/// ```
63/// use logforth_layout_text::TextLayout;
64///
65/// let layout = TextLayout::default();
66/// ```
67#[derive(Debug, Clone)]
68pub struct TextLayout {
69 colors: LevelColor,
70 no_color: bool,
71 timezone: TimeZone,
72 timestamp_format: Option<fn(Timestamp, &TimeZone) -> String>,
73}
74
75impl Default for TextLayout {
76 fn default() -> Self {
77 Self {
78 colors: LevelColor::default(),
79 no_color: false,
80 timezone: TimeZone::system(),
81 timestamp_format: None,
82 }
83 }
84}
85
86impl TextLayout {
87 /// Customize the color of the error log level. Default to bright red.
88 ///
89 /// No effect if `no_color` is set to `true`.
90 pub fn fatal_color(mut self, color: Color) -> Self {
91 self.colors.fatal = color;
92 self
93 }
94
95 /// Customize the color of the error log level. Default to red.
96 ///
97 /// No effect if `no_color` is set to `true`.
98 pub fn error_color(mut self, color: Color) -> Self {
99 self.colors.error = color;
100 self
101 }
102
103 /// Customize the color of the warn log level. Default to yellow.
104 ///
105 /// No effect if `no_color` is set to `true`.
106 pub fn warn_color(mut self, color: Color) -> Self {
107 self.colors.warn = color;
108 self
109 }
110
111 /// Customize the color of the info log level/ Default to green.
112 ///
113 /// No effect if `no_color` is set to `true`.
114 pub fn info_color(mut self, color: Color) -> Self {
115 self.colors.info = color;
116 self
117 }
118
119 /// Customize the color of the debug log level. Default to blue.
120 ///
121 /// No effect if `no_color` is set to `true`.
122 pub fn debug_color(mut self, color: Color) -> Self {
123 self.colors.debug = color;
124 self
125 }
126
127 /// Customize the color of the trace log level. Default to magenta.
128 ///
129 /// No effect if `no_color` is set to `true`.
130 pub fn trace_color(mut self, color: Color) -> Self {
131 self.colors.trace = color;
132 self
133 }
134
135 /// Disable colored output.
136 pub fn no_color(mut self) -> Self {
137 self.no_color = true;
138 self
139 }
140
141 /// Set the timezone for timestamps.
142 ///
143 /// Defaults to the system timezone if not set.
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// use jiff::tz::TimeZone;
149 /// use logforth_layout_text::TextLayout;
150 ///
151 /// let layout = TextLayout::default().timezone(TimeZone::UTC);
152 /// ```
153 pub fn timezone(mut self, tz: TimeZone) -> Self {
154 self.timezone = tz;
155 self
156 }
157
158 /// Set a user-defined timestamp format function.
159 ///
160 /// Default to formatting the timestamp with offset as ISO 8601. See the example below.
161 ///
162 /// For other formatting options, refer to the [jiff::fmt::strtime] documentation.
163 ///
164 /// # Examples
165 ///
166 /// ```
167 /// use jiff::Timestamp;
168 /// use jiff::tz::TimeZone;
169 /// use logforth_layout_text::TextLayout;
170 ///
171 /// // This is equivalent to the default timestamp format.
172 /// let layout = TextLayout::default()
173 /// .timestamp_format(|ts, tz| format!("{:.6}", ts.display_with_offset(tz.to_offset(ts))));
174 /// ```
175 pub fn timestamp_format(mut self, format: fn(Timestamp, &TimeZone) -> String) -> Self {
176 self.timestamp_format = Some(format);
177 self
178 }
179
180 fn format_record_level(&self, level: Level) -> ColoredString {
181 self.colors.colorize_record_level(self.no_color, level)
182 }
183}
184
185struct KvWriter {
186 text: String,
187}
188
189impl Visitor for KvWriter {
190 fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> {
191 use std::fmt::Write;
192
193 // SAFETY: write to a string always succeeds
194 write!(&mut self.text, " {key}={value}").unwrap();
195 Ok(())
196 }
197}
198
199fn default_timestamp_format(ts: Timestamp, tz: &TimeZone) -> String {
200 let offset = tz.to_offset(ts);
201 format!("{:.6}", ts.display_with_offset(offset))
202}
203
204impl Layout for TextLayout {
205 fn format(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<Vec<u8>, Error> {
206 // SAFETY: jiff::Timestamp::try_from only fails if the time is out of range, which is
207 // very unlikely if the system clock is correct.
208 let ts = Timestamp::try_from(record.time()).unwrap();
209 let time = if let Some(format) = self.timestamp_format {
210 format(ts, &self.timezone)
211 } else {
212 default_timestamp_format(ts, &self.timezone)
213 };
214
215 let level = self.format_record_level(record.level());
216 let target = record.target();
217 let file = record.filename();
218 let line = record.line().unwrap_or_default();
219 let message = record.payload();
220
221 let mut visitor = KvWriter { text: time };
222 write!(
223 &mut visitor.text,
224 " {level:>6} {target}: {file}:{line} {message}"
225 )
226 .unwrap();
227 record.key_values().visit(&mut visitor)?;
228 for d in diags {
229 d.visit(&mut visitor)?;
230 }
231
232 Ok(visitor.text.into_bytes())
233 }
234}
235
236/// Colors for different log levels.
237#[derive(Debug, Clone)]
238struct LevelColor {
239 /// Color for fatal level logs.
240 fatal: Color,
241 /// Color for error level logs.
242 error: Color,
243 /// Color for warning level logs.
244 warn: Color,
245 /// Color for info level logs.
246 info: Color,
247 /// Color for debug level logs.
248 debug: Color,
249 /// Color for trace level logs.
250 trace: Color,
251}
252
253impl Default for LevelColor {
254 fn default() -> Self {
255 Self {
256 fatal: Color::BrightRed,
257 error: Color::Red,
258 warn: Color::Yellow,
259 info: Color::Green,
260 debug: Color::Blue,
261 trace: Color::Magenta,
262 }
263 }
264}
265
266impl LevelColor {
267 /// Colorize the log level.
268 fn colorize_record_level(&self, no_color: bool, level: Level) -> ColoredString {
269 if no_color {
270 ColoredString::from(level.to_string())
271 } else {
272 let color = match level {
273 Level::Fatal | Level::Fatal2 | Level::Fatal3 | Level::Fatal4 => self.fatal,
274 Level::Error | Level::Error2 | Level::Error3 | Level::Error4 => self.error,
275 Level::Warn | Level::Warn2 | Level::Warn3 | Level::Warn4 => self.warn,
276 Level::Info | Level::Info2 | Level::Info3 | Level::Info4 => self.info,
277 Level::Debug | Level::Debug2 | Level::Debug3 | Level::Debug4 => self.debug,
278 Level::Trace | Level::Trace2 | Level::Trace3 | Level::Trace4 => self.trace,
279 };
280 ColoredString::from(level.to_string()).color(color)
281 }
282 }
283}