use super::{util, EventFormatter};
use crate::error::Result;
use crate::model::LogEvent;
use once_cell::sync::Lazy;
use regex::Regex;
use std::fmt::Write;
static PATTERN_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?P<specifier>%(?P<padding>-?\d+)?(?P<converter>[a-zA-Z])(?:\{(?P<options>[^}]+)\})?)|(?P<escaped>%%)")
.expect("Pattern regex should be valid")
});
#[derive(Debug)]
enum Segment {
Literal(String),
Specifier(PatternSpecifier),
}
#[derive(Debug)]
struct PatternSpecifier {
converter: char,
padding: Option<i32>,
options: Option<String>,
}
pub struct PatternFormatter {
segments: Vec<Segment>,
}
impl PatternFormatter {
pub fn new(pattern_string: &str) -> Self {
let segments = Self::parse(pattern_string);
Self { segments }
}
fn parse(pattern: &str) -> Vec<Segment> {
let mut segments = Vec::new();
let mut last_end = 0;
for caps in PATTERN_REGEX.captures_iter(pattern) {
let mat = caps.get(0).unwrap();
if mat.start() > last_end {
segments.push(Segment::Literal(pattern[last_end..mat.start()].to_string()));
}
if caps.name("specifier").is_some() {
let converter = caps
.name("converter")
.unwrap()
.as_str()
.chars()
.next()
.unwrap();
let padding = caps.name("padding").and_then(|m| m.as_str().parse().ok());
let options = caps.name("options").map(|m| m.as_str().to_string());
segments.push(Segment::Specifier(PatternSpecifier {
converter,
padding,
options,
}));
} else if caps.name("escaped").is_some() {
segments.push(Segment::Literal("%".to_string()));
}
last_end = mat.end();
}
if last_end < pattern.len() {
segments.push(Segment::Literal(pattern[last_end..].to_string()));
}
segments
}
fn format_specifier(&self, buf: &mut String, spec: &PatternSpecifier, event: &LogEvent) {
let mut content = String::with_capacity(64);
let needs_padding = spec.padding.is_some();
let target_buf: &mut String = if needs_padding { &mut content } else { buf };
match spec.converter {
'd' => {
if let Some(format_str) = &spec.options {
util::write_timestamp_with_format(target_buf, &event.timestamp, format_str);
} else {
util::write_timestamp(target_buf, &event.timestamp);
}
}
'p' | 'l' => {
let _ = write!(target_buf, "{}", event.level);
}
't' => target_buf.push_str(&event.target),
'm' => {
if let Some(msg) = &event.message {
target_buf.push_str(msg);
}
}
'T' => {
if let Some(name) = &event.thread_name {
target_buf.push_str(name);
}
}
'n' => {
buf.push('\n');
return; }
'X' => {
if let Some(field_name) = &spec.options {
if let Some(log_value) = event.fields.get(field_name) {
let _ = write!(target_buf, "{}", log_value);
}
} else {
if !event.fields.is_empty() {
target_buf.push('{');
let mut sorted_keys: Vec<_> = event
.fields
.keys()
.filter(|k| k.as_str() != "message")
.collect();
sorted_keys.sort();
for (i, key) in sorted_keys.iter().enumerate() {
if let Some(value) = event.fields.get(*key) {
if i > 0 {
target_buf.push_str(", ");
}
let _ = write!(target_buf, "{}={}", key, value);
}
}
target_buf.push('}');
}
}
}
_ => {}
}
if let Some(padding) = spec.padding {
self.apply_padding(buf, &content, padding);
}
}
fn apply_padding(&self, buf: &mut String, content: &str, padding: i32) {
let width = padding.abs() as usize;
if content.len() >= width {
buf.push_str(content);
return;
}
if padding > 0 {
let _ = write!(buf, "{:>width$}", content, width = width);
} else {
let _ = write!(buf, "{:<width$}", content, width = width);
}
}
}
const KNOWN_CONVERTERS: &[char] = &['d', 'p', 'l', 't', 'm', 'T', 'n', 'X'];
pub(crate) fn validate_pattern(pattern: &str) -> std::result::Result<(), String> {
for segment in PatternFormatter::parse(pattern) {
if let Segment::Specifier(spec) = segment {
if !KNOWN_CONVERTERS.contains(&spec.converter) {
return Err(format!(
"Unknown conversion specifier '%{}'. Known specifiers: %d, %p, %l, %t, %m, %T, %n, %X.",
spec.converter
));
}
if spec.converter == 'd' {
if let Some(format_str) = &spec.options {
let mut probe = String::new();
if write!(
probe,
"{}",
chrono::Utc::now().format(format_str)
)
.is_err()
{
return Err(format!(
"Invalid date format string '{}' in %d{{...}}.",
format_str
));
}
}
}
}
}
Ok(())
}
impl EventFormatter for PatternFormatter {
fn format_event(&self, event: &LogEvent) -> Result<Vec<u8>> {
let mut output = String::with_capacity(256);
for segment in &self.segments {
match segment {
Segment::Literal(text) => {
output.push_str(text);
}
Segment::Specifier(spec) => {
self.format_specifier(&mut output, spec, event);
}
}
}
if !output.ends_with('\n') {
output.push('\n');
}
Ok(output.into_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::LogEvent;
use chrono::{TimeZone, Utc};
use tracing::Level;
fn create_test_event() -> LogEvent {
let mut event = LogEvent::new(
Level::INFO,
"test_target",
"test_event",
Some("This is a test message.".to_string()),
);
event.timestamp = Utc.with_ymd_and_hms(2023, 1, 1, 1, 1, 1).unwrap();
event.thread_name = Some("main".to_string());
event
}
#[test]
fn format_with_standard_pattern() {
let formatter = PatternFormatter::new("[%d] %p %t - %m%n");
let event = create_test_event();
let formatted_str = String::from_utf8(formatter.format_event(&event).unwrap()).unwrap();
assert_eq!(
formatted_str,
"[2023-01-01T01:01:01.000Z] INFO test_target - This is a test message.\n"
);
}
#[test]
fn format_with_custom_date_format() {
let formatter = PatternFormatter::new("[%d{%Y-%m-%d %H:%M}] %p - %m");
let event = create_test_event();
let formatted_str = String::from_utf8(formatter.format_event(&event).unwrap()).unwrap();
assert_eq!(
formatted_str.trim(),
"[2023-01-01 01:01] INFO - This is a test message."
);
}
#[test]
fn format_with_padding() {
let formatter = PatternFormatter::new("[%5p] %-8t [%-6T] %m");
let event = create_test_event();
let formatted_str = String::from_utf8(formatter.format_event(&event).unwrap()).unwrap();
assert_eq!(
formatted_str.trim(),
"[ INFO] test_target [main ] This is a test message."
);
}
#[test]
fn format_handles_escaped_percent() {
let formatter = PatternFormatter::new("100%% %p");
let event = create_test_event();
let formatted_str = String::from_utf8(formatter.format_event(&event).unwrap()).unwrap();
assert_eq!(formatted_str.trim(), "100% INFO");
}
#[test]
fn x_specifier_separators_with_message_field_present() {
use crate::model::LogValue;
let formatter = PatternFormatter::new("%X");
let mut event = create_test_event();
event.fields.insert(
"alpha".to_string(),
LogValue::String("1".to_string()),
);
event.fields.insert(
"message".to_string(),
LogValue::String("dup".to_string()),
);
event
.fields
.insert("zeta".to_string(), LogValue::String("2".to_string()));
let out = String::from_utf8(formatter.format_event(&event).unwrap()).unwrap();
assert_eq!(out.trim(), "{alpha=1, zeta=2}");
let mut event = create_test_event();
event.fields.insert(
"message".to_string(),
LogValue::String("dup".to_string()),
);
let out = String::from_utf8(formatter.format_event(&event).unwrap()).unwrap();
assert_eq!(out.trim(), "{}");
let mut event = create_test_event();
event
.fields
.insert("a".to_string(), LogValue::Int(1));
event
.fields
.insert("b".to_string(), LogValue::Int(2));
let out = String::from_utf8(formatter.format_event(&event).unwrap()).unwrap();
assert_eq!(out.trim(), "{a=1, b=2}");
}
#[test]
fn validate_pattern_accepts_known_and_rejects_unknown() {
assert!(validate_pattern("[%d] %-5p %t - %m %X %T%n 100%%").is_ok());
assert!(validate_pattern("[%d{%Y-%m-%d %H:%M}] %m").is_ok());
let err = validate_pattern("%q %m").unwrap_err();
assert!(err.contains("%q"), "error should name the bad specifier: {}", err);
assert!(validate_pattern("%d{%Q-bogus} %m").is_err());
}
#[test]
fn parse_handles_literals_and_specifiers() {
let formatter = PatternFormatter::new("LITERAL %-10p AND %m");
assert_eq!(formatter.segments.len(), 4);
assert!(matches!(&formatter.segments[0], Segment::Literal(s) if s == "LITERAL "));
assert!(
matches!(&formatter.segments[1], Segment::Specifier(spec) if spec.converter == 'p' && spec.padding == Some(-10))
);
assert!(matches!(&formatter.segments[2], Segment::Literal(s) if s == " AND "));
assert!(
matches!(&formatter.segments[3], Segment::Specifier(spec) if spec.converter == 'm' && spec.padding.is_none())
);
}
}