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}