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