logforth_layout_logfmt/
lib.rs1#![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#[derive(Default, Debug, Clone)]
56pub struct LogfmtLayout {
57 tz: Option<TimeZone>,
58}
59
60impl LogfmtLayout {
61 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 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 return Err(Error::new(format!("key contains special chars: {key}")));
93 }
94
95 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 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}