use std::fmt;
use std::process;
use time::format_description::FormatItem;
use time::macros::format_description;
use time::{OffsetDateTime, UtcOffset};
use tracing::{Event, Subscriber};
use tracing_subscriber::fmt::format::{FormatEvent, FormatFields, Writer};
use tracing_subscriber::fmt::FmtContext;
use tracing_subscriber::registry::LookupSpan;
use super::host::local_hostname;
pub const STRUCTURED_DATA_ID: &str = "origin@32473";
pub const APP_NAME: &str = "dynomited";
pub const TAG: &str = "dynomited";
const RFC5424_TIMESTAMP: &[FormatItem<'_>] = format_description!(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6][offset_hour sign:mandatory]:[offset_minute]"
);
const RFC3164_TIMESTAMP_DAY_PADDED: &[FormatItem<'_>] =
format_description!("[month repr:short] [day padding:space] [hour]:[minute]:[second]");
fn pri_for(level: tracing::Level) -> u8 {
let severity: u8 = match level {
tracing::Level::ERROR => 3,
tracing::Level::WARN => 4,
tracing::Level::INFO => 6,
tracing::Level::DEBUG | tracing::Level::TRACE => 7,
};
8 + severity
}
fn now_local() -> OffsetDateTime {
OffsetDateTime::now_local()
.unwrap_or_else(|_| OffsetDateTime::now_utc().to_offset(UtcOffset::UTC))
}
#[derive(Debug, Clone)]
pub struct Rfc5424Formatter {
hostname: String,
pid: u32,
}
impl Default for Rfc5424Formatter {
fn default() -> Self {
Self::new()
}
}
impl Rfc5424Formatter {
#[must_use]
pub fn new() -> Self {
Self {
hostname: local_hostname(),
pid: process::id(),
}
}
}
impl<S, N> FormatEvent<S, N> for Rfc5424Formatter
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
ctx: &FmtContext<'_, S, N>,
mut writer: Writer<'_>,
event: &Event<'_>,
) -> fmt::Result {
let metadata = event.metadata();
let pri = pri_for(*metadata.level());
let timestamp = now_local()
.format(&RFC5424_TIMESTAMP)
.unwrap_or_else(|_| "-".to_string());
let file = metadata.file().unwrap_or("-");
let line = metadata
.line()
.map_or_else(|| "-".to_string(), |n| n.to_string());
write!(
writer,
"<{pri}>1 {timestamp} {host} {app} {pid} - [{sd_id} file=\"{file}\" line=\"{line}\" target=\"{target}\" level=\"{level}\"] ",
pri = pri,
timestamp = timestamp,
host = self.hostname,
app = APP_NAME,
pid = self.pid,
sd_id = STRUCTURED_DATA_ID,
file = sanitize_sd_value(file),
line = sanitize_sd_value(&line),
target = sanitize_sd_value(metadata.target()),
level = metadata.level(),
)?;
ctx.field_format().format_fields(writer.by_ref(), event)?;
writeln!(writer)
}
}
#[derive(Debug, Clone)]
pub struct Rfc3164Formatter {
hostname: String,
pid: u32,
}
impl Default for Rfc3164Formatter {
fn default() -> Self {
Self::new()
}
}
impl Rfc3164Formatter {
#[must_use]
pub fn new() -> Self {
Self {
hostname: local_hostname(),
pid: process::id(),
}
}
}
impl<S, N> FormatEvent<S, N> for Rfc3164Formatter
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
ctx: &FmtContext<'_, S, N>,
mut writer: Writer<'_>,
event: &Event<'_>,
) -> fmt::Result {
let metadata = event.metadata();
let pri = pri_for(*metadata.level());
let timestamp = now_local()
.format(&RFC3164_TIMESTAMP_DAY_PADDED)
.unwrap_or_else(|_| "Jan 1 00:00:00".to_string());
write!(
writer,
"<{pri}>{timestamp} {host} {tag}[{pid}]: {target} ",
pri = pri,
timestamp = timestamp,
host = self.hostname,
tag = TAG,
pid = self.pid,
target = metadata.target(),
)?;
ctx.field_format().format_fields(writer.by_ref(), event)?;
writeln!(writer)
}
}
fn sanitize_sd_value(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for c in value.chars() {
match c {
'\\' | '"' | ']' => {
out.push('\\');
out.push(c);
}
'\n' | '\r' => {}
other => out.push(other),
}
}
out
}
#[cfg(test)]
pub(crate) fn rfc5424_prefix(level: tracing::Level) -> String {
use std::fmt::Write as _;
let mut s = String::new();
let _ = write!(&mut s, "<{}>1 ", pri_for(level));
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pri_table_matches_brief() {
assert_eq!(pri_for(tracing::Level::TRACE), 15);
assert_eq!(pri_for(tracing::Level::DEBUG), 15);
assert_eq!(pri_for(tracing::Level::INFO), 14);
assert_eq!(pri_for(tracing::Level::WARN), 12);
assert_eq!(pri_for(tracing::Level::ERROR), 11);
}
#[test]
fn sd_value_escapes_required_chars() {
assert_eq!(sanitize_sd_value(r#"a "b" c\d ]e"#), r#"a \"b\" c\\d \]e"#);
assert_eq!(sanitize_sd_value("a\nb\rc"), "abc");
}
#[test]
fn rfc5424_prefix_shape() {
let s = rfc5424_prefix(tracing::Level::INFO);
assert_eq!(s, "<14>1 ");
}
}