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        let priority = match record.level() {
258            Level::Error => b"3",
259            Level::Warn => b"4",
260            Level::Info => b"5",
261            Level::Debug => b"6",
262            Level::Trace => b"7",
263        };
264
265        put_field_bytes(&mut buffer, FieldName::WellFormed("PRIORITY"), priority);
266        put_field_length_encoded(
267            &mut buffer,
268            FieldName::WellFormed("MESSAGE"),
269            record.payload(),
270        );
271        // Syslog compatibility fields
272        // SAFETY: write to a Vec<u8> always succeeds
273        writeln!(&mut buffer, "SYSLOG_PID={}", std::process::id()).unwrap();
274        if !self.syslog_identifier.is_empty() {
275            put_field_bytes(
276                &mut buffer,
277                FieldName::WellFormed("SYSLOG_IDENTIFIER"),
278                self.syslog_identifier.as_bytes(),
279            );
280        }
281        if let Some(file) = record.file() {
282            put_field_bytes(
283                &mut buffer,
284                FieldName::WellFormed("CODE_FILE"),
285                file.as_bytes(),
286            );
287        }
288        if let Some(module) = record.module_path() {
289            put_field_bytes(
290                &mut buffer,
291                FieldName::WellFormed("CODE_MODULE"),
292                module.as_bytes(),
293            );
294        }
295        if let Some(line) = record.line() {
296            // SAFETY: write to a Vec<u8> always succeeds
297            writeln!(&mut buffer, "CODE_LINE={line}").unwrap();
298        }
299        put_field_bytes(
300            &mut buffer,
301            FieldName::WellFormed("TARGET"),
302            record.target().as_bytes(),
303        );
304        // Put all structured values of the record
305        let mut visitor = WriteKeyValues(&mut buffer);
306        record.key_values().visit(&mut visitor)?;
307        for d in diags {
308            d.visit(&mut visitor)?;
309        }
310        // Put all extra fields of the appender
311        buffer.extend_from_slice(&self.extra_fields);
312        self.send_payload(&buffer)?;
313        Ok(())
314    }
315
316    fn flush(&self) -> Result<(), Error> {
317        // UnixDatagram doesn't buffer anything, so nothing to do here.
318        Ok(())
319    }
320}