Skip to main content

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}