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}