systemd_journal_logger_memfd_syscall/lib.rs
1// Copyright Sebastian Wiesner <sebastian@swsnr.de>
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! A pure Rust [log] logger for the [systemd journal][1].
10//!
11//! [log]: https://docs.rs/log
12//! [1]: https://www.freedesktop.org/software/systemd/man/journalctl.html
13//!
14//! # Usage
15//!
16//! Create a [`JournalLog`] with [`JournalLog::new`] and then use [`JournalLog::install`] to
17//! setup journal logging. Then configure the logging level and now you can use the standard macros
18//! from the [`log`] crate to send log messages to the systemd journal:
19//!
20//! ```rust
21//! use log::{info, warn, error, LevelFilter};
22//! use systemd_journal_logger_memfd_syscall::JournalLog;
23//!
24//! JournalLog::new().unwrap().install().unwrap();
25//! log::set_max_level(LevelFilter::Info);
26//!
27//! info!("hello log");
28//! warn!("warning");
29//! error!("oops");
30//! ```
31//!
32//! See [`JournalLog`] for details about the logging format.
33//!
34//! ## Journal connections
35//!
36//! In a service you can use [`connected_to_journal`] to check whether
37//! the standard output or error stream of the current process is directly
38//! connected to the systemd journal (the default for services started by
39//! systemd) and fall back to logging to standard error if that's not the
40//! case. Take a look at the [systemd_service.rs] example for details.
41//!
42//! [systemd_service.rs]: https://github.com/swsnr/systemd-journal-logger.rs/blob/main/examples/systemd_service.rs
43//!
44//! ```rust
45//! use log::{info, warn, error, LevelFilter};
46//! use systemd_journal_logger_memfd_syscall::JournalLog;
47//!
48//! JournalLog::new()
49//! .unwrap()
50//! .with_extra_fields(vec![("VERSION", env!("CARGO_PKG_VERSION"))])
51//! .with_syslog_identifier("foo".to_string())
52//! .install().unwrap();
53//! log::set_max_level(LevelFilter::Info);
54//!
55//! info!("this message has an extra VERSION field in the journal");
56//! ```
57//!
58//! You can display these extra fields with `journalctl --output=verbose` and extract them with any of the structured
59//! output formats of `journalctl`, e.g. `journalctl --output=json`.
60
61#![deny(warnings, missing_docs, clippy::all)]
62
63use std::io::prelude::*;
64use std::os::fd::AsFd;
65use std::os::linux::fs::MetadataExt;
66
67use client::JournalClient;
68use log::kv::{Error, Key, Value, Visitor};
69use log::{Level, Log, Metadata, Record, SetLoggerError};
70
71mod client;
72mod fields;
73mod memfd;
74mod socket;
75
76use fields::*;
77
78/// Whether the current process is directly connected to the systemd journal.
79///
80/// Return `true` if the device and inode numbers of the [`std::io::stderr`]
81/// file descriptor match the value of `$JOURNAL_STREAM` (see `systemd.exec(5)`).
82/// Otherwise, return `false`.
83pub fn connected_to_journal() -> bool {
84 std::io::stderr()
85 .as_fd()
86 .try_clone_to_owned()
87 .and_then(|fd| std::fs::File::from(fd).metadata())
88 .map(|metadata| format!("{}:{}", metadata.st_dev(), metadata.st_ino()))
89 .ok()
90 .and_then(|stderr| {
91 std::env::var_os("JOURNAL_STREAM").map(|s| s.to_string_lossy() == stderr.as_str())
92 })
93 .unwrap_or(false)
94}
95
96/// Create a syslog identifier from the current executable.
97///
98/// Return `None` if we're unable to determine the name, e.g. because
99/// [`std::env::current_exe`] failed or returned some weird name.
100pub fn current_exe_identifier() -> Option<String> {
101 let executable = std::env::current_exe().ok()?;
102 Some(executable.file_name()?.to_string_lossy().into_owned())
103}
104
105struct WriteKeyValues<'a>(&'a mut Vec<u8>);
106
107impl<'a, 'kvs> Visitor<'kvs> for WriteKeyValues<'a> {
108 fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), Error> {
109 put_field_length_encoded(self.0, FieldName::WriteEscaped(key.as_str()), value);
110 Ok(())
111 }
112}
113
114/// A systemd journal logger.
115///
116/// ## Journal access
117///
118/// ## Standard fields
119///
120/// The journald logger always sets the following standard [journal fields]:
121///
122/// - `PRIORITY`: The log level mapped to a priority (see below).
123/// - `MESSAGE`: The formatted log message (see [`log::Record::args()`]).
124/// - `SYSLOG_PID`: The PID of the running process (see [`std::process::id()`]).
125/// - `CODE_FILE`: The filename the log message originates from (see [`log::Record::file()`], only if present).
126/// - `CODE_LINE`: The line number the log message originates from (see [`log::Record::line()`], only if present).
127///
128/// It also sets `SYSLOG_IDENTIFIER` if non-empty (see [`JournalLog::with_syslog_identifier`]).
129///
130/// Additionally it also adds the following non-standard fields:
131///
132/// - `TARGET`: The target of the log record (see [`log::Record::target()`]).
133/// - `CODE_MODULE`: The module path of the log record (see [`log::Record::module_path()`], only if present).
134///
135/// [journal fields]: https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html
136///
137/// ## Log levels and Priorities
138///
139/// [`log::Level`] gets mapped to journal (syslog) priorities as follows:
140///
141/// - [`Level::Error`] → `3` (err)
142/// - [`Level::Warn`] → `4` (warning)
143/// - [`Level::Info`] → `5` (notice)
144/// - [`Level::Debug`] → `6` (info)
145/// - [`Level::Trace`] → `7` (debug)
146///
147/// Higher priorities (crit, alert, and emerg) are not used.
148///
149/// ## Custom fields and structured record fields
150///
151/// In addition to these fields the logger also adds all structures key-values
152/// (see [`log::Record::key_values`]) from each log record as journal fields,
153/// and also supports global extra fields via [`Self::with_extra_fields`].
154///
155/// Journald allows only ASCII uppercase letters, ASCII digits, and the
156/// underscore in field names, and limits field names to 64 bytes. See upstream's
157/// [`journal_field_valid`][jfv] for the precise validation rules.
158///
159/// This logger mangles the keys of additional key-values on records and names
160/// of custom fields according to the following rules, to turn them into valid
161/// journal fields:
162///
163/// - If the key is entirely empty, use `EMPTY`.
164/// - Transform the entire value to ASCII uppercase.
165/// - Replace all invalid characters with underscore.
166/// - If the key starts with an underscore or digit, which is not permitted,
167/// prepend `ESCAPED_`.
168/// - Cap the result to 64 bytes.
169///
170/// [jfv]: https://github.com/systemd/systemd/blob/a8b53f4f1558b17169809effd865232580e4c4af/src/libsystemd/sd-journal/journal-file.c#L1698
171///
172/// # Errors
173///
174/// The logger tries to connect to journald when constructed, to provide early
175/// on feedback if journald is not available (e.g. in containers where the
176/// journald socket is not mounted into the container).
177///
178/// Later on, the logger simply ignores any errors when sending log records to
179/// journald, simply because the log interface does not expose faillible operations.
180pub struct JournalLog {
181 /// The journald client
182 client: JournalClient,
183 /// Preformatted extra fields to be appended to every log message.
184 extra_fields: Vec<u8>,
185 /// The syslog identifier.
186 syslog_identifier: String,
187}
188
189fn record_payload(syslog_identifier: &str, record: &Record) -> Vec<u8> {
190 use FieldName::*;
191 let mut buffer = Vec::with_capacity(1024);
192 // Write standard fields. Numeric fields can't contain new lines so we
193 // write them directly, everything else goes through the put functions
194 // for property mangling and length-encoding
195 let priority = match record.level() {
196 Level::Error => b"3",
197 Level::Warn => b"4",
198 Level::Info => b"5",
199 Level::Debug => b"6",
200 Level::Trace => b"7",
201 };
202 put_field_bytes(&mut buffer, WellFormed("PRIORITY"), priority);
203 put_field_length_encoded(&mut buffer, WellFormed("MESSAGE"), record.args());
204 // Syslog compatibility fields
205 writeln!(&mut buffer, "SYSLOG_PID={}", std::process::id()).unwrap();
206 if !syslog_identifier.is_empty() {
207 put_field_bytes(
208 &mut buffer,
209 WellFormed("SYSLOG_IDENTIFIER"),
210 syslog_identifier.as_bytes(),
211 );
212 }
213 if let Some(file) = record.file() {
214 put_field_bytes(&mut buffer, WellFormed("CODE_FILE"), file.as_bytes());
215 }
216 if let Some(module) = record.module_path() {
217 put_field_bytes(&mut buffer, WellFormed("CODE_MODULE"), module.as_bytes());
218 }
219 if let Some(line) = record.line() {
220 writeln!(&mut buffer, "CODE_LINE={}", line).unwrap();
221 }
222 put_field_bytes(
223 &mut buffer,
224 WellFormed("TARGET"),
225 record.target().as_bytes(),
226 );
227 // Put all structured values of the record
228 record
229 .key_values()
230 .visit(&mut WriteKeyValues(&mut buffer))
231 .unwrap();
232 buffer
233}
234
235impl JournalLog {
236 /// Create a journal log instance with a default syslog identifier.
237 pub fn new() -> std::io::Result<Self> {
238 let logger = Self::empty()?;
239 Ok(logger.with_syslog_identifier(current_exe_identifier().unwrap_or_default()))
240 }
241
242 /// Create an empty journal log instance, with no extra fields and no syslog
243 /// identifier.
244 ///
245 /// See [`Self::with_syslog_identifier`] and [`Self::with_extra_fields`] to
246 /// set either. It's recommended to at least set the syslog identifier.
247 pub fn empty() -> std::io::Result<Self> {
248 Ok(Self {
249 client: JournalClient::new()?,
250 extra_fields: Vec::new(),
251 syslog_identifier: String::new(),
252 })
253 }
254
255 /// Install this logger globally.
256 ///
257 /// See [`log::set_boxed_logger`].
258 pub fn install(self) -> Result<(), SetLoggerError> {
259 log::set_boxed_logger(Box::new(self))
260 }
261
262 /// Add an extra field to be added to every log entry.
263 ///
264 /// `name` is the name of a custom field, and `value` its value. Fields are
265 /// appended to every log entry, in order they were added to the logger.
266 ///
267 /// ## Restrictions on field names
268 ///
269 /// `name` should be a valid journal file name, i.e. it must only contain
270 /// ASCII uppercase alphanumeric characters and the underscore, and must
271 /// start with an ASCII uppercase letter.
272 ///
273 /// Invalid keys in `extra_fields` are escaped according to the rules
274 /// documented in [`JournalLog`].
275 ///
276 /// It is not recommended that `name` is any of the standard fields already
277 /// added by this logger (see [`JournalLog`]); though journald supports
278 /// multiple values for a field, journald clients may not handle unexpected
279 /// multi-value fields properly and perhaps only show the first value.
280 /// Specifically, even `journalctl` will only shouw the first `MESSAGE` value
281 /// of journal entries.
282 ///
283 /// ## Restrictions on values
284 ///
285 /// There are no restrictions on the value.
286 pub fn add_extra_field<K: AsRef<str>, V: AsRef<[u8]>>(mut self, name: K, value: V) -> Self {
287 put_field_bytes(
288 &mut self.extra_fields,
289 FieldName::WriteEscaped(name.as_ref()),
290 value.as_ref(),
291 );
292 self
293 }
294
295 /// Set extra fields to be added to every log entry.
296 ///
297 /// Remove all previously added fields.
298 ///
299 /// See [`Self::add_extra_field`] for details.
300 pub fn with_extra_fields<I, K, V>(mut self, extra_fields: I) -> Self
301 where
302 I: IntoIterator<Item = (K, V)>,
303 K: AsRef<str>,
304 V: AsRef<[u8]>,
305 {
306 self.extra_fields.clear();
307 let mut logger = self;
308 for (name, value) in extra_fields {
309 logger = logger.add_extra_field(name, value);
310 }
311 logger
312 }
313
314 /// Set the given syslog identifier for this logger.
315 ///
316 /// The logger writes this string in the `SYSLOG_IDENTIFIER` field, which
317 /// can be filtered for with `journalctl -t`.
318 ///
319 /// Use [`current_exe_identifier()`] to obtain the standard identifier for
320 /// the current executable.
321 pub fn with_syslog_identifier(mut self, identifier: String) -> Self {
322 self.syslog_identifier = identifier;
323 self
324 }
325
326 /// Get the complete journal payload for `record`, including extra fields
327 /// from this logger.
328 fn record_payload(&self, record: &Record) -> Vec<u8> {
329 let mut payload = record_payload(&self.syslog_identifier, record);
330 payload.extend_from_slice(&self.extra_fields);
331 payload
332 }
333
334 /// Send a single log record to the journal.
335 ///
336 /// Extract all fields (standard and custom) from `record` (`see [`JournalLog`]),
337 /// append all `extra_fields` given to this logger, and send the result to
338 /// journald.
339 pub fn journal_send(&self, record: &Record) -> std::io::Result<()> {
340 let _ = self.client.send_payload(&self.record_payload(record))?;
341 Ok(())
342 }
343}
344
345/// The [`Log`] interface for [`JournalLog`].
346impl Log for JournalLog {
347 /// Whether this logger is enabled.
348 ///
349 /// Always returns `true`.
350 fn enabled(&self, _metadata: &Metadata) -> bool {
351 true
352 }
353
354 /// Send the given `record` to the systemd journal.
355 ///
356 /// # Errors
357 ///
358 /// Ignore any errors which occur when sending `record` to journald because
359 /// we cannot reasonably handle them at this place.
360 ///
361 /// See [`JournalLog::journal_send`] for a function which returns any error
362 /// which might have occurred while sending the `record` to the journal.
363 fn log(&self, record: &Record) {
364 // We can't really handle errors here, so simply discard them.
365 // The alternative would be to panic, but a failed logging call should
366 // not bring the entire process down.
367 let _ = self.journal_send(record);
368 }
369
370 /// Flush log records.
371 ///
372 /// A no-op for journal logging.
373 fn flush(&self) {}
374}