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