diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 398dab70..13ca0952 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -63,7 +63,6 @@ use std::io::prelude::Write; use std::rc::Rc; use std::{fmt, io, mem}; -#[cfg(feature = "color")] use log::Level; use log::Record; @@ -84,6 +83,18 @@ pub use crate::writer::WriteStyle; use crate::writer::{Buffer, Writer}; +/// Controls whether systemd journal priority prefixes are prepended to log lines. +#[derive(Copy, Clone, Debug, Default)] +pub enum JournalPrefix { + /// Prepend `` prefix based on log level when `JOURNAL_STREAM` is set. + #[default] + Auto, + /// Always prepend `` prefix. + Enable, + /// Never prepend prefix. + Disable, +} + /// Formatting precision of timestamps. /// /// Seconds give precision of full seconds, milliseconds give thousands of a @@ -218,6 +229,7 @@ pub(crate) type FormatFn = Box; pub(crate) struct Builder { pub(crate) default_format: ConfigurableFormat, pub(crate) custom_format: Option, + pub(crate) journal_prefix: JournalPrefix, built: bool, } @@ -241,7 +253,13 @@ impl Builder { if let Some(fmt) = built.custom_format { fmt } else { - Box::new(built.default_format) + let mut format = built.default_format; + format.journal_prefix = match built.journal_prefix { + JournalPrefix::Enable => true, + JournalPrefix::Disable => false, + JournalPrefix::Auto => std::env::var_os("JOURNAL_STREAM").is_some(), + }; + Box::new(format) } } } @@ -286,6 +304,7 @@ pub struct ConfigurableFormat { pub(crate) source_line_number: bool, pub(crate) indent: Option, pub(crate) suffix: &'static str, + pub(crate) journal_prefix: bool, #[cfg(feature = "kv")] pub(crate) kv_format: Option>, } @@ -355,6 +374,12 @@ impl ConfigurableFormat { self } + /// Whether or not to prepend systemd journal priority prefixes (e.g. `<3>` for error). + pub fn journal_prefix(&mut self, write: bool) -> &mut Self { + self.journal_prefix = write; + self + } + /// Set the format for structured key/value pairs in the log record /// /// With the default format, this function is called for each record and should format @@ -386,6 +411,7 @@ impl Default for ConfigurableFormat { source_line_number: false, indent: Some(4), suffix: "\n", + journal_prefix: false, #[cfg(feature = "kv")] kv_format: None, } @@ -409,6 +435,7 @@ struct ConfigurableFormatWriter<'a> { impl ConfigurableFormatWriter<'_> { fn write(mut self, record: &Record<'_>) -> io::Result<()> { + self.write_journal_prefix(record)?; self.write_timestamp()?; self.write_level(record)?; self.write_module_path(record)?; @@ -422,6 +449,21 @@ impl ConfigurableFormatWriter<'_> { write!(self.buf, "{}", self.format.suffix) } + fn write_journal_prefix(&mut self, record: &Record<'_>) -> io::Result<()> { + if !self.format.journal_prefix { + return Ok(()); + } + + let priority = match record.level() { + Level::Error => 3, + Level::Warn => 4, + Level::Info => 6, + Level::Debug | Level::Trace => 7, + }; + + write!(self.buf, "<{priority}>") + } + fn subtle_style(&self, text: &'static str) -> SubtleStyle { #[cfg(feature = "color")] { @@ -672,6 +714,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: None, suffix: "\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -696,6 +739,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: None, suffix: "\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -720,6 +764,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: Some(4), suffix: "\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -744,6 +789,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: Some(0), suffix: "\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -768,6 +814,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: Some(4), suffix: "\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -792,6 +839,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: None, suffix: "\n\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -816,6 +864,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: Some(4), suffix: "\n\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -842,6 +891,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: None, suffix: "\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -867,6 +917,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: None, suffix: "\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -893,6 +944,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: None, suffix: "\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -918,6 +970,7 @@ mod tests { kv_format: Some(Box::new(hidden_kv_format)), indent: None, suffix: "\n", + journal_prefix: false, }, written_header_value: false, buf: &mut f, @@ -999,4 +1052,90 @@ mod tests { written ); } + + #[test] + fn format_journal_prefix_enabled() { + let mut f = formatter(); + + let written = write(ConfigurableFormatWriter { + format: &ConfigurableFormat { + timestamp: None, + module_path: false, + target: false, + level: true, + source_file: false, + source_line_number: false, + #[cfg(feature = "kv")] + kv_format: Some(Box::new(hidden_kv_format)), + indent: None, + suffix: "\n", + journal_prefix: true, + }, + written_header_value: false, + buf: &mut f, + }); + + assert_eq!("<6>[INFO ] log\nmessage\n", written); + } + + #[test] + fn format_journal_prefix_error_level() { + let mut f = formatter(); + + let record = Record::builder() + .args(format_args!("oops")) + .level(Level::Error) + .build(); + + let buf = f.buf.clone(); + + let fmt = ConfigurableFormatWriter { + format: &ConfigurableFormat { + timestamp: None, + module_path: false, + target: false, + level: false, + source_file: false, + source_line_number: false, + #[cfg(feature = "kv")] + kv_format: Some(Box::new(hidden_kv_format)), + indent: None, + suffix: "\n", + journal_prefix: true, + }, + written_header_value: false, + buf: &mut f, + }; + + fmt.write(&record).unwrap(); + let buf = buf.borrow(); + let written = String::from_utf8(buf.as_bytes().to_vec()).unwrap(); + + assert_eq!("<3>oops\n", written); + } + + #[test] + fn format_journal_prefix_disabled() { + let mut f = formatter(); + + let written = write(ConfigurableFormatWriter { + format: &ConfigurableFormat { + timestamp: None, + module_path: false, + target: false, + level: true, + source_file: false, + source_line_number: false, + #[cfg(feature = "kv")] + kv_format: Some(Box::new(hidden_kv_format)), + indent: None, + suffix: "\n", + journal_prefix: false, + }, + written_header_value: false, + buf: &mut f, + }); + + assert_eq!("[INFO ] log\nmessage\n", written); + } } diff --git a/src/logger.rs b/src/logger.rs index 1ecfcc84..dc0b3b0b 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -336,6 +336,22 @@ impl Builder { self } + /// Whether or not to prepend systemd journal priority prefixes. + /// + /// Defaults to [`Auto`](fmt::JournalPrefix::Auto), which enables prefixes + /// when `JOURNAL_STREAM` is set. + /// + /// ``` + /// use env_logger::{Builder, fmt::JournalPrefix}; + /// + /// let mut builder = Builder::new(); + /// builder.format_journal_prefix(JournalPrefix::Disable); + /// ``` + pub fn format_journal_prefix(&mut self, value: fmt::JournalPrefix) -> &mut Self { + self.format.journal_prefix = value; + self + } + /// Set the format for structured key/value pairs in the log record /// /// With the default format, this function is called for each record and should format @@ -1055,4 +1071,73 @@ mod tests { assert_eq!(builder.filter.build().filter(), LevelFilter::Debug); } + + #[test] + fn journal_prefix_auto_enabled_when_journal_stream_set() { + env::set_var("JOURNAL_STREAM", "8:12345"); + + let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let buf2 = buf.clone(); + + let mut builder = Builder::new(); + builder + .target(fmt::Target::Pipe(Box::new(SharedBuf(buf2)))) + .format_timestamp(None) + .format_journal_prefix(fmt::JournalPrefix::Auto) + .filter_level(LevelFilter::Info); + let logger = builder.build(); + + logger.log( + &Record::builder() + .args(format_args!("hello")) + .level(log::Level::Warn) + .target("") + .build(), + ); + + let output = String::from_utf8(buf.lock().unwrap().clone()).unwrap(); + assert!(output.starts_with("<4>"), "expected journal prefix, got: {output}"); + + env::remove_var("JOURNAL_STREAM"); + } + + #[test] + fn journal_prefix_auto_disabled_when_journal_stream_unset() { + env::remove_var("JOURNAL_STREAM"); + + let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let buf2 = buf.clone(); + + let mut builder = Builder::new(); + builder + .target(fmt::Target::Pipe(Box::new(SharedBuf(buf2)))) + .format_timestamp(None) + .format_journal_prefix(fmt::JournalPrefix::Auto) + .filter_level(LevelFilter::Info); + let logger = builder.build(); + + logger.log( + &Record::builder() + .args(format_args!("hello")) + .level(log::Level::Warn) + .target("") + .build(), + ); + + let output = String::from_utf8(buf.lock().unwrap().clone()).unwrap(); + assert!(!output.starts_with("<"), "unexpected journal prefix, got: {output}"); + } + + #[derive(Clone)] + struct SharedBuf(std::sync::Arc>>); + + impl io::Write for SharedBuf { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.lock().unwrap().extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } }