logforth_layout_logfmt/
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 logfmt layout for formatting log records.
16
17#![cfg_attr(docsrs, feature(doc_auto_cfg))]
18
19use jiff::Timestamp;
20use jiff::tz::TimeZone;
21use logforth_core::Diagnostic;
22use logforth_core::Error;
23use logforth_core::kv::Key;
24use logforth_core::kv::Value;
25use logforth_core::kv::Visitor;
26use logforth_core::layout::Layout;
27use logforth_core::record::Record;
28
29/// A logfmt layout for formatting log records.
30///
31/// Output format:
32///
33/// ```text
34/// ```
35///
36/// # Examples
37///
38/// ```
39/// use logforth_layout_logfmt::LogfmtLayout;
40///
41/// let layout = LogfmtLayout::default();
42/// ```
43#[derive(Default, Debug, Clone)]
44pub struct LogfmtLayout {
45    tz: Option<TimeZone>,
46}
47
48impl LogfmtLayout {
49    /// Set the timezone for timestamps.
50    ///
51    /// Output format:
52    ///
53    /// ```text
54    /// timestamp=2025-03-31T21:04:28.986032+08:00 level=TRACE module=rs_log position=main.rs:22 message="Hello trace!"
55    /// timestamp=2025-03-31T21:04:28.991233+08:00 level=DEBUG module=rs_log position=main.rs:23 message="Hello debug!"
56    /// timestamp=2025-03-31T21:04:28.991239+08:00 level=INFO module=rs_log position=main.rs:24 message="Hello info!"
57    /// timestamp=2025-03-31T21:04:28.991273+08:00 level=WARN module=rs_log position=main.rs:25 message="Hello warn!"
58    /// timestamp=2025-03-31T21:04:28.991277+08:00 level=ERROR module=rs_log position=main.rs:26 message="Hello err!"
59    /// ```
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use jiff::tz::TimeZone;
65    /// use logforth_layout_logfmt::LogfmtLayout;
66    ///
67    /// let layout = LogfmtLayout::default().timezone(TimeZone::UTC);
68    /// ```
69    pub fn timezone(mut self, tz: TimeZone) -> Self {
70        self.tz = Some(tz);
71        self
72    }
73}
74
75struct KvFormatter {
76    text: String,
77}
78
79impl Visitor for KvFormatter {
80    // The encode logic is copied from https://github.com/go-logfmt/logfmt/blob/76262ea7/encode.go.
81    fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> {
82        use std::fmt::Write;
83
84        let key = key.as_str();
85        let value = value.to_string();
86        let value = value.as_str();
87
88        if key.contains([' ', '=', '"']) {
89            // omit keys contain special chars
90            return Err(Error::new(format!("key contains special chars: {key}")));
91        }
92
93        // SAFETY: write to a string always succeeds
94        if value.contains([' ', '=', '"']) {
95            write!(&mut self.text, " {key}=\"{}\"", value.escape_debug()).unwrap();
96        } else {
97            write!(&mut self.text, " {key}={value}").unwrap();
98        }
99
100        Ok(())
101    }
102}
103
104impl Layout for LogfmtLayout {
105    fn format(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<Vec<u8>, Error> {
106        // SAFETY: jiff::Timestamp::try_from only fails if the time is out of range, which is
107        // very unlikely if the system clock is correct.
108        let ts = Timestamp::try_from(record.time()).unwrap();
109        let tz = self.tz.clone().unwrap_or_else(TimeZone::system);
110        let offset = tz.to_offset(ts);
111        let time = ts.display_with_offset(offset);
112
113        let level = record.level();
114        let target = record.target();
115        let file = record.filename();
116        let line = record.line().unwrap_or_default();
117        let message = record.args();
118
119        let mut visitor = KvFormatter {
120            text: format!("timestamp={time:.6}"),
121        };
122
123        visitor.visit("level".into(), level.as_str().into())?;
124        visitor.visit("module".into(), target.into())?;
125        visitor.visit(
126            "position".into(),
127            Value::from_debug(&format_args!("{file}:{line}")),
128        )?;
129        visitor.visit("message".into(), Value::from_debug(message))?;
130
131        record.key_values().visit(&mut visitor)?;
132        for d in diags {
133            d.visit(&mut visitor)?;
134        }
135
136        Ok(visitor.text.into_bytes())
137    }
138}