use chrono::{Local, Utc};
use derive_more::Debug;
use log::{Level, Record};
use std::{default::Default, io, mem, process, thread};
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
use crate::encode::{
self,
pattern::parser::{Alignment, Parameters, Parser, Piece},
Color, Encode, Style, NEWLINE,
};
#[cfg(feature = "config_parsing")]
use crate::config::{Deserialize, Deserializers};
use self::parser::Formatter;
mod parser;
thread_local!(
static TID: usize = thread_id::get()
);
#[cfg(feature = "config_parsing")]
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PatternEncoderConfig {
pattern: Option<String>,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
enum Chunk {
Text(String),
Formatted {
chunk: FormattedChunk,
params: Parameters,
},
Error(String),
}
impl Chunk {
fn encode(&self, w: &mut dyn encode::Write, record: &Record) -> io::Result<()> {
match *self {
Chunk::Text(ref s) => w.write_all(s.as_bytes()),
Chunk::Formatted {
ref chunk,
ref params,
} => match (params.min_width, params.max_width) {
(None, None) => chunk.encode(w, record),
_ => {
let mut w = StringBasedWriter::new(w, params);
chunk.encode(&mut w, record)?;
w.chunk_end()
}
},
Chunk::Error(ref s) => write!(w, "{{ERROR: {}}}", s),
}
}
}
impl<'a> From<Piece<'a>> for Chunk {
fn from(piece: Piece<'a>) -> Chunk {
match piece {
Piece::Text(text) => Chunk::Text(text.to_owned()),
Piece::Argument {
mut formatter,
parameters,
} => match formatter.name {
"d" | "date" => {
if formatter.args.len() > 2 {
return Chunk::Error("expected at most two arguments".to_owned());
}
let format = match formatter.args.first() {
Some(arg) => {
let mut format = String::new();
for piece in arg {
match *piece {
Piece::Text(text) => format.push_str(text),
Piece::Argument { .. } => {
format.push_str("{ERROR: unexpected formatter}");
}
Piece::Error(ref err) => {
format.push_str("{ERROR: ");
format.push_str(err);
format.push('}');
}
}
}
format
}
None => "%+".to_owned(),
};
let timezone = match formatter.args.get(1) {
Some(arg) => {
if let Some(arg) = arg.first() {
match *arg {
Piece::Text("utc") => Timezone::Utc,
Piece::Text("local") => Timezone::Local,
Piece::Text(z) => {
return Chunk::Error(format!("invalid timezone `{}`", z));
}
_ => return Chunk::Error("invalid timezone".to_owned()),
}
} else {
return Chunk::Error("invalid timezone".to_owned());
}
}
None => Timezone::Local,
};
Chunk::Formatted {
chunk: FormattedChunk::Time(format, timezone),
params: parameters,
}
}
"h" | "highlight" => {
if formatter.args.len() != 1 {
return Chunk::Error("expected exactly one argument".to_owned());
}
let chunks = formatter
.args
.pop()
.unwrap()
.into_iter()
.map(From::from)
.collect();
Chunk::Formatted {
chunk: FormattedChunk::Highlight(chunks),
params: parameters,
}
}
"D" | "debug" => {
if formatter.args.len() != 1 {
return Chunk::Error("expected exactly one argument".to_owned());
}
let chunks = formatter
.args
.pop()
.unwrap()
.into_iter()
.map(From::from)
.collect();
Chunk::Formatted {
chunk: FormattedChunk::Debug(chunks),
params: parameters,
}
}
"R" | "release" => {
if formatter.args.len() != 1 {
return Chunk::Error("expected exactly one argument".to_owned());
}
let chunks = formatter
.args
.pop()
.unwrap()
.into_iter()
.map(From::from)
.collect();
Chunk::Formatted {
chunk: FormattedChunk::Release(chunks),
params: parameters,
}
}
"l" | "level" => no_args(&formatter.args, parameters, FormattedChunk::Level),
"m" | "message" => no_args(&formatter.args, parameters, FormattedChunk::Message),
"M" | "module" => no_args(&formatter.args, parameters, FormattedChunk::Module),
"n" => no_args(&formatter.args, parameters, FormattedChunk::Newline),
"f" | "file" => no_args(&formatter.args, parameters, FormattedChunk::File),
"L" | "line" => no_args(&formatter.args, parameters, FormattedChunk::Line),
"T" | "thread" => no_args(&formatter.args, parameters, FormattedChunk::Thread),
"I" | "thread_id" => no_args(&formatter.args, parameters, FormattedChunk::ThreadId),
"P" | "pid" => no_args(&formatter.args, parameters, FormattedChunk::ProcessId),
"i" | "tid" => no_args(&formatter.args, parameters, FormattedChunk::SystemThreadId),
"t" | "target" => no_args(&formatter.args, parameters, FormattedChunk::Target),
"X" | "mdc" => match kv_parsing(&formatter) {
Err(e) => Chunk::Error(format!("MDC: {e}")),
Ok((key, default)) => Chunk::Formatted {
chunk: FormattedChunk::Mdc(key, default),
params: parameters,
},
},
#[cfg(feature = "log_kv")]
"K" | "key_value" => match kv_parsing(&formatter) {
Err(e) => Chunk::Error(format!("key_value: {e}")),
Ok((key, default)) => Chunk::Formatted {
chunk: FormattedChunk::Kv(key, default),
params: parameters,
},
},
#[cfg(not(feature = "log_kv"))]
"K" | "key_value" => Chunk::Error(
"The log_kv feature is required to parse the key_value argument".to_owned(),
),
"" => {
if formatter.args.len() != 1 {
return Chunk::Error("expected exactly one argument".to_owned());
}
let chunks = formatter
.args
.pop()
.unwrap()
.into_iter()
.map(From::from)
.collect();
Chunk::Formatted {
chunk: FormattedChunk::Align(chunks),
params: parameters,
}
}
name => Chunk::Error(format!("unknown formatter `{}`", name)),
},
Piece::Error(err) => Chunk::Error(err),
}
}
}
enum StringOrStyle {
String { glen: usize, s: String }, Style(Style),
}
struct StringBasedWriter<'writer, 'params> {
buf: Vec<u8>,
strings_and_styles: Vec<StringOrStyle>,
w: &'writer mut dyn encode::Write,
params: &'params Parameters,
}
impl encode::Write for StringBasedWriter<'_, '_> {
fn set_style(&mut self, style: &Style) -> io::Result<()> {
self.push_string();
self.strings_and_styles
.push(StringOrStyle::Style(style.clone()));
Ok(())
}
}
impl io::Write for StringBasedWriter<'_, '_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buf.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'writer, 'params> StringBasedWriter<'writer, 'params> {
fn new(w: &'writer mut dyn encode::Write, params: &'params Parameters) -> Self {
StringBasedWriter {
buf: Vec::new(),
strings_and_styles: Vec::new(),
w,
params,
}
}
fn push_string(&mut self) {
if !self.buf.is_empty() {
let old_buf = mem::take(&mut self.buf);
let s = String::from_utf8_lossy(&old_buf[..]).into_owned();
let glen = s.graphemes(true).count();
self.strings_and_styles
.push(StringOrStyle::String { glen, s });
}
}
fn chunk_end(&mut self) -> io::Result<()> {
self.push_string();
let total_width = self.compute_width();
let mut done = false;
if let Some(max_width) = self.params.max_width {
if total_width > max_width {
if self.params.right_truncate {
self.output_right_truncate(max_width)?;
} else {
self.output_left_truncate(total_width, max_width)?;
}
done = true;
}
}
if let Some(min_width) = self.params.min_width {
if total_width < min_width {
if self.params.align == Alignment::Left {
self.output_everything()?;
self.output_padding(min_width - total_width)?;
} else {
self.output_padding(min_width - total_width)?;
self.output_everything()?;
}
done = true;
}
}
if !done {
self.output_everything()?;
}
Ok(())
}
fn compute_width(&self) -> usize {
let mut size = 0;
for x in &self.strings_and_styles {
if let StringOrStyle::String { glen, s: _ } = x {
size += glen;
}
}
size
}
fn output_left_truncate(&mut self, total_width: usize, max_width: usize) -> io::Result<()> {
let mut to_cut = total_width - max_width;
for x in &self.strings_and_styles {
match x {
StringOrStyle::String { glen, s } => {
if to_cut == 0 {
self.w.write_all(s.as_bytes())?;
} else if *glen <= to_cut {
to_cut -= glen;
} else {
let start = Self::boundary_or(s, to_cut, 0);
self.w.write_all(&s.as_bytes()[start..])?;
to_cut = 0;
}
}
StringOrStyle::Style(s) => self.w.set_style(s)?,
}
}
Ok(())
}
fn boundary_or(s: &str, count: usize, or: usize) -> usize {
let mut cursor = GraphemeCursor::new(0, s.len(), true);
let mut start = 0;
for _i in 0..count {
let r = cursor.next_boundary(s, 0);
if let Ok(Some(x)) = r {
start = x;
} else {
start = or;
break;
}
}
start
}
fn output_right_truncate(&mut self, mut max_width: usize) -> io::Result<()> {
for x in &self.strings_and_styles {
match x {
StringOrStyle::String { glen, s } => {
if *glen <= max_width {
self.w.write_all(s.as_bytes())?;
max_width -= glen;
} else {
let end = Self::boundary_or(s, max_width, s.len());
self.w.write_all(&s.as_bytes()[0..end])?;
max_width = 0;
}
if max_width == 0 {
break;
}
}
StringOrStyle::Style(s) => self.w.set_style(s)?,
}
}
Ok(())
}
fn output_everything(&mut self) -> io::Result<()> {
for x in &self.strings_and_styles {
match x {
StringOrStyle::String { glen: _, s } => self.w.write_all(s.as_bytes())?,
StringOrStyle::Style(s) => self.w.set_style(s)?,
}
}
Ok(())
}
fn output_padding(&mut self, len: usize) -> io::Result<()> {
for _i in 0..len {
write!(self.w, "{}", self.params.fill)?;
}
Ok(())
}
}
fn no_args(arg: &[Vec<Piece>], params: Parameters, chunk: FormattedChunk) -> Chunk {
if arg.is_empty() {
Chunk::Formatted { chunk, params }
} else {
Chunk::Error("unexpected arguments".to_owned())
}
}
fn kv_parsing<'a>(formatter: &'a Formatter) -> Result<(String, String), &'a str> {
if formatter.args.len() > 2 {
return Err("expected at most two arguments");
}
let key = match formatter.args.first() {
Some(arg) => {
if let Some(arg) = arg.first() {
match arg {
Piece::Text(key) => key.to_owned(),
Piece::Error(ref e) => return Err(e),
_ => return Err("invalid key"),
}
} else {
return Err("invalid key");
}
}
None => return Err("missing key"),
};
let default = match formatter.args.get(1) {
Some(arg) => {
if let Some(arg) = arg.first() {
match arg {
Piece::Text(key) => key.to_owned(),
Piece::Error(ref e) => return Err(e),
_ => return Err("invalid default"),
}
} else {
return Err("invalid default");
}
}
None => "",
};
Ok((key.into(), default.into()))
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
enum Timezone {
Utc,
Local,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
enum FormattedChunk {
Time(String, Timezone),
Level,
Message,
Module,
File,
Line,
Thread,
ThreadId,
ProcessId,
SystemThreadId,
Target,
Newline,
Align(Vec<Chunk>),
Highlight(Vec<Chunk>),
Debug(Vec<Chunk>),
Release(Vec<Chunk>),
Mdc(String, String),
#[cfg(feature = "log_kv")]
Kv(String, String),
}
impl FormattedChunk {
fn encode(&self, w: &mut dyn encode::Write, record: &Record) -> io::Result<()> {
match *self {
FormattedChunk::Time(ref fmt, Timezone::Utc) => write!(w, "{}", Utc::now().format(fmt)),
FormattedChunk::Time(ref fmt, Timezone::Local) => {
write!(w, "{}", Local::now().format(fmt))
}
FormattedChunk::Level => write!(w, "{}", record.level()),
FormattedChunk::Message => w.write_fmt(*record.args()),
FormattedChunk::Module => w.write_all(record.module_path().unwrap_or("???").as_bytes()),
FormattedChunk::File => w.write_all(record.file().unwrap_or("???").as_bytes()),
FormattedChunk::Line => match record.line() {
Some(line) => write!(w, "{}", line),
None => w.write_all(b"???"),
},
FormattedChunk::Thread => {
w.write_all(thread::current().name().unwrap_or("unnamed").as_bytes())
}
FormattedChunk::ThreadId => w.write_all(thread_id::get().to_string().as_bytes()),
FormattedChunk::ProcessId => w.write_all(process::id().to_string().as_bytes()),
FormattedChunk::SystemThreadId => {
TID.with(|tid| w.write_all(tid.to_string().as_bytes()))
}
FormattedChunk::Target => w.write_all(record.target().as_bytes()),
FormattedChunk::Newline => w.write_all(NEWLINE.as_bytes()),
FormattedChunk::Align(ref chunks) => {
for chunk in chunks {
chunk.encode(w, record)?;
}
Ok(())
}
FormattedChunk::Highlight(ref chunks) => {
match record.level() {
Level::Error => {
w.set_style(Style::new().text(Color::Red).intense(true))?;
}
Level::Warn => w.set_style(Style::new().text(Color::Yellow))?,
Level::Info => w.set_style(Style::new().text(Color::Green))?,
Level::Trace => w.set_style(Style::new().text(Color::Cyan))?,
_ => {}
}
for chunk in chunks {
chunk.encode(w, record)?;
}
match record.level() {
Level::Error | Level::Warn | Level::Info | Level::Trace => {
w.set_style(&Style::new())?
}
_ => {}
}
Ok(())
}
FormattedChunk::Debug(ref chunks) => {
if cfg!(debug_assertions) {
for chunk in chunks {
chunk.encode(w, record)?;
}
}
Ok(())
}
FormattedChunk::Release(ref chunks) => {
if !cfg!(debug_assertions) {
for chunk in chunks {
chunk.encode(w, record)?;
}
}
Ok(())
}
FormattedChunk::Mdc(ref key, ref default) => {
log_mdc::get(key, |v| write!(w, "{}", v.unwrap_or(default)))
}
#[cfg(feature = "log_kv")]
FormattedChunk::Kv(ref key, ref default) => {
use log::kv::ToKey;
if let Some(v) = record.key_values().get(key.to_key()) {
write!(w, "{v}")
} else {
write!(w, "{default}")
}
}
}
}
}
#[derive(Clone, Eq, Debug, PartialEq, Hash)]
pub struct PatternEncoder {
#[debug(skip)]
chunks: Vec<Chunk>,
pattern: String,
}
impl Default for PatternEncoder {
fn default() -> PatternEncoder {
PatternEncoder::new("{d} {l} {t} - {m}{n}")
}
}
impl Encode for PatternEncoder {
fn encode(&self, w: &mut dyn encode::Write, record: &Record) -> anyhow::Result<()> {
for chunk in &self.chunks {
chunk.encode(w, record)?;
}
Ok(())
}
}
impl PatternEncoder {
pub fn new(pattern: &str) -> PatternEncoder {
PatternEncoder {
chunks: Parser::new(pattern).map(From::from).collect(),
pattern: pattern.to_owned(),
}
}
}
#[cfg(feature = "config_parsing")]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct PatternEncoderDeserializer;
#[cfg(feature = "config_parsing")]
impl Deserialize for PatternEncoderDeserializer {
type Trait = dyn Encode;
type Config = PatternEncoderConfig;
fn deserialize(
&self,
config: PatternEncoderConfig,
_: &Deserializers,
) -> anyhow::Result<Box<dyn Encode>> {
let encoder = match config.pattern {
Some(pattern) => PatternEncoder::new(&pattern),
None => PatternEncoder::default(),
};
Ok(Box::new(encoder))
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "simple_writer")]
use log::{Level, Record};
#[cfg(feature = "simple_writer")]
use std::process;
#[cfg(feature = "simple_writer")]
use std::thread;
#[cfg(feature = "log_kv")]
use super::Parser;
use super::{Chunk, PatternEncoder};
#[cfg(feature = "simple_writer")]
use crate::encode::writer::simple::SimpleWriter;
#[cfg(feature = "simple_writer")]
use crate::encode::Encode;
fn error_free(encoder: &PatternEncoder) -> bool {
encoder.chunks.iter().all(|c| match *c {
Chunk::Error(_) => false,
_ => true,
})
}
#[test]
fn invalid_formatter() {
assert!(!error_free(&PatternEncoder::new("{x}")));
}
#[test]
fn unclosed_delimiter() {
assert!(!error_free(&PatternEncoder::new("{d(%Y-%m-%d)")));
}
#[test]
#[cfg(feature = "simple_writer")]
fn log() {
let pw = PatternEncoder::new("{l} {m} at {M} in {f}:{L}");
let mut buf = vec![];
pw.encode(
&mut SimpleWriter(&mut buf),
&Record::builder()
.level(Level::Debug)
.args(format_args!("the message"))
.module_path(Some("path"))
.file(Some("file"))
.line(Some(132))
.build(),
)
.unwrap();
assert_eq!(buf, &b"DEBUG the message at path in file:132"[..]);
}
#[test]
#[cfg(feature = "simple_writer")]
fn unnamed_thread() {
thread::spawn(|| {
let pw = PatternEncoder::new("{T}");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, b"unnamed");
})
.join()
.unwrap();
}
#[test]
#[cfg(feature = "simple_writer")]
fn named_thread() {
thread::Builder::new()
.name("foobar".to_string())
.spawn(|| {
let pw = PatternEncoder::new("{T}");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, b"foobar");
})
.unwrap()
.join()
.unwrap();
}
#[test]
#[cfg(feature = "simple_writer")]
fn thread_id_field() {
thread::spawn(|| {
let pw = PatternEncoder::new("{I}");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, thread_id::get().to_string().as_bytes());
})
.join()
.unwrap();
}
#[test]
#[cfg(feature = "simple_writer")]
fn process_id() {
let pw = PatternEncoder::new("{P}");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, process::id().to_string().as_bytes());
}
#[test]
#[cfg(feature = "simple_writer")]
fn system_thread_id() {
let pw = PatternEncoder::new("{i}");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, thread_id::get().to_string().as_bytes());
}
#[test]
#[cfg(feature = "simple_writer")]
fn default_okay() {
assert!(error_free(&PatternEncoder::default()));
}
#[test]
#[cfg(feature = "simple_writer")]
fn left_align() {
let pw = PatternEncoder::new("{m:~<5.6}");
let mut buf = vec![];
pw.encode(
&mut SimpleWriter(&mut buf),
&Record::builder().args(format_args!("foo")).build(),
)
.unwrap();
assert_eq!(buf, b"foo~~");
buf.clear();
pw.encode(
&mut SimpleWriter(&mut buf),
&Record::builder().args(format_args!("foobar!")).build(),
)
.unwrap();
assert_eq!(buf, b"foobar");
}
#[test]
#[cfg(feature = "simple_writer")]
fn right_align() {
let pw = PatternEncoder::new("{m:~>5.6}");
let mut buf = vec![];
pw.encode(
&mut SimpleWriter(&mut buf),
&Record::builder().args(format_args!("foo")).build(),
)
.unwrap();
assert_eq!(buf, b"~~foo");
buf.clear();
pw.encode(
&mut SimpleWriter(&mut buf),
&Record::builder().args(format_args!("foobar!")).build(),
)
.unwrap();
assert_eq!(buf, b"foobar");
}
#[cfg(feature = "simple_writer")]
fn assert_info_message(pattern: &str, msg: &str, expected: &[u8]) {
let pw = PatternEncoder::new(pattern);
let mut buf = vec![];
pw.encode(
&mut SimpleWriter(&mut buf),
&Record::builder()
.level(Level::Info)
.args(format_args!("{}", msg))
.build(),
)
.unwrap();
assert_eq!(buf, expected);
}
#[test]
#[cfg(feature = "simple_writer")]
fn left_align_formatter() {
assert_info_message("{({l} {m}):15}", "foobar!", b"INFO foobar! ");
assert_info_message("{({l} {m}):7}", "foobar!", b"INFO foobar!");
}
#[test]
#[cfg(feature = "simple_writer")]
fn right_truncate_formatter() {
assert_info_message("{({l} {m}):7.7}", "foobar!", b"INFO fo");
assert_info_message("{({l} {m}):12.12}", "foobar!", b"INFO foobar!");
assert_info_message("{({l} {m}):7.14}", "foobar!", b"INFO foobar!");
}
#[test]
#[cfg(feature = "simple_writer")]
fn left_truncate_formatter() {
assert_info_message("{({l} {m}):-9.9}", "foobar!", b"O foobar!");
assert_info_message("{({l} {m}):-12.12}", "foobar!", b"INFO foobar!");
assert_info_message("{({l} {m}):-7.14}", "foobar!", b"INFO foobar!");
}
#[test]
#[cfg(feature = "simple_writer")]
fn right_align_formatter() {
assert_info_message("{({l} {m}):>15}", "foobar!", b" INFO foobar!");
assert_info_message("{({l} {m}):>12}", "foobar!", b"INFO foobar!");
assert_info_message("{({l} {m}):>7}", "foobar!", b"INFO foobar!");
}
#[test]
#[cfg(feature = "simple_writer")]
fn right_align_formatter_hard_unicode() {
assert_info_message(
"{({l} {m}):>15}",
"\u{01f5}\u{0067}\u{0301}",
" INFO \u{01f5}\u{0067}\u{0301}".as_bytes(),
);
}
#[test]
#[cfg(feature = "simple_writer")]
fn zalgo_text() {
let zalgo = "m\u{0301}\u{0302}o\u{0303}\u{0304}\u{0305}\u{0306}re testing l\u{113}ss \u{1F1F7}\u{1F1F8}\u{1F1EE}\u{1F1F4} CVE-2021-30860";
assert_info_message(
"{({l} {m}):10.10}",
zalgo,
"INFO m\u{0301}\u{0302}o\u{0303}\u{0304}\u{0305}\u{0306}re ".as_bytes(),
);
assert_info_message(
"{({l} {m}):24.24}",
zalgo,
"INFO m\u{0301}\u{0302}o\u{0303}\u{0304}\u{0305}\u{0306}re testing l\u{113}ss \u{1F1F7}\u{1F1F8}".as_bytes(),
);
assert_info_message(
"{({l} {m}):-24.24}",
zalgo,
"g l\u{113}ss \u{1F1F7}\u{1F1F8}\u{1F1EE}\u{1F1F4} CVE-2021-30860".as_bytes(),
);
}
#[test]
fn custom_date_format() {
assert!(error_free(&PatternEncoder::new(
"{d(%Y-%m-%d %H:%M:%S)} {m}{n}"
)));
}
#[test]
fn timezones() {
assert!(error_free(&PatternEncoder::new("{d(%+)(utc)}")));
assert!(error_free(&PatternEncoder::new("{d(%+)(local)}")));
assert!(!error_free(&PatternEncoder::new("{d(%+)(foo)}")));
}
#[test]
fn unescaped_parens() {
assert!(!error_free(&PatternEncoder::new("(hi)")));
}
#[test]
#[cfg(feature = "simple_writer")]
fn escaped_chars() {
let pw = PatternEncoder::new("{{{m}(())}}");
let mut buf = vec![];
pw.encode(
&mut SimpleWriter(&mut buf),
&Record::builder().args(format_args!("foobar!")).build(),
)
.unwrap();
assert_eq!(buf, b"{foobar!()}");
}
#[test]
#[cfg(feature = "simple_writer")]
fn quote_braces_with_backslash() {
let pw = PatternEncoder::new(r"\{\({l}\)\}\\");
let mut buf = vec![];
pw.encode(
&mut SimpleWriter(&mut buf),
&Record::builder().level(Level::Info).build(),
)
.unwrap();
assert_eq!(buf, br"{(INFO)}\");
}
#[test]
#[cfg(feature = "simple_writer")]
fn mdc() {
let pw = PatternEncoder::new("{X(user_id)}");
log_mdc::insert("user_id", "mdc value");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, b"mdc value");
}
#[test]
#[cfg(feature = "simple_writer")]
fn mdc_missing_default() {
let pw = PatternEncoder::new("{X(user_id)}");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, b"");
}
#[test]
#[cfg(feature = "simple_writer")]
fn mdc_missing_custom() {
let pw = PatternEncoder::new("{X(user_id)(missing value)}");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, b"missing value");
}
#[test]
#[cfg(all(feature = "simple_writer", feature = "log_kv"))]
fn test_kv() {
let pw = PatternEncoder::new("{K(user_id)}");
let kv = [("user_id", "kv value")];
let mut buf = vec![];
pw.encode(
&mut SimpleWriter(&mut buf),
&Record::builder().key_values(&kv).build(),
)
.unwrap();
assert_eq!(buf, b"kv value");
}
#[test]
#[cfg(all(feature = "simple_writer", feature = "log_kv"))]
fn test_kv_missing_default() {
let pw = PatternEncoder::new("{K(user_id)}");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, b"");
}
#[test]
#[cfg(all(feature = "simple_writer", feature = "log_kv"))]
fn test_kv_missing_custom() {
let pw = PatternEncoder::new("{K(user_id)(missing value)}");
let mut buf = vec![];
pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
.unwrap();
assert_eq!(buf, b"missing value");
}
#[test]
#[cfg(feature = "log_kv")]
fn test_kv_from_piece_to_chunk() {
let tests = vec![
(
"[{K(user_id)(foobar)(test):<5.5}]",
"expected at most two arguments",
),
("[{K({l user_id):<5.5}]", "expected '}'"),
("[{K({l} user_id):<5.5}]", "key_value: invalid key"),
("[{K:<5.5}]", "key_value: missing key"),
("[{K(user_id)({l):<5.5}]", "expected '}'"),
("[{K(user_id)({l}):<5.5}]", "key_value: invalid default"),
("[{K(user_id)():<5.5} {M}]", "key_value: invalid default"),
];
for (pattern, error_msg) in tests {
let chunks: Vec<Chunk> = Parser::new(pattern).map(From::from).collect();
match chunks.get(1).unwrap() {
Chunk::Error(err) => assert!(err.contains(error_msg)),
_ => panic!(),
}
}
}
#[test]
#[cfg(feature = "simple_writer")]
fn debug_release() {
let debug_pat = "{D({l})}";
let release_pat = "{R({l})}";
let debug_encoder = PatternEncoder::new(debug_pat);
let release_encoder = PatternEncoder::new(release_pat);
let mut debug_buf = vec![];
let mut release_buf = vec![];
debug_encoder
.encode(
&mut SimpleWriter(&mut debug_buf),
&Record::builder().level(Level::Info).build(),
)
.unwrap();
release_encoder
.encode(
&mut SimpleWriter(&mut release_buf),
&Record::builder().level(Level::Info).build(),
)
.unwrap();
if cfg!(debug_assertions) {
assert_eq!(debug_buf, b"INFO");
assert!(release_buf.is_empty());
} else {
assert_eq!(release_buf, b"INFO");
assert!(debug_buf.is_empty());
}
}
}