logforth_append_journald/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//! Appender for integrating with systemd's journald.
16
17#![cfg(unix)]
18#![cfg_attr(docsrs, feature(doc_cfg))]
19
20use std::io::Write;
21use std::os::unix::net::UnixDatagram;
22
23use logforth_core::Append;
24use logforth_core::Diagnostic;
25use logforth_core::Error;
26use logforth_core::kv::Key;
27use logforth_core::kv::Value;
28use logforth_core::kv::Visitor;
29use logforth_core::record::Level;
30use logforth_core::record::Record;
31
32mod field;
33#[cfg(target_os = "linux")]
34mod memfd;
35
36const JOURNALD_PATH: &str = "/run/systemd/journal/socket";
37
38fn current_exe_identifier() -> Option<String> {
39 let executable = std::env::current_exe().ok()?;
40 Some(executable.file_name()?.to_string_lossy().into_owned())
41}
42
43/// A systemd journal appender.
44///
45/// ## Journal access
46///
47/// ## Standard fields
48///
49/// The journald appender always sets the following standard [journal fields]:
50///
51/// - `PRIORITY`: The log level mapped to a priority (see below).
52/// - `MESSAGE`: The formatted log message (see [`Record::payload()`]).
53/// - `SYSLOG_PID`: The PID of the running process (see [`std::process::id()`]).
54/// - `CODE_FILE`: The filename the log message originates from (see [`Record::file()`], only if
55/// present).
56/// - `CODE_LINE`: The line number the log message originates from (see [`Record::line()`], only if
57/// present).
58///
59/// It also sets `SYSLOG_IDENTIFIER` if non-empty (see [`Journald::with_syslog_identifier`]).
60///
61/// Additionally, it also adds the following non-standard fields:
62///
63/// - `TARGET`: The target of the log record (see [`Record::target()`]).
64/// - `CODE_MODULE`: The module path of the log record (see [`Record::module_path()`], only if
65/// present).
66///
67/// [journal fields]: https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html
68///
69/// ## Log levels and Priorities
70///
71/// [`Level`] gets mapped to journal (syslog) priorities as follows:
72///
73/// - [`Level::Error`] → `3` (err)
74/// - [`Level::Warn`] → `4` (warning)
75/// - [`Level::Info`] → `5` (notice)
76/// - [`Level::Debug`] → `6` (info)
77/// - [`Level::Trace`] → `7` (debug)
78///
79/// Higher priorities (crit, alert, and emerg) are not used.
80///
81/// ## Custom fields and structured record fields
82///
83/// In addition to these fields the appender also adds all structures key-values
84/// from each log record as journal fields, and also supports global extra fields via
85/// [`Journald::with_extra_fields`].
86///
87/// Journald allows only ASCII uppercase letters, ASCII digits, and the
88/// underscore in field names, and limits field names to 64 bytes. See
89/// [`journal_field_valid`][jfv] for the precise validation rules.
90///
91/// This appender mangles the keys of additional key-values on records and names
92/// of custom fields according to the following rules, to turn them into valid
93/// journal fields:
94///
95/// - If the key is entirely empty, use `EMPTY`.
96/// - Transform the entire value to ASCII uppercase.
97/// - Replace all invalid characters with underscore.
98/// - If the key starts with an underscore or digit, which is not permitted, prepend `ESCAPED_`.
99/// - Cap the result to 64 bytes.
100///
101/// [jfv]: https://github.com/systemd/systemd/blob/v256.7/src/libsystemd/sd-journal/journal-file.c#L1703
102///
103/// # Errors
104///
105/// The appender tries to connect to journald when constructed, to provide early
106/// on feedback if journald is not available (e.g. in containers where the
107/// journald socket is not mounted into the container).
108#[derive(Debug)]
109pub struct Journald {
110 /// The datagram socket to send messages to journald.
111 socket: UnixDatagram,
112 /// Preformatted extra fields to be appended to every log message.
113 extra_fields: Vec<u8>,
114 /// The syslog identifier.
115 syslog_identifier: String,
116}
117
118impl Journald {
119 /// Construct a journald appender
120 ///
121 /// Fails if the journald socket couldn't be opened.
122 pub fn new() -> Result<Self, Error> {
123 let socket = UnixDatagram::unbound().map_err(Error::from_io_error)?;
124 let sub = Self {
125 socket,
126 extra_fields: Vec::new(),
127 syslog_identifier: current_exe_identifier().unwrap_or_default(),
128 };
129 // Check that we can talk to journald, by sending empty payload which journald discards.
130 // However, if the socket didn't exist or if none listened we'd get an error here.
131 sub.send_payload(&[])?;
132 Ok(sub)
133 }
134
135 /// Add an extra field to be added to every log entry.
136 ///
137 /// `name` is the name of a custom field, and `value` its value. Fields are
138 /// appended to every log entry, in order they were added to the appender.
139 ///
140 /// ## Restrictions on field names
141 ///
142 /// `name` should be a valid journal file name, i.e. it must only contain
143 /// ASCII uppercase alphanumeric characters and the underscore, and must
144 /// start with an ASCII uppercase letter.
145 ///
146 /// Invalid keys in `extra_fields` are escaped according to the rules
147 /// documented in [`Journald`].
148 ///
149 /// It is not recommended that `name` is any of the standard fields already
150 /// added by this appender (see [`Journald`]); though journald supports
151 /// multiple values for a field, journald clients may not handle unexpected
152 /// multi-value fields properly and perhaps only show the first value.
153 /// Specifically, even `journalctl` will only show the first `MESSAGE` value
154 /// of journal entries.
155 ///
156 /// ## Restrictions on values
157 ///
158 /// There are no restrictions on the value.
159 pub fn with_extra_field<K: AsRef<str>, V: AsRef<[u8]>>(mut self, name: K, value: V) -> Self {
160 field::put_field_bytes(
161 &mut self.extra_fields,
162 field::FieldName::WriteEscaped(name.as_ref()),
163 value.as_ref(),
164 );
165 self
166 }
167
168 /// Add extra fields to be added to every log entry.
169 ///
170 /// See [`Self::with_extra_field`] for details.
171 pub fn with_extra_fields<I, K, V>(mut self, extra_fields: I) -> Self
172 where
173 I: IntoIterator<Item = (K, V)>,
174 K: AsRef<str>,
175 V: AsRef<[u8]>,
176 {
177 for (name, value) in extra_fields {
178 field::put_field_bytes(
179 &mut self.extra_fields,
180 field::FieldName::WriteEscaped(name.as_ref()),
181 value.as_ref(),
182 );
183 }
184 self
185 }
186
187 /// Set the syslog identifier for this appender.
188 ///
189 /// The syslog identifier comes from the classic syslog interface (`openlog()`
190 /// and `syslog()`) and tags log entries with a given identifier.
191 /// Systemd exposes it in the `SYSLOG_IDENTIFIER` journal field, and allows
192 /// filtering log messages by syslog identifier with `journalctl -t`.
193 /// Unlike the unit (`journalctl -u`) this field is not trusted, i.e. applications
194 /// can set it freely, and use it e.g. to further categorize log entries emitted under
195 /// the same systemd unit or in the same process. It also allows to filter for log
196 /// entries of processes not started in their own unit.
197 ///
198 /// See [Journal Fields](https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html)
199 /// and [journalctl](https://www.freedesktop.org/software/systemd/man/journalctl.html)
200 /// for more information.
201 ///
202 /// Defaults to the file name of the executable of the current process, if any.
203 pub fn with_syslog_identifier(mut self, identifier: String) -> Self {
204 self.syslog_identifier = identifier;
205 self
206 }
207
208 /// Return the syslog identifier in use.
209 pub fn syslog_identifier(&self) -> &str {
210 &self.syslog_identifier
211 }
212
213 fn send_payload(&self, payload: &[u8]) -> Result<usize, Error> {
214 self.socket.send_to(payload, JOURNALD_PATH).or_else(|err| {
215 if Some(libc::EMSGSIZE) == err.raw_os_error() {
216 self.send_large_payload(payload)
217 } else {
218 Err(Error::from_io_error(err))
219 }
220 })
221 }
222
223 #[cfg(all(unix, not(target_os = "linux")))]
224 fn send_large_payload(&self, _payload: &[u8]) -> Result<usize, Error> {
225 Err(Error::new("large payloads not supported on non-Linux OS"))
226 }
227
228 /// Send large payloads to journald via a memfd.
229 #[cfg(target_os = "linux")]
230 fn send_large_payload(&self, payload: &[u8]) -> Result<usize, Error> {
231 memfd::send_large_payload(&self.socket, payload)
232 .map_err(|err| Error::new("failed to send payload via memfd").set_source(err))
233 }
234}
235
236struct WriteKeyValues<'a>(&'a mut Vec<u8>);
237
238impl Visitor for WriteKeyValues<'_> {
239 fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> {
240 let key = key.as_str();
241 field::put_field_length_encoded(self.0, field::FieldName::WriteEscaped(key), value);
242 Ok(())
243 }
244}
245
246impl Append for Journald {
247 /// Extract all fields (standard and custom) from `record`, append all `extra_fields` given
248 /// to this appender, and send the result to journald.
249 fn append(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<(), Error> {
250 use field::*;
251
252 let mut buffer = vec![];
253
254 // Write standard fields. Numeric fields can't contain new lines so we
255 // write them directly, everything else goes through the put functions
256 // for property mangling and length-encoding
257 //
258 // For PRIORITY mapping, see syslog severity levels and opentelemetry mapping guideline:
259 // https://opentelemetry.io/docs/specs/otel/logs/data-model-appendix/#appendix-b-severitynumber-example-mappings
260 let priority = match record.level() {
261 Level::Fatal | Level::Fatal2 | Level::Fatal3 | Level::Fatal4 => b"0", // Emergency
262 Level::Error3 | Level::Error4 => b"1", // Alert
263 Level::Error2 => b"2", // Critical
264 Level::Error => b"3", // Error
265 Level::Warn | Level::Warn2 | Level::Warn3 | Level::Warn4 => b"4", // Warning
266 Level::Info2 | Level::Info3 | Level::Info4 => b"5", // Notice
267 Level::Info => b"6", // Informational
268 Level::Debug
269 | Level::Debug2
270 | Level::Debug3
271 | Level::Debug4
272 | Level::Trace
273 | Level::Trace2
274 | Level::Trace3
275 | Level::Trace4 => b"7", // Debug
276 };
277
278 put_field_bytes(&mut buffer, FieldName::WellFormed("PRIORITY"), priority);
279 put_field_length_encoded(
280 &mut buffer,
281 FieldName::WellFormed("MESSAGE"),
282 record.payload(),
283 );
284 // Syslog compatibility fields
285 // SAFETY: write to a Vec<u8> always succeeds
286 writeln!(&mut buffer, "SYSLOG_PID={}", std::process::id()).unwrap();
287 if !self.syslog_identifier.is_empty() {
288 put_field_bytes(
289 &mut buffer,
290 FieldName::WellFormed("SYSLOG_IDENTIFIER"),
291 self.syslog_identifier.as_bytes(),
292 );
293 }
294 if let Some(file) = record.file() {
295 put_field_bytes(
296 &mut buffer,
297 FieldName::WellFormed("CODE_FILE"),
298 file.as_bytes(),
299 );
300 }
301 if let Some(module) = record.module_path() {
302 put_field_bytes(
303 &mut buffer,
304 FieldName::WellFormed("CODE_MODULE"),
305 module.as_bytes(),
306 );
307 }
308 if let Some(line) = record.line() {
309 // SAFETY: write to a Vec<u8> always succeeds
310 writeln!(&mut buffer, "CODE_LINE={line}").unwrap();
311 }
312 put_field_bytes(
313 &mut buffer,
314 FieldName::WellFormed("TARGET"),
315 record.target().as_bytes(),
316 );
317 // Put all structured values of the record
318 let mut visitor = WriteKeyValues(&mut buffer);
319 record.key_values().visit(&mut visitor)?;
320 for d in diags {
321 d.visit(&mut visitor)?;
322 }
323 // Put all extra fields of the appender
324 buffer.extend_from_slice(&self.extra_fields);
325 self.send_payload(&buffer)?;
326 Ok(())
327 }
328
329 fn flush(&self) -> Result<(), Error> {
330 // UnixDatagram doesn't buffer anything, so nothing to do here.
331 Ok(())
332 }
333}