From e6e7f6d612ba0aac751757e052ac74195773e004 Mon Sep 17 00:00:00 2001 From: Gabriel Goller Date: Thu, 11 Jan 2024 21:28:41 +0100 Subject: [PATCH] journald: make level mappings configurable (#2824) This allows to manually map tracing levels to journald levels. It seems that @little-dude, who started the original PR, doesn't have time to finish this, so I picked it up. Reapplied the changes to the newest master branch and fixed the latest comments/issues. This will also fix/close: Closes #2649 Closes #2661 Closes #2347 (the original pr) --- tracing-journald/src/lib.rs | 177 +++++++++++++++++++++++++++--- tracing-journald/tests/journal.rs | 48 +++++++- 2 files changed, 209 insertions(+), 16 deletions(-) diff --git a/tracing-journald/src/lib.rs b/tracing-journald/src/lib.rs index d7229af801..1ecbea9edb 100644 --- a/tracing-journald/src/lib.rs +++ b/tracing-journald/src/lib.rs @@ -60,7 +60,7 @@ mod socket; /// names by translating `.`s into `_`s, stripping leading `_`s and non-ascii-alphanumeric /// characters other than `_`, and upcasing. /// -/// Levels are mapped losslessly to journald `PRIORITY` values as follows: +/// By default, levels are mapped losslessly to journald `PRIORITY` values as follows: /// /// - `ERROR` => Error (3) /// - `WARN` => Warning (4) @@ -68,6 +68,8 @@ mod socket; /// - `DEBUG` => Informational (6) /// - `TRACE` => Debug (7) /// +/// These mappings can be changed with [`Subscriber::with_priority_mappings`]. +/// /// The standard journald `CODE_LINE` and `CODE_FILE` fields are automatically emitted. A `TARGET` /// field is emitted containing the event's target. /// @@ -84,6 +86,7 @@ pub struct Layer { field_prefix: Option, syslog_identifier: String, additional_fields: Vec, + priority_mappings: PriorityMappings, } #[cfg(unix)] @@ -109,6 +112,7 @@ impl Layer { // If we fail to get the name of the current executable fall back to an empty string. .unwrap_or_default(), additional_fields: Vec::new(), + priority_mappings: PriorityMappings::new(), }; // Check that we can talk to journald, by sending empty payload which journald discards. // However if the socket didn't exist or if none listened we'd get an error here. @@ -129,6 +133,41 @@ impl Layer { self } + /// Sets how [`tracing_core::Level`]s are mapped to [journald priorities](Priority). + /// + /// # Examples + /// + /// ```rust + /// use tracing_journald::{Priority, PriorityMappings}; + /// use tracing_subscriber::prelude::*; + /// use tracing::error; + /// + /// let registry = tracing_subscriber::registry(); + /// match tracing_journald::subscriber() { + /// Ok(subscriber) => { + /// registry.with( + /// subscriber + /// // We can tweak the mappings between the trace level and + /// // the journal priorities. + /// .with_priority_mappings(PriorityMappings { + /// info: Priority::Informational, + /// ..PriorityMappings::new() + /// }), + /// ); + /// } + /// // journald is typically available on Linux systems, but nowhere else. Portable software + /// // should handle its absence gracefully. + /// Err(e) => { + /// registry.init(); + /// error!("couldn't connect to journald: {}", e); + /// } + /// } + /// ``` + pub fn with_priority_mappings(mut self, mappings: PriorityMappings) -> Self { + self.priority_mappings = mappings; + self + } + /// Sets the syslog identifier for this logger. /// /// The syslog identifier comes from the classic syslog interface (`openlog()` @@ -232,6 +271,20 @@ impl Layer { memfd::seal_fully(mem.as_raw_fd())?; socket::send_one_fd_to(&self.socket, mem.as_raw_fd(), JOURNALD_PATH) } + + fn put_priority(&self, buf: &mut Vec, meta: &Metadata) { + put_field_wellformed( + buf, + "PRIORITY", + &[match *meta.level() { + Level::ERROR => self.priority_mappings.error as u8, + Level::WARN => self.priority_mappings.warn as u8, + Level::INFO => self.priority_mappings.info as u8, + Level::DEBUG => self.priority_mappings.debug as u8, + Level::TRACE => self.priority_mappings.trace as u8, + }], + ); + } } /// Construct a journald layer @@ -286,7 +339,7 @@ where } // Record event fields - put_priority(&mut buf, event.metadata()); + self.put_priority(&mut buf, event.metadata()); put_metadata(&mut buf, event.metadata(), None); put_field_length_encoded(&mut buf, "SYSLOG_IDENTIFIER", |buf| { write!(buf, "{}", self.syslog_identifier).unwrap() @@ -374,18 +427,114 @@ impl Visit for EventVisitor<'_> { } } -fn put_priority(buf: &mut Vec, meta: &Metadata) { - put_field_wellformed( - buf, - "PRIORITY", - match *meta.level() { - Level::ERROR => b"3", - Level::WARN => b"4", - Level::INFO => b"5", - Level::DEBUG => b"6", - Level::TRACE => b"7", - }, - ); +/// A priority (called "severity code" by syslog) is used to mark the +/// importance of a message. +/// +/// Descriptions and examples are taken from the [Arch Linux wiki]. +/// Priorities are also documented in the +/// [section 6.2.1 of the Syslog protocol RFC][syslog]. +/// +/// [Arch Linux wiki]: https://wiki.archlinux.org/title/Systemd/Journal#Priority_level +/// [syslog]: https://www.rfc-editor.org/rfc/rfc5424#section-6.2.1 +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[repr(u8)] +pub enum Priority { + /// System is unusable. + /// + /// Examples: + /// + /// - severe Kernel BUG + /// - systemd dumped core + /// + /// This level should not be used by applications. + Emergency = b'0', + /// Should be corrected immediately. + /// + /// Examples: + /// + /// - Vital subsystem goes out of work, data loss: + /// - `kernel: BUG: unable to handle kernel paging request at ffffc90403238ffc` + Alert = b'1', + /// Critical conditions + /// + /// Examples: + /// + /// - Crashe, coredumps + /// - `systemd-coredump[25319]: Process 25310 (plugin-container) of user 1000 dumped core` + Critical = b'2', + /// Error conditions + /// + /// Examples: + /// + /// - Not severe error reported + /// - `kernel: usb 1-3: 3:1: cannot get freq at ep 0x84, systemd[1]: Failed unmounting /var` + /// - `libvirtd[1720]: internal error: Failed to initialize a valid firewall backend` + Error = b'3', + /// May indicate that an error will occur if action is not taken. + /// + /// Examples: + /// + /// - a non-root file system has only 1GB free + /// - `org.freedesktop. Notifications[1860]: (process:5999): Gtk-WARNING **: Locale not supported by C library. Using the fallback 'C' locale` + Warning = b'4', + /// Events that are unusual, but not error conditions. + /// + /// Examples: + /// + /// - `systemd[1]: var.mount: Directory /var to mount over is not empty, mounting anyway` + /// - `gcr-prompter[4997]: Gtk: GtkDialog mapped without a transient parent. This is discouraged` + Notice = b'5', + /// Normal operational messages that require no action. + /// + /// Example: `lvm[585]: 7 logical volume(s) in volume group "archvg" now active` + Informational = b'6', + /// Information useful to developers for debugging the + /// application. + /// + /// Example: `kdeinit5[1900]: powerdevil: Scheduling inhibition from ":1.14" "firefox" with cookie 13 and reason "screen"` + Debug = b'7', +} + +/// Mappings from tracing [`Level`]s to journald [priorities]. +/// +/// [priorities]: Priority +#[derive(Debug, Clone)] +pub struct PriorityMappings { + /// Priority mapped to the `ERROR` level + pub error: Priority, + /// Priority mapped to the `WARN` level + pub warn: Priority, + /// Priority mapped to the `INFO` level + pub info: Priority, + /// Priority mapped to the `DEBUG` level + pub debug: Priority, + /// Priority mapped to the `TRACE` level + pub trace: Priority, +} + +impl PriorityMappings { + /// Returns the default priority mappings: + /// + /// - [`tracing::Level::ERROR`]: [`Priority::Error`] (3) + /// - [`tracing::Level::WARN`]: [`Priority::Warning`] (4) + /// - [`tracing::Level::INFO`]: [`Priority::Notice`] (5) + /// - [`tracing::Level::DEBUG`]: [`Priority::Informational`] (6) + /// - [`tracing::Level::TRACE`]: [`Priority::Debug`] (7) + pub fn new() -> PriorityMappings { + Self { + error: Priority::Error, + warn: Priority::Warning, + info: Priority::Notice, + debug: Priority::Informational, + trace: Priority::Debug, + } + } +} + +impl Default for PriorityMappings { + fn default() -> Self { + Self::new() + } } fn put_metadata(buf: &mut Vec, meta: &Metadata, prefix: Option<&str>) { diff --git a/tracing-journald/tests/journal.rs b/tracing-journald/tests/journal.rs index c2f3010879..3ccc31ff29 100644 --- a/tracing-journald/tests/journal.rs +++ b/tracing-journald/tests/journal.rs @@ -7,7 +7,7 @@ use std::time::Duration; use serde::Deserialize; use tracing::{debug, error, info, info_span, warn}; -use tracing_journald::Layer; +use tracing_journald::{Priority, PriorityMappings, Layer}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::Registry; @@ -17,7 +17,16 @@ fn journalctl_version() -> std::io::Result { } fn with_journald(f: impl FnOnce()) { - with_journald_layer(Layer::new().unwrap().with_field_prefix(None), f) + with_journald_layer( + Layer::new() + .unwrap() + .with_field_prefix(None) + .with_priority_mappings(PriorityMappings { + trace: Priority::Informational, + ..PriorityMappings::new() + }), + f, + ) } fn with_journald_layer(layer: Layer, f: impl FnOnce()) { @@ -168,6 +177,41 @@ fn simple_message() { }); } +#[test] +fn custom_priorities() { + fn check_message(level: &str, priority: &str) { + let entry = retry_read_one_line_from_journal(&format!("custom_priority.{}", level)); + assert_eq!(entry["MESSAGE"], format!("hello {}", level).as_str()); + assert_eq!(entry["PRIORITY"], priority); + } + + let priorities = PriorityMappings { + error: Priority::Critical, + warn: Priority::Error, + info: Priority::Warning, + debug: Priority::Notice, + trace: Priority::Informational, + }; + let layer = Layer::new() + .unwrap() + .with_field_prefix(None) + .with_priority_mappings(priorities); + let test = || { + trace!(test.name = "custom_priority.trace", "hello trace"); + check_message("trace", "6"); + debug!(test.name = "custom_priority.debug", "hello debug"); + check_message("debug", "5"); + info!(test.name = "custom_priority.info", "hello info"); + check_message("info", "4"); + warn!(test.name = "custom_priority.warn", "hello warn"); + check_message("warn", "3"); + error!(test.name = "custom_priority.error", "hello error"); + check_message("error", "2"); + }; + + with_journald_layer(layer, test); +} + #[test] fn multiline_message() { with_journald(|| {