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