use core::fmt;
use std::io;
use crate::{Facility, Priority, Severity};
const SPACE_BYTE: u8 = 0x20;
#[derive(Default)]
pub struct Config<'a> {
pub facility: Facility,
pub hostname: Option<&'a Hostname>,
pub app_name: Option<&'a AppName>,
pub proc_id: Option<&'a ProcId>,
}
impl Config<'_> {
pub fn into_formatter(self) -> Formatter {
self.into()
}
}
impl<'a> From<Config<'a>> for Formatter {
fn from(config: Config<'a>) -> Self {
Formatter::from_config(config)
}
}
#[derive(Clone, Debug)]
pub struct Formatter {
facility: Facility,
host_app_proc_id: Box<str>,
}
impl Default for Formatter {
fn default() -> Self {
Config::default().into_formatter()
}
}
impl Formatter {
pub fn from_config(config: Config<'_>) -> Self {
let hostname = config.hostname;
let app_name = config.app_name;
let proc_id = config.proc_id;
let hostname = hostname.unwrap_or(NILVALUE);
let app_name = app_name.unwrap_or(NILVALUE);
let proc_id = proc_id.unwrap_or(NILVALUE);
let host_app_proc_id = format!("{hostname} {app_name} {proc_id}").into_boxed_str();
Self {
facility: config.facility,
host_app_proc_id,
}
}
pub fn write_with_data<'a, W, TS, M, I, P>(
&self,
w: &mut W,
severity: Severity,
timestamp: TS,
msg: M,
msg_id: Option<&MsgId>,
data: I,
) -> io::Result<()>
where
W: io::Write,
TS: Into<Timestamp<'a>>,
M: Into<Msg<'a>>,
I: IntoIterator<Item = (&'a SdId, P)> + 'a,
P: IntoIterator<Item = SdParam<'a>> + 'a,
{
self.write_header(w, severity, timestamp, msg_id)?;
write_data(w, data)?;
write_msg(w, msg)
}
pub fn write_without_data<'a, W, TS, M>(
&self,
w: &mut W,
severity: Severity,
timestamp: TS,
msg: M,
msg_id: Option<&MsgId>,
) -> io::Result<()>
where
W: io::Write,
TS: Into<Timestamp<'a>>,
M: Into<Msg<'a>>,
{
self.write_header(w, severity, timestamp, msg_id)?;
write_nil_value(w)?;
write_msg(w, msg)
}
pub fn write_header<'a, W, TS>(
&self,
w: &mut W,
severity: Severity,
timestamp: TS,
msg_id: Option<&MsgId>,
) -> io::Result<()>
where
W: io::Write,
TS: Into<Timestamp<'a>>,
{
let Self {
facility,
host_app_proc_id,
} = self;
let prio = encode_priority(severity, *facility);
let msg_id = msg_id.unwrap_or(NILVALUE);
write!(w, "<{prio}>{VERSION} ")?;
let timestamp = timestamp.into();
match timestamp {
#[cfg(feature = "chrono")]
Timestamp::Chrono(datetime) => {
write_chrono_datetime(w, datetime)?;
}
#[cfg(feature = "chrono")]
Timestamp::CreateChronoLocal => {
let datetime = chrono::Local::now();
write_chrono_datetime(w, &datetime)?;
}
Timestamp::PreformattedStr(s) => w.write_all(s.as_bytes())?,
Timestamp::PreformattedString(s) => w.write_all(s.as_bytes())?,
Timestamp::None => write_nil_value(w)?,
};
write!(w, " {host_app_proc_id} {msg_id}")?;
Ok(())
}
}
pub fn write_data<'a, W, I, P>(w: &mut W, data: I) -> io::Result<()>
where
W: io::Write,
I: IntoIterator<Item = (&'a SdId, P)> + 'a,
P: IntoIterator<Item = SdParam<'a>> + 'a,
{
let mut elems = data.into_iter();
let Some(elem) = elems.next() else {
write!(w, " {NILVALUE}")?;
return Ok(());
};
write!(w, " ")?;
write_data_elem(w, elem)?;
for elem in elems {
write_data_elem(w, elem)?;
}
Ok(())
}
fn write_data_elem<'a, W, P>(w: &mut W, elem: (&'a SdId, P)) -> io::Result<()>
where
W: io::Write,
P: IntoIterator<Item = SdParam<'a>> + 'a,
{
let (id, params) = elem;
let mut params = params.into_iter();
let Some(param) = params.next() else {
write!(w, "[{id}]")?;
return Ok(());
};
let (name, value) = param;
write!(w, "[{id} {name}=\"{value}\"")?;
for param in params {
let (name, value) = param;
write!(w, " {name}=\"{value}\"")?;
}
write!(w, "]")
}
pub fn write_msg<'a, W, M>(w: &mut W, msg: M) -> io::Result<()>
where
W: io::Write,
M: Into<Msg<'a>>,
{
let msg = msg.into();
match msg {
Msg::Utf8Str(s) => write_str_msg(w, s),
Msg::Utf8String(s) => write_str_msg(w, &s),
Msg::NonUnicodeBytes(bytes) => {
let bytes_written = w.write(&[SPACE_BYTE])?;
debug_assert_eq!(bytes_written, 1);
let bytes_written = w.write(bytes)?;
debug_assert_eq!(bytes_written, bytes.len());
Ok(())
}
Msg::FmtArguments(args) => write!(w, " {args}"),
Msg::FmtArgumentsRef(args) => write!(w, " {args}"),
}
}
pub fn write_nil_value<W>(w: &mut W) -> io::Result<()>
where
W: io::Write,
{
write!(w, " {NILVALUE}")?;
Ok(())
}
#[cfg(feature = "chrono")]
pub fn write_chrono_datetime<W: io::Write>(
w: &mut W,
datetime: &ChronoLocalTime,
) -> io::Result<()> {
use chrono::Timelike;
const MILLI_IN_NANO: u32 = 1000;
const SEC_IN_HOUR: i32 = 3600;
const PLUS: &str = "+";
const MIN: &str = "-";
let date = datetime.date_naive();
let time = datetime.time();
let h = time.hour();
let m = time.minute();
let s = time.second();
let ms = time.nanosecond() / MILLI_IN_NANO;
let offset_hour = datetime.offset().local_minus_utc() / SEC_IN_HOUR;
let sign = if offset_hour >= 0 { PLUS } else { MIN };
write!(
w,
"{date:?}T{h:02}:{m:02}:{s:02}.{ms:06}{sign}{offset_hour:02}:00"
)?;
Ok(())
}
pub fn write_utf8_bom<W: io::Write>(w: &mut W) -> io::Result<()> {
const BOM: [u8; 4] = [SPACE_BYTE, 0xEF, 0xBB, 0xBF];
w.write_all(&BOM)
}
fn write_str_msg<W: io::Write>(w: &mut W, s: &str) -> io::Result<()> {
if !s.is_empty() {
write_utf8_bom(w)?;
w.write_all(s.as_bytes())?;
}
Ok(())
}
const NILVALUE: &str = "-";
const VERSION: &str = "1";
#[cfg(feature = "chrono")]
type ChronoLocalTime = chrono::DateTime<chrono::Local>;
pub enum Timestamp<'a> {
#[cfg(feature = "chrono")]
Chrono(&'a ChronoLocalTime),
#[cfg(feature = "chrono")]
CreateChronoLocal,
PreformattedStr(&'a str),
PreformattedString(String),
None,
}
impl<'a> From<&'a str> for Timestamp<'a> {
fn from(s: &'a str) -> Self {
Self::PreformattedStr(s)
}
}
impl From<String> for Timestamp<'_> {
fn from(s: String) -> Self {
Self::PreformattedString(s)
}
}
#[cfg(feature = "chrono")]
impl<'a> From<&'a ChronoLocalTime> for Timestamp<'a> {
fn from(datetime: &'a ChronoLocalTime) -> Self {
Self::Chrono(datetime)
}
}
type Hostname = str;
type AppName = str;
type ProcId = str;
type MsgId = str;
pub enum Msg<'a> {
Utf8Str(&'a str),
Utf8String(String),
NonUnicodeBytes(&'a [u8]),
FmtArguments(fmt::Arguments<'a>),
FmtArgumentsRef(&'a fmt::Arguments<'a>),
}
impl<'a> From<&'a str> for Msg<'a> {
fn from(s: &'a str) -> Self {
Self::Utf8Str(s)
}
}
impl From<String> for Msg<'_> {
fn from(s: String) -> Self {
Self::Utf8String(s)
}
}
impl<'a> From<&'a [u8]> for Msg<'a> {
fn from(bytes: &'a [u8]) -> Self {
Self::NonUnicodeBytes(bytes)
}
}
impl<'a> From<fmt::Arguments<'a>> for Msg<'a> {
fn from(args: fmt::Arguments<'a>) -> Self {
Self::FmtArguments(args)
}
}
impl<'a> From<&'a fmt::Arguments<'a>> for Msg<'a> {
fn from(args: &'a fmt::Arguments<'a>) -> Self {
Self::FmtArgumentsRef(args)
}
}
type SdId = str;
type SdParam<'a> = (ParamName<'a>, ParamValue<'a>);
type ParamName<'a> = &'a str;
type ParamValue<'a> = &'a str;
fn encode_priority(severity: Severity, facility: Facility) -> Priority {
facility as u8 | severity as u8
}
#[cfg(test)]
mod tests {
use std::io::ErrorKind;
use assert_matches::assert_matches;
use super::*;
#[test]
#[cfg(feature = "chrono")]
fn should_format_date_like_chrono() {
let datetime = chrono::Local::now();
let use_z = false;
let chrono_s = datetime.to_rfc3339_opts(chrono::SecondsFormat::Micros, use_z);
let mut buf = Vec::with_capacity(32);
write_chrono_datetime(&mut buf, &datetime).unwrap();
let s = String::from_utf8(buf).unwrap();
assert_eq!(
chrono_s, s,
"syslog-fmt date formatter should be char for char equal to Chrono"
);
}
#[test]
fn should_write_message_in_sections() {
let hostname = "mymachine.example.com";
let app_name = "su";
let severity = Severity::Crit;
let msg_id = "ID47";
let msg = "'su root' failed for lonvick on /dev/pts/8";
let fmt = Config {
facility: Facility::Auth,
hostname: hostname.into(),
app_name: app_name.into(),
proc_id: None,
}
.into_formatter();
let mut buf = vec![];
fmt.write_header(
&mut buf,
severity,
Timestamp::CreateChronoLocal,
Some(msg_id),
)
.unwrap();
write_nil_value(&mut buf).unwrap();
write_msg(&mut buf, msg).unwrap();
let parts = parse_syslog_message(&buf);
assert_matches!(
parts,
Parts {
prio: "<34>1",
timestamp: _,
hostname: "mymachine.example.com",
app_name: "su",
proc_id: NILVALUE,
msg_id,
data: NILVALUE,
msg
} if msg_id == msg_id && msg == msg
);
}
#[test]
fn should_write_message_with_custom_formatting() {
use std::io::Write;
let hostname = "mymachine.example.com";
let app_name = "su";
let severity = Severity::Crit;
let msg_id = "ID47";
let fmt = Config {
facility: Facility::Auth,
hostname: hostname.into(),
app_name: app_name.into(),
proc_id: None,
}
.into_formatter();
let mut buf = vec![];
fmt.write_header(
&mut buf,
severity,
Timestamp::CreateChronoLocal,
Some(msg_id),
)
.unwrap();
write_nil_value(&mut buf).unwrap();
write_utf8_bom(&mut buf).unwrap();
let msg = "'su root' failed for lonvick on /dev/pts/8";
let module = "app::connection";
let lineno = "101";
write!(&mut buf, "{module} l:{lineno} {msg}").unwrap();
let parts = parse_syslog_message(&buf);
let comp = format!("{module} l:{lineno} {msg}");
assert_matches!(
parts,
Parts {
prio: "<34>1",
timestamp: _,
hostname: "mymachine.example.com",
app_name: "su",
proc_id: NILVALUE,
msg_id,
data: NILVALUE,
msg
} if msg_id == msg_id && msg == comp
);
}
#[test]
fn should_format_message_without_msg_id() {
let hostname = "mymachine.example.com";
let app_name = "su";
let severity = Severity::Crit;
let msg = "'su root' failed for lonvick on /dev/pts/8";
let fmt = Config {
facility: Facility::Auth,
hostname: hostname.into(),
app_name: app_name.into(),
proc_id: None,
}
.into_formatter();
let mut buf = vec![];
fmt.write_without_data(&mut buf, severity, Timestamp::CreateChronoLocal, msg, None)
.unwrap();
let parts = parse_syslog_message(&buf);
assert_matches!(
parts,
Parts {
prio: "<34>1",
timestamp: _,
hostname,
app_name,
proc_id: NILVALUE,
msg_id: NILVALUE,
data: NILVALUE,
msg
} if hostname == hostname && app_name == app_name && msg == msg
);
}
#[test]
fn should_format_message_with_msg_id() {
let hostname = "mymachine.example.com";
let app_name = "su";
let severity = Severity::Crit;
let msg_id = "ID47";
let msg = "'su root' failed for lonvick on /dev/pts/8";
let fmt = Config {
facility: Facility::Auth,
hostname: hostname.into(),
app_name: app_name.into(),
proc_id: None,
}
.into_formatter();
let mut buf = vec![];
fmt.write_without_data(
&mut buf,
severity,
Timestamp::CreateChronoLocal,
msg,
Some(msg_id),
)
.unwrap();
let parts = parse_syslog_message(&buf);
assert_matches!(
parts,
Parts {
prio: "<34>1",
timestamp: _,
hostname: "mymachine.example.com",
app_name: "su",
proc_id: NILVALUE,
msg_id,
data: NILVALUE,
msg
} if msg_id == msg_id && msg == msg
);
}
#[test]
fn should_format_message_with_structured_data_and_message() {
let hostname = "mymachine.example.com";
let app_name = "evntslog";
let severity = Severity::Notice;
let msg_id = "ID47";
let msg = "An application event log entry...";
let fmt = Config {
facility: Facility::Local4,
hostname: hostname.into(),
app_name: app_name.into(),
proc_id: None,
}
.into_formatter();
let mut buf = vec![];
fmt.write_with_data(
&mut buf,
severity,
Timestamp::CreateChronoLocal,
msg,
Some(msg_id),
vec![(
"exampleSDID@32473",
vec![
("iut", "3"),
("eventSource", "Application"),
("eventID", "1011"),
],
)],
)
.unwrap();
let parts = parse_syslog_message(&buf);
assert_matches!(
parts,
Parts {
prio: "<165>1",
timestamp: _,
hostname: "mymachine.example.com",
app_name: "evntslog",
proc_id: NILVALUE,
msg_id,
data: r#"[exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"]"#,
msg
} if hostname == hostname && app_name == app_name && msg_id == msg_id && msg == msg
);
}
#[test]
fn should_format_message_with_structured_data_and_no_message() {
let hostname = "mymachine.example.com";
let app_name = "evntslog";
let severity = Severity::Notice;
let msg_id = "ID47";
let msg = "";
let fmt = Config {
facility: Facility::Local4,
hostname: hostname.into(),
app_name: app_name.into(),
proc_id: None,
}
.into_formatter();
let mut buf = vec![];
fmt.write_with_data(
&mut buf,
severity,
Timestamp::CreateChronoLocal,
msg,
Some(msg_id),
vec![(
"exampleSDID@32473",
vec![
("iut", "3"),
("eventSource", "Application"),
("eventID", "1011"),
],
)],
)
.unwrap();
let parts = parse_syslog_message(&buf);
assert_matches!(
parts,
Parts {
prio: "<165>1",
timestamp: _,
hostname,
app_name,
proc_id: NILVALUE,
msg_id,
data: r#"[exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"]"#,
msg
} if hostname == hostname && app_name == app_name && msg_id == msg_id && msg == msg
);
}
#[test]
fn should_truncate_message_to_buffer_size() {
use arrayvec::ArrayVec;
let timestamp = "1985-04-12T23:20:50.52Z";
let hostname = "mymachine.example.com";
let app_name = "su";
let severity = Severity::Crit;
let msg = "'su root' failed for lonvick on /dev/pts/8";
let fmt = Config {
facility: Facility::Auth,
hostname: hostname.into(),
app_name: app_name.into(),
proc_id: None,
}
.into_formatter();
let mut buf = ArrayVec::<u8, 100>::new();
let err = fmt
.write_without_data(&mut buf, severity, timestamp, msg, None)
.unwrap_err();
assert_eq!(
err.kind(),
ErrorKind::WriteZero,
"The given buffer is too small for the message. But the formatter should write as much as possible"
);
let parts = parse_syslog_message(&buf);
assert_matches!(
parts,
Parts {
prio: "<34>1",
timestamp,
hostname,
app_name,
proc_id: NILVALUE,
msg_id: NILVALUE,
data: NILVALUE,
msg: "'su root' failed for lonvick on /dev"
} if timestamp == timestamp && hostname == hostname && app_name == app_name
);
}
#[test]
fn should_fmt_structured_data() {
use arrayvec::ArrayVec;
let mut buf = ArrayVec::<u8, 100>::new();
buf.clear();
write_data::<_, [(&str, [(&str, &str); 0]); 0], _>(&mut buf, []).unwrap();
assert_eq!(std::str::from_utf8(&buf).unwrap(), " -");
buf.clear();
write_data(&mut buf, [("first", [])]).unwrap();
assert_eq!(std::str::from_utf8(&buf).unwrap(), " [first]");
buf.clear();
write_data(&mut buf, [("first", []), ("second", [])]).unwrap();
assert_eq!(std::str::from_utf8(&buf).unwrap(), " [first][second]");
buf.clear();
write_data(&mut buf, [("first", [("p-one", "pv-one")])]).unwrap();
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
r#" [first p-one="pv-one"]"#
);
buf.clear();
write_data(
&mut buf,
[("first", [("p-one", "pv-one"), ("p-two", "pv-two")])],
)
.unwrap();
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
r#" [first p-one="pv-one" p-two="pv-two"]"#
);
buf.clear();
write_data(
&mut buf,
[
("first", [("p-one", "pv-one"), ("p-two", "pv-two")]),
("second", [("p-one", "pv-one"), ("p-two", "pv-two")]),
],
)
.unwrap();
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
r#" [first p-one="pv-one" p-two="pv-two"][second p-one="pv-one" p-two="pv-two"]"#
);
}
#[derive(Debug)]
struct Parts<'a> {
prio: &'a str,
timestamp: &'a str,
hostname: &'a str,
app_name: &'a str,
proc_id: &'a str,
msg_id: &'a str,
data: &'a str,
msg: &'a str,
}
fn parse_syslog_message(buf: &[u8]) -> Parts<'_> {
const DELIM: char = ' ';
const UTF8_BOM: char = '\u{feff}';
let s = std::str::from_utf8(buf).unwrap();
let (prio, s) = s.split_once(DELIM).unwrap();
let (timestamp, s) = s.split_once(DELIM).unwrap();
let (hostname, s) = s.split_once(DELIM).unwrap();
let (app_name, s) = s.split_once(DELIM).unwrap();
let (proc_id, s) = s.split_once(DELIM).unwrap();
let (msg_id, s) = s.split_once(DELIM).unwrap();
let (data, msg) = if s.starts_with('[') {
let index = s.rfind(']').expect("There should be a closing delimiter");
let (data, s) = s.split_at(index + 1);
let s = s.trim();
(data, s.strip_prefix(UTF8_BOM).unwrap_or(s))
} else {
let (data, s) = s.split_once(DELIM).unwrap();
(data, s.strip_prefix(UTF8_BOM).unwrap_or(s))
};
Parts {
prio,
timestamp,
hostname,
app_name,
proc_id,
msg_id,
data,
msg,
}
}
#[test]
fn should_parse_example_1_with_no_structured_data() {
let msg_buf= b"<34>1 2003-10-11T22:14:15.003Z mymachine.example.com su - ID47 - 'su root' failed for lonvick on /dev/pts/8";
let parts = parse_syslog_message(msg_buf);
assert_matches!(
parts,
Parts {
prio: "<34>1",
timestamp: "2003-10-11T22:14:15.003Z",
hostname: "mymachine.example.com",
app_name: "su",
proc_id: NILVALUE,
msg_id: "ID47",
data: NILVALUE,
msg: "'su root' failed for lonvick on /dev/pts/8"
}
);
}
#[test]
fn should_parse_example_2_with_no_structured_data() {
let msg_buf= b"<165>1 2003-08-24T05:14:15.000003-07:00 192.0.2.1 myproc 8710 - - %% It's time to make the do-nuts.";
let parts = parse_syslog_message(msg_buf);
assert_matches!(
parts,
Parts {
prio: "<165>1",
timestamp: "2003-08-24T05:14:15.000003-07:00",
hostname: "192.0.2.1",
app_name: "myproc",
proc_id: "8710",
msg_id: NILVALUE,
data: NILVALUE,
msg: "%% It's time to make the do-nuts."
}
);
}
#[test]
fn should_parse_example_3_with_structured_data() {
let msg_buf= br#"<165>1 2003-10-11T22:14:15.003Z mymachine.example.com evntslog - ID47 [exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"] An application event log entry..."#;
let parts = parse_syslog_message(msg_buf);
assert_matches!(
parts,
Parts {
prio: "<165>1",
timestamp: "2003-10-11T22:14:15.003Z",
hostname: "mymachine.example.com",
app_name: "evntslog",
proc_id: NILVALUE,
msg_id: "ID47",
data: r#"[exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"]"#,
msg: "An application event log entry..."
}
);
}
#[test]
fn should_parse_example_4_structured_data_only() {
let msg_buf= br#"<165>1 2003-10-11T22:14:15.003Z mymachine.example.com evntslog - ID47 [exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"][examplePriority@32473 class="high"]"#;
let parts = parse_syslog_message(msg_buf);
assert_matches!(
parts,
Parts {
prio: "<165>1",
timestamp: "2003-10-11T22:14:15.003Z",
hostname: "mymachine.example.com",
app_name: "evntslog",
proc_id: NILVALUE,
msg_id: "ID47",
data: r#"[exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"][examplePriority@32473 class="high"]"#,
msg: ""
}
);
}
}