From 44720171c1d8daba042b9d33c8138d5a9d9d4d71 Mon Sep 17 00:00:00 2001 From: Finomnis Date: Tue, 5 Sep 2023 22:13:43 +0200 Subject: [PATCH] journald: allow custom journal fields (#2708) ## Motivation It's currently not possible to customize how messages will get send to journald. This became apparent in #2425, where first a specific API got designed, but then it was decided that users should not get restricted in only a subset of fields, but should be able to simply choose by themselves what fields get set with what values. So in a sense, this is the successor/rework of #2425. ## Solution Allow custom fields to be set in tracing-journald. ## Open Questions - [x] How should we deal with fields that also get supplied by other options? For example, setting `SYSLOG_IDENTIFIER` here and also setting `.with_syslog_identifier()` will send said field twice, potentially with differing values. Is that a problem? - Answer: No, this is not a problem. Closes #2425 --- tracing-journald/src/lib.rs | 37 +++++++++++++++++++++++++++++++ tracing-journald/tests/journal.rs | 22 ++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tracing-journald/src/lib.rs b/tracing-journald/src/lib.rs index ac816058ca..a15b0d6f67 100644 --- a/tracing-journald/src/lib.rs +++ b/tracing-journald/src/lib.rs @@ -83,6 +83,7 @@ pub struct Layer { socket: UnixDatagram, field_prefix: Option, syslog_identifier: String, + additional_fields: Vec, } #[cfg(unix)] @@ -107,6 +108,7 @@ impl Layer { .map(|n| n.to_string_lossy().into_owned()) // If we fail to get the name of the current executable fall back to an empty string. .unwrap_or_else(String::new), + additional_fields: Vec::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. @@ -148,6 +150,40 @@ impl Layer { self } + /// Adds fields that will get be passed to journald with every log entry. + /// + /// The input values of this function are interpreted as `(field, value)` pairs. + /// + /// This can for example be used to configure the syslog facility. + /// See [Journal Fields](https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html) + /// and [journalctl](https://www.freedesktop.org/software/systemd/man/journalctl.html) + /// for more information. + /// + /// Fields specified using this method will be added to the journald + /// message alongside fields generated from the event's fields, its + /// metadata, and the span context. If the name of a field provided using + /// this method is the same as the name of a field generated by the + /// subscriber, both fields will be sent to journald. + /// + /// ```no_run + /// # use tracing_journald::Subscriber; + /// let sub = Subscriber::new() + /// .unwrap() + /// .with_custom_fields([("SYSLOG_FACILITY", "17")]); + /// ``` + /// + pub fn with_custom_fields, U: AsRef<[u8]>>( + mut self, + fields: impl IntoIterator, + ) -> Self { + for (name, value) in fields { + put_field_length_encoded(&mut self.additional_fields, name.as_ref(), |buf| { + buf.extend_from_slice(value.as_ref()) + }) + } + self + } + /// Returns the syslog identifier in use. pub fn syslog_identifier(&self) -> &str { &self.syslog_identifier @@ -255,6 +291,7 @@ where put_field_length_encoded(&mut buf, "SYSLOG_IDENTIFIER", |buf| { write!(buf, "{}", self.syslog_identifier).unwrap() }); + buf.extend_from_slice(&self.additional_fields); event.record(&mut EventVisitor::new( &mut buf, diff --git a/tracing-journald/tests/journal.rs b/tracing-journald/tests/journal.rs index 7cbcd24d15..ae38633fcc 100644 --- a/tracing-journald/tests/journal.rs +++ b/tracing-journald/tests/journal.rs @@ -238,6 +238,28 @@ fn simple_metadata() { }); } +#[test] +fn journal_fields() { + let sub = Subscriber::new() + .unwrap() + .with_field_prefix(None) + .with_custom_fields([("SYSLOG_FACILITY", "17")]) + .with_custom_fields([("ABC", "dEf"), ("XYZ", "123")]); + with_journald_subscriber(sub, || { + info!(test.name = "journal_fields", "Hello World"); + + let message = retry_read_one_line_from_journal("journal_fields"); + assert_eq!(message["MESSAGE"], "Hello World"); + assert_eq!(message["PRIORITY"], "5"); + assert_eq!(message["TARGET"], "journal"); + assert_eq!(message["SYSLOG_FACILITY"], "17"); + assert_eq!(message["ABC"], "dEf"); + assert_eq!(message["XYZ"], "123"); + assert!(message["CODE_FILE"].as_text().is_some()); + assert!(message["CODE_LINE"].as_text().is_some()); + }); +} + #[test] fn span_metadata() { with_journald(|| {