Skip to main content

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