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