Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions tracing-journald/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ categories = [
keywords = ["tracing", "journald"]
rust-version = "1.65.0"

[features]
msg-formatter = ["dep:tracing-log"]

[dependencies]
libc = "0.2.126"
tracing-core = { path = "../tracing-core", version = "0.1.28" }
tracing-subscriber = { path = "../tracing-subscriber", version = "0.3.0", default-features = false, features = ["registry"] }
tracing-core = { path = "../tracing-core", version = "0.1.36" }
tracing-subscriber = { path = "../tracing-subscriber", version = "0.3.22", default-features = false, features = ["registry"] }
tracing-log = { path = "../tracing-log", version = "0.2.0", features = ["log-tracer"], optional = true }

[dev-dependencies]
serde_json = "1.0.82"
serde = { version = "1.0.140", features = ["derive"] }
tracing = { path = "../tracing", version = "0.1.35" }
tracing = { path = "../tracing", version = "0.1.44" }

[lints]
workspace = true
108 changes: 98 additions & 10 deletions tracing-journald/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
#[cfg(unix)]
use std::os::unix::net::UnixDatagram;
use std::{fmt, io, io::Write};
#[cfg(feature = "msg-formatter")]
use tracing_log::NormalizeEvent;

use tracing_core::{
event::Event,
Expand All @@ -54,6 +56,21 @@ mod memfd;
#[cfg(target_os = "linux")]
mod socket;

/// A trait for formatting log messages with access to the message and metadata.
///
/// Implement this trait to provide custom message formatting logic.
pub trait MessageFormatter: Send + Sync {
/// Format a message with access to its content and metadata.
///
/// # Arguments
/// * `message` - The original message content
/// * `metadata` - The event metadata containing level, target, file, line, etc.
///
/// # Returns
/// The formatted message as a `String`
fn format_message(&self, message: &str, metadata: &Metadata<'_>) -> String;
}

/// Sends events and their fields to journald
///
/// [journald conventions] for structured field names differ from typical tracing idioms, and journald
Expand Down Expand Up @@ -88,6 +105,7 @@ pub struct Layer {
syslog_identifier: String,
additional_fields: Vec<u8>,
priority_mappings: PriorityMappings,
message_formatter: Option<Box<dyn MessageFormatter>>,
}

#[cfg(unix)]
Expand Down Expand Up @@ -116,6 +134,7 @@ impl Layer {
.unwrap_or_default(),
additional_fields: Vec::new(),
priority_mappings: PriorityMappings::new(),
message_formatter: None,
};
// 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 Down Expand Up @@ -171,6 +190,34 @@ impl Layer {
self
}

/// Sets a custom message formatter for log messages.
///
/// The formatter will receive the message content and event metadata,
/// allowing you to customize how messages appear in journald logs.
///
/// # Examples
///
/// ```no_run
/// use tracing_journald::{Layer, MessageFormatter};
/// use tracing_core::Metadata;
///
/// struct MyFormatter;
///
/// impl MessageFormatter for MyFormatter {
/// fn format_message(&self, message: &str, metadata: &Metadata<'_>) -> String {
/// format!("[{}] {}", metadata.level(), message)
/// }
/// }
///
/// let layer = Layer::new()
/// .unwrap()
/// .with_message_formatter(MyFormatter);
/// ```
pub fn with_message_formatter(mut self, formatter: impl MessageFormatter + 'static) -> Self {
self.message_formatter = Some(Box::new(formatter));
self
}

/// Sets the syslog identifier for this logger.
///
/// The syslog identifier comes from the classic syslog interface (`openlog()`
Expand Down Expand Up @@ -341,9 +388,16 @@ where
buf.extend_from_slice(&fields.0);
}

#[cfg(feature = "msg-formatter")]
let normalized_meta = event.normalized_metadata();
#[cfg(feature = "msg-formatter")]
let meta = normalized_meta.as_ref().unwrap_or_else(|| event.metadata());
#[cfg(not(feature = "msg-formatter"))]
let meta = event.metadata();

// Record event fields
self.put_priority(&mut buf, event.metadata());
put_metadata(&mut buf, event.metadata(), None);
self.put_priority(&mut buf, meta);
put_metadata(&mut buf, meta, None);
put_field_length_encoded(&mut buf, "SYSLOG_IDENTIFIER", |buf| {
write!(buf, "{}", self.syslog_identifier).unwrap()
});
Expand All @@ -352,6 +406,8 @@ where
event.record(&mut EventVisitor::new(
&mut buf,
self.field_prefix.as_deref(),
meta,
self.message_formatter.as_deref(),
));

// At this point we can't handle the error anymore so just ignore it.
Expand Down Expand Up @@ -396,11 +452,23 @@ impl Visit for SpanVisitor<'_> {
struct EventVisitor<'a> {
buf: &'a mut Vec<u8>,
prefix: Option<&'a str>,
metadata: &'a Metadata<'a>,
formatter: Option<&'a dyn MessageFormatter>,
}

impl<'a> EventVisitor<'a> {
fn new(buf: &'a mut Vec<u8>, prefix: Option<&'a str>) -> Self {
Self { buf, prefix }
fn new(
buf: &'a mut Vec<u8>,
prefix: Option<&'a str>,
metadata: &'a Metadata<'a>,
formatter: Option<&'a dyn MessageFormatter>,
) -> Self {
Self {
buf,
prefix,
metadata,
formatter,
}
}

fn put_prefix(&mut self, field: &Field) {
Expand All @@ -412,21 +480,41 @@ impl<'a> EventVisitor<'a> {
}
}
}

fn format_message(&mut self, formatter: &dyn MessageFormatter, field: &Field, value: &str) {
let formatted = formatter.format_message(value, self.metadata);
put_field_length_encoded(self.buf, field.name(), |buf| {
buf.extend_from_slice(formatted.as_bytes())
});
}
}

impl Visit for EventVisitor<'_> {
fn record_str(&mut self, field: &Field, value: &str) {
self.put_prefix(field);
put_field_length_encoded(self.buf, field.name(), |buf| {
buf.extend_from_slice(value.as_bytes())
});

// Apply custom formatter to message field if available
if let (Some(formatter), "message") = (self.formatter, field.name()) {
self.format_message(formatter, field, value);
} else {
put_field_length_encoded(self.buf, field.name(), |buf| {
buf.extend_from_slice(value.as_bytes())
});
}
}

fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
self.put_prefix(field);
put_field_length_encoded(self.buf, field.name(), |buf| {
write!(buf, "{:?}", value).unwrap()
});

// Apply custom formatter to message field if available
if let (Some(formatter), "message") = (self.formatter, field.name()) {
let message = format!("{:?}", value);
self.format_message(formatter, field, &message);
} else {
put_field_length_encoded(self.buf, field.name(), |buf| {
write!(buf, "{:?}", value).unwrap()
});
}
}
}

Expand Down
54 changes: 54 additions & 0 deletions tracing-journald/tests/journal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,57 @@ fn spans_field_collision() {
assert_eq!(message["SPAN_FIELD"], vec!["foo1", "foo2", "foo3"]);
});
}

#[test]
fn message_formatter_basic() {
use tracing_core::Metadata;
use tracing_journald::MessageFormatter;

struct TestFormatter;

impl MessageFormatter for TestFormatter {
fn format_message(&self, message: &str, metadata: &Metadata<'_>) -> String {
format!("[{}] {}", metadata.level(), message)
}
}

let layer = Layer::new()
.unwrap()
.with_field_prefix(None)
.with_message_formatter(TestFormatter);

with_journald_layer(layer, || {
info!(test.name = "message_formatter_basic", "Hello World");

let message = retry_read_one_line_from_journal("message_formatter_basic");
assert_eq!(message["MESSAGE"], "[INFO] Hello World");
assert_eq!(message["PRIORITY"], "5");
});
}

#[test]
fn message_formatter_with_metadata() {
use tracing_core::Metadata;
use tracing_journald::MessageFormatter;

struct DetailedFormatter;

impl MessageFormatter for DetailedFormatter {
fn format_message(&self, message: &str, metadata: &Metadata<'_>) -> String {
format!("{} [{}]: {}", metadata.target(), metadata.level(), message)
}
}

let layer = Layer::new()
.unwrap()
.with_field_prefix(None)
.with_message_formatter(DetailedFormatter);

with_journald_layer(layer, || {
error!(test.name = "message_formatter_with_metadata", "Something went wrong");

let message = retry_read_one_line_from_journal("message_formatter_with_metadata");
assert_eq!(message["MESSAGE"], "journal [ERROR]: Something went wrong");
assert_eq!(message["PRIORITY"], "3");
});
}