Skip to main content

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