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}