#![allow(dead_code)]
use core::fmt::Write;
use crate::decode::{DecodedArg, LogRecord};
pub trait Formatter: Send + Sync {
fn format(&self, record: &LogRecord, out: &mut String);
}
#[derive(Debug)]
pub enum InvalidPatternError {
UnknownField(String),
UnclosedBrace,
InvalidFormatSpec(String),
}
impl core::fmt::Display for InvalidPatternError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnknownField(name) => write!(
f,
"unknown pattern field \"{name}\"; \
known fields: level, secs, millis, file, line, module, logger, message",
),
Self::UnclosedBrace => f.write_str("unclosed '{' in pattern string"),
Self::InvalidFormatSpec(spec) => write!(
f,
"invalid format spec \"{spec}\"; \
expected [[fill]align]['0'][width] where align is '<', '>', or '^' \
and width is a non-negative integer",
),
}
}
}
impl std::error::Error for InvalidPatternError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Align {
Left,
Right,
Center,
}
#[derive(Debug, Clone)]
struct FormatSpec {
fill: char,
align: Option<Align>,
zero_pad: bool,
width: usize,
}
impl Default for FormatSpec {
fn default() -> Self {
Self {
fill: ' ',
align: None,
zero_pad: false,
width: 0,
}
}
}
#[derive(Debug, Clone, Copy)]
enum FieldKind {
Level,
Secs,
Millis,
File,
Line,
Module,
Logger,
Message,
}
impl FieldKind {
const fn is_numeric(self) -> bool {
matches!(self, Self::Secs | Self::Millis | Self::Line)
}
}
#[derive(Debug, Clone)]
struct ParsedField {
kind: FieldKind,
spec: FormatSpec,
}
#[derive(Debug, Clone)]
enum PatternElement {
Literal(String),
Field(ParsedField),
}
#[derive(Debug, Clone)]
pub struct PatternFormatter {
elements: Vec<PatternElement>,
}
impl PatternFormatter {
pub const DEFAULT_PATTERN: &'static str =
"[{level} {secs}.{millis:03}] {file}:{line} {message}";
pub fn new(pattern: &str) -> Result<Self, InvalidPatternError> {
Ok(Self {
elements: parse_pattern(pattern)?,
})
}
}
impl Default for PatternFormatter {
fn default() -> Self {
Self {
elements: parse_pattern(Self::DEFAULT_PATTERN)
.expect("DEFAULT_PATTERN is always valid"),
}
}
}
impl Formatter for PatternFormatter {
fn format(&self, record: &LogRecord, out: &mut String) {
let secs = record.timestamp_ns / 1_000_000_000;
let millis = (record.timestamp_ns % 1_000_000_000) / 1_000_000;
for element in &self.elements {
match element {
PatternElement::Literal(s) => out.push_str(s),
PatternElement::Field(pf) => {
let value = render_field(pf.kind, record, secs, millis);
apply_format_spec(out, &value, &pf.spec, pf.kind.is_numeric());
}
}
}
}
}
fn render_field(kind: FieldKind, record: &LogRecord, secs: u64, millis: u64) -> String {
let mut s = String::new();
match kind {
FieldKind::Level => {
let _ = write!(s, "{}", record.metadata.level);
}
FieldKind::Secs => {
let _ = write!(s, "{secs}");
}
FieldKind::Millis => {
let _ = write!(s, "{millis}");
}
FieldKind::File => s.push_str(record.metadata.file),
FieldKind::Line => {
let _ = write!(s, "{}", record.metadata.line);
}
FieldKind::Module => s.push_str(record.metadata.module_path),
FieldKind::Logger => s.push_str(&record.logger_name),
FieldKind::Message => {
format_message(&mut s, record.metadata.fmt_str, &record.args);
}
}
s
}
fn parse_pattern(pattern: &str) -> Result<Vec<PatternElement>, InvalidPatternError> {
let mut elements: Vec<PatternElement> = Vec::new();
let mut literal = String::new();
let mut chars = pattern.chars();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut token = String::new();
let mut closed = false;
for c in chars.by_ref() {
if c == '}' {
closed = true;
break;
}
token.push(c);
}
if !closed {
return Err(InvalidPatternError::UnclosedBrace);
}
let (field_name, spec_str) = token.find(':').map_or((token.as_str(), ""), |pos| {
(&token[..pos], &token[pos + 1..])
});
let kind = parse_field_kind(field_name)?;
let spec = parse_format_spec(spec_str)?;
if !literal.is_empty() {
elements.push(PatternElement::Literal(core::mem::take(&mut literal)));
}
elements.push(PatternElement::Field(ParsedField { kind, spec }));
} else {
literal.push(ch);
}
}
if !literal.is_empty() {
elements.push(PatternElement::Literal(literal));
}
Ok(elements)
}
fn parse_field_kind(name: &str) -> Result<FieldKind, InvalidPatternError> {
match name {
"level" => Ok(FieldKind::Level),
"secs" => Ok(FieldKind::Secs),
"millis" => Ok(FieldKind::Millis),
"file" => Ok(FieldKind::File),
"line" => Ok(FieldKind::Line),
"module" => Ok(FieldKind::Module),
"logger" => Ok(FieldKind::Logger),
"message" => Ok(FieldKind::Message),
other => Err(InvalidPatternError::UnknownField(other.to_owned())),
}
}
fn parse_format_spec(spec: &str) -> Result<FormatSpec, InvalidPatternError> {
if spec.is_empty() {
return Ok(FormatSpec::default());
}
let mut chars = spec.chars().peekable();
let mut fill = ' ';
let mut align: Option<Align> = None;
let mut probe = spec.chars();
let first = probe.next();
let second = probe.next();
if let (Some(f), Some(a)) = (first, second.and_then(to_align)) {
fill = f;
align = Some(a);
chars.next(); chars.next(); } else if let Some(a) = first.and_then(to_align) {
align = Some(a);
chars.next(); }
let zero_pad = if chars.peek() == Some(&'0') {
chars.next();
true
} else {
false
};
let rest: String = chars.collect();
let width = if rest.is_empty() {
0
} else {
rest.parse::<usize>()
.map_err(|_| InvalidPatternError::InvalidFormatSpec(spec.to_owned()))?
};
Ok(FormatSpec {
fill,
align,
zero_pad,
width,
})
}
const fn to_align(c: char) -> Option<Align> {
match c {
'<' => Some(Align::Left),
'>' => Some(Align::Right),
'^' => Some(Align::Center),
_ => None,
}
}
fn apply_format_spec(buf: &mut String, value: &str, spec: &FormatSpec, is_numeric: bool) {
if spec.width == 0 {
buf.push_str(value);
return;
}
let value_len = value.chars().count();
if value_len >= spec.width {
buf.push_str(value);
return;
}
let padding = spec.width - value_len;
let (fill, align) = if spec.zero_pad {
('0', Align::Right)
} else {
let align = spec.align.unwrap_or(if is_numeric {
Align::Right
} else {
Align::Left
});
(spec.fill, align)
};
match align {
Align::Left => {
buf.push_str(value);
for _ in 0..padding {
buf.push(fill);
}
}
Align::Right => {
for _ in 0..padding {
buf.push(fill);
}
buf.push_str(value);
}
Align::Center => {
let left_pad = padding / 2;
let right_pad = padding - left_pad;
for _ in 0..left_pad {
buf.push(fill);
}
buf.push_str(value);
for _ in 0..right_pad {
buf.push(fill);
}
}
}
}
fn format_message(buf: &mut String, fmt_str: &str, args: &[DecodedArg]) {
let mut arg_iter = args.iter();
let mut chars = fmt_str.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' && chars.peek() == Some(&'}') {
chars.next();
if let Some(arg) = arg_iter.next() {
let _ = write!(buf, "{arg}");
} else {
buf.push_str("{}");
}
} else {
buf.push(ch);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decode::{DecodedArg, LogRecord};
use crate::level::LogLevel;
use crate::metadata::LogMetadata;
static META_INFO: LogMetadata = LogMetadata {
level: LogLevel::Info,
fmt_str: "msg",
file: "f.rs",
line: 1,
module_path: "m",
arg_count: 0,
};
static META_WARN: LogMetadata = LogMetadata {
level: LogLevel::Warning,
fmt_str: "msg",
file: "f.rs",
line: 1,
module_path: "m",
arg_count: 0,
};
static FMT_TWO: LogMetadata = LogMetadata {
level: LogLevel::Info,
fmt_str: "hello {} world {}",
file: "test.rs",
line: 42,
module_path: "test",
arg_count: 2,
};
static FMT_NONE: LogMetadata = LogMetadata {
level: LogLevel::Warning,
fmt_str: "simple message",
file: "lib.rs",
line: 1,
module_path: "test",
arg_count: 0,
};
static FMT_ONE: LogMetadata = LogMetadata {
level: LogLevel::Info,
fmt_str: "x={}",
file: "f.rs",
line: 0,
module_path: "test",
arg_count: 1,
};
static FMT_LITERAL_BRACE: LogMetadata = LogMetadata {
level: LogLevel::Info,
fmt_str: "open {brace x={}",
file: "f.rs",
line: 0,
module_path: "test",
arg_count: 1,
};
static FMT_THREE_PLACEHOLDERS: LogMetadata = LogMetadata {
level: LogLevel::Info,
fmt_str: "{} {} {}",
file: "f.rs",
line: 0,
module_path: "test",
arg_count: 3,
};
fn bare(meta: &'static LogMetadata) -> LogRecord {
LogRecord {
timestamp_ns: 0,
logger_name: "test".to_owned(),
metadata: meta,
args: vec![],
}
}
fn with_ts(meta: &'static LogMetadata, ts_ns: u64) -> LogRecord {
LogRecord {
timestamp_ns: ts_ns,
logger_name: "test".to_owned(),
metadata: meta,
args: vec![],
}
}
fn fmt(pattern: &str, record: &LogRecord) -> String {
let f = PatternFormatter::new(pattern).expect("valid pattern");
let mut out = String::new();
f.format(record, &mut out);
out
}
#[test]
fn spec_empty_is_valid() {
PatternFormatter::new("{level:}").unwrap();
}
#[test]
fn spec_just_width_is_valid() {
PatternFormatter::new("{level:10}").unwrap();
}
#[test]
fn spec_left_align_is_valid() {
PatternFormatter::new("{level:<10}").unwrap();
}
#[test]
fn spec_right_align_is_valid() {
PatternFormatter::new("{level:>10}").unwrap();
}
#[test]
fn spec_center_align_is_valid() {
PatternFormatter::new("{level:^10}").unwrap();
}
#[test]
fn spec_fill_and_align_is_valid() {
PatternFormatter::new("{level:*>10}").unwrap();
}
#[test]
fn spec_zero_pad_is_valid() {
PatternFormatter::new("{millis:03}").unwrap();
}
#[test]
fn spec_zero_pad_no_width_is_valid() {
PatternFormatter::new("{millis:0}").unwrap();
}
#[test]
fn spec_invalid_non_numeric_width() {
assert!(matches!(
PatternFormatter::new("{level:abc}"),
Err(InvalidPatternError::InvalidFormatSpec(_)),
));
}
#[test]
fn spec_invalid_align_then_non_numeric_width() {
assert!(matches!(
PatternFormatter::new("{level:>abc}"),
Err(InvalidPatternError::InvalidFormatSpec(_)),
));
}
#[test]
fn spec_invalid_lone_fill_without_align() {
assert!(matches!(
PatternFormatter::new("{level:*}"),
Err(InvalidPatternError::InvalidFormatSpec(_)),
));
}
#[test]
fn spec_right_align_pads_left_with_spaces() {
assert_eq!(fmt("{level:>8}", &bare(&META_INFO)), " INFO");
}
#[test]
fn spec_left_align_pads_right_with_spaces() {
assert_eq!(fmt("{level:<8}", &bare(&META_INFO)), "INFO ");
}
#[test]
fn spec_center_align_even_padding() {
assert_eq!(fmt("{level:^8}", &bare(&META_INFO)), " INFO ");
}
#[test]
fn spec_center_align_odd_padding_goes_right() {
assert_eq!(fmt("{level:^9}", &bare(&META_INFO)), " INFO ");
}
#[test]
fn spec_custom_fill_char() {
assert_eq!(fmt("{level:*>8}", &bare(&META_INFO)), "****INFO");
}
#[test]
fn spec_custom_fill_left() {
assert_eq!(fmt("{level:.<8}", &bare(&META_INFO)), "INFO....");
}
#[test]
fn spec_custom_fill_middle() {
assert_eq!(fmt("{level:.^12}", &bare(&META_INFO)), "....INFO....");
}
#[test]
fn spec_zero_pad_pads_left_with_zeros() {
assert_eq!(fmt("{millis:03}", &with_ts(&META_INFO, 7_000_000)), "007");
}
#[test]
fn spec_zero_pad_wider_value_not_truncated() {
assert_eq!(
fmt("{millis:03}", &with_ts(&META_INFO, 1_234_000_000)),
"234"
);
}
#[test]
fn spec_value_wider_than_width_is_not_truncated() {
assert_eq!(fmt("{level:>2}", &bare(&META_INFO)), "INFO");
}
#[test]
fn spec_numeric_field_defaults_to_right_align() {
assert_eq!(fmt("{line:5}", &bare(&META_INFO)), " 1");
}
#[test]
fn spec_string_field_defaults_to_left_align() {
assert_eq!(fmt("{level:8}", &bare(&META_INFO)), "INFO ");
}
#[test]
fn spec_empty_is_no_op() {
assert_eq!(fmt("{level:}", &bare(&META_INFO)), "INFO");
}
#[test]
fn formatter_trait_is_dyn_compatible() {
let _: &dyn Formatter = &PatternFormatter::default();
}
#[test]
fn pattern_formatter_is_send_and_sync() {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<PatternFormatter>();
}
#[test]
fn new_rejects_unknown_field() {
assert!(matches!(
PatternFormatter::new("{typo}"),
Err(InvalidPatternError::UnknownField(ref s)) if s == "typo"
));
}
#[test]
fn new_rejects_unclosed_brace() {
assert!(matches!(
PatternFormatter::new("hello {level"),
Err(InvalidPatternError::UnclosedBrace),
));
}
#[test]
fn new_rejects_empty_field_name() {
assert!(matches!(
PatternFormatter::new("{}"),
Err(InvalidPatternError::UnknownField(ref s)) if s.is_empty()
));
}
#[test]
fn default_pattern_is_valid() {
assert!(PatternFormatter::new(PatternFormatter::DEFAULT_PATTERN).is_ok());
}
#[test]
fn pattern_format_basic_with_args() {
let r = LogRecord {
timestamp_ns: 1_700_000_000_123_000_000,
logger_name: "test".to_owned(),
metadata: &FMT_TWO,
args: vec![DecodedArg::Str("alice".to_owned()), DecodedArg::U64(99)],
};
let mut out = String::new();
PatternFormatter::default().format(&r, &mut out);
assert_eq!(out, "[INFO 1700000000.123] test.rs:42 hello alice world 99");
}
#[test]
fn pattern_format_no_args() {
let r = LogRecord {
timestamp_ns: 0,
logger_name: "test".to_owned(),
metadata: &FMT_NONE,
args: vec![],
};
let mut out = String::new();
PatternFormatter::default().format(&r, &mut out);
assert_eq!(out, "[WARNING 0.000] lib.rs:1 simple message");
}
#[test]
fn pattern_format_pads_millis_to_three_digits() {
let r = LogRecord {
timestamp_ns: 5_007_000_000,
logger_name: "test".to_owned(),
metadata: &FMT_NONE,
args: vec![],
};
let mut out = String::new();
PatternFormatter::default().format(&r, &mut out);
assert!(out.contains("5.007"), "out={out:?}");
}
#[test]
fn pattern_format_renders_seconds_and_millis_from_full_timestamp() {
let r = LogRecord {
timestamp_ns: 1_700_000_000_456_000_000,
logger_name: "test".to_owned(),
metadata: &FMT_NONE,
args: vec![],
};
let mut out = String::new();
PatternFormatter::default().format(&r, &mut out);
assert!(out.contains("1700000000.456"), "out={out:?}");
}
#[test]
fn pattern_format_extra_placeholders_left_as_is() {
let r = LogRecord {
timestamp_ns: 0,
logger_name: "test".to_owned(),
metadata: &FMT_THREE_PLACEHOLDERS,
args: vec![DecodedArg::U32(7)],
};
let mut out = String::new();
PatternFormatter::default().format(&r, &mut out);
assert!(out.ends_with("7 {} {}"), "out={out:?}");
}
#[test]
fn pattern_format_extra_args_ignored() {
let r = LogRecord {
timestamp_ns: 0,
logger_name: "test".to_owned(),
metadata: &FMT_ONE,
args: vec![
DecodedArg::U32(7),
DecodedArg::Str("ignored".to_owned()),
DecodedArg::Bool(true),
],
};
let mut out = String::new();
PatternFormatter::default().format(&r, &mut out);
assert!(out.ends_with("x=7"), "out={out:?}");
assert!(!out.contains("ignored"), "out={out:?}");
}
#[test]
fn pattern_format_brace_without_close_is_literal() {
let r = LogRecord {
timestamp_ns: 0,
logger_name: "test".to_owned(),
metadata: &FMT_LITERAL_BRACE,
args: vec![DecodedArg::U32(3)],
};
let mut out = String::new();
PatternFormatter::default().format(&r, &mut out);
assert!(out.contains("open {brace x=3"), "out={out:?}");
}
#[test]
fn pattern_format_appends_to_existing_buffer() {
let r = LogRecord {
timestamp_ns: 0,
logger_name: "test".to_owned(),
metadata: &FMT_NONE,
args: vec![],
};
let mut out = String::from("PREFIX|");
PatternFormatter::default().format(&r, &mut out);
assert!(out.starts_with("PREFIX|"), "out={out:?}");
assert!(out.contains("simple message"), "out={out:?}");
}
#[test]
fn pattern_format_renders_various_arg_types() {
static META: LogMetadata = LogMetadata {
level: LogLevel::Debug,
fmt_str: "i={} u={} f={} b={} s={} c={}",
file: "f.rs",
line: 0,
module_path: "test",
arg_count: 6,
};
let r = LogRecord {
timestamp_ns: 0,
logger_name: "test".to_owned(),
metadata: &META,
args: vec![
DecodedArg::I64(-42),
DecodedArg::U32(7),
DecodedArg::F64(1.5),
DecodedArg::Bool(true),
DecodedArg::Str("hi".to_owned()),
DecodedArg::Custom("rgb(1, 2, 3)".to_owned()),
],
};
let mut out = String::new();
PatternFormatter::default().format(&r, &mut out);
assert!(
out.contains("i=-42 u=7 f=1.5 b=true s=hi c=rgb(1, 2, 3)"),
"out={out:?}",
);
}
#[test]
fn pattern_format_custom_pattern_all_fields() {
let f = PatternFormatter::new(
"{level}|{secs}|{millis}|{file}|{line}|{module}|{logger}|{message}",
)
.unwrap();
let r = LogRecord {
timestamp_ns: 2_001_000_000,
logger_name: "payments".to_owned(),
metadata: &FMT_ONE,
args: vec![DecodedArg::U32(42)],
};
let mut out = String::new();
f.format(&r, &mut out);
assert_eq!(out, "INFO|2|1|f.rs|0|test|payments|x=42");
}
#[test]
fn pattern_format_logger_field_renders_name() {
let r = LogRecord {
timestamp_ns: 0,
logger_name: "payments".to_owned(),
metadata: &META_INFO,
args: vec![],
};
assert_eq!(fmt("{logger}", &r), "payments");
}
#[test]
fn pattern_format_logger_field_pads_left() {
let r = LogRecord {
timestamp_ns: 0,
logger_name: "app".to_owned(),
metadata: &META_INFO,
args: vec![],
};
assert_eq!(fmt("{logger:<10}", &r), "app ");
}
#[test]
fn new_rejects_unknown_field_logger_typo() {
assert!(matches!(
PatternFormatter::new("{loggerr}"),
Err(InvalidPatternError::UnknownField(ref s)) if s == "loggerr"
));
}
#[test]
fn pattern_format_custom_pattern_literal_text() {
let f = PatternFormatter::new("level={level} msg={message}").unwrap();
let r = LogRecord {
timestamp_ns: 0,
logger_name: "test".to_owned(),
metadata: &FMT_NONE,
args: vec![],
};
let mut out = String::new();
f.format(&r, &mut out);
assert_eq!(out, "level=WARNING msg=simple message");
}
#[test]
fn pattern_format_warning_level() {
assert_eq!(fmt("{level}", &bare(&META_WARN)), "WARNING");
}
}