Skip to content

Commit

Permalink
journald: make level mappings configurable (#2824)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
kaffarell authored and hds committed Nov 20, 2024
1 parent af54fc9 commit 07d3404
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 17 deletions.
177 changes: 163 additions & 14 deletions tracing-journald/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ 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)
/// - `INFO` => Notice (5)
/// - `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.
///
Expand All @@ -84,6 +86,7 @@ pub struct Layer {
field_prefix: Option<String>,
syslog_identifier: String,
additional_fields: Vec<u8>,
priority_mappings: PriorityMappings,
}

#[cfg(unix)]
Expand All @@ -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.
Expand All @@ -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()`
Expand Down Expand Up @@ -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<u8>, 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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -374,18 +427,114 @@ impl Visit for EventVisitor<'_> {
}
}

fn put_priority(buf: &mut Vec<u8>, 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<u8>, meta: &Metadata, prefix: Option<&str>) {
Expand Down
50 changes: 47 additions & 3 deletions tracing-journald/tests/journal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use std::time::Duration;

use serde::Deserialize;

use tracing::{debug, error, info, info_span, warn};
use tracing_journald::Layer;
use tracing::{debug, error, info, info_span, trace, warn};
use tracing_journald::{Layer, Priority, PriorityMappings};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::Registry;

Expand All @@ -17,7 +17,16 @@ fn journalctl_version() -> std::io::Result<String> {
}

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()) {
Expand Down Expand Up @@ -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(|| {
Expand Down

0 comments on commit 07d3404

Please sign in to comment.