1use chrono::{Local, Utc};
123use derivative::Derivative;
124use log::{Level, Record};
125use std::{default::Default, io, process, thread};
126
127use crate::encode::{
128 self,
129 pattern::parser::{Alignment, Parameters, Parser, Piece},
130 Color, Encode, Style, NEWLINE,
131};
132
133#[cfg(feature = "config_parsing")]
134use crate::config::{Deserialize, Deserializers};
135
136mod parser;
137
138thread_local!(
139 static TID: usize = thread_id::get()
141);
142
143#[cfg(feature = "config_parsing")]
145#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, serde::Deserialize)]
146#[serde(deny_unknown_fields)]
147pub struct PatternEncoderConfig {
148 pattern: Option<String>,
149}
150
151fn is_char_boundary(b: u8) -> bool {
152 b as i8 >= -0x40
153}
154
155fn char_starts(buf: &[u8]) -> usize {
156 buf.iter().filter(|&&b| is_char_boundary(b)).count()
157}
158
159struct MaxWidthWriter<'a> {
160 remaining: usize,
161 w: &'a mut dyn encode::Write,
162}
163
164impl<'a> io::Write for MaxWidthWriter<'a> {
165 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
166 let mut remaining = self.remaining;
167 let mut end = buf.len();
168 for (idx, _) in buf
169 .iter()
170 .enumerate()
171 .filter(|&(_, &b)| is_char_boundary(b))
172 {
173 if remaining == 0 {
174 end = idx;
175 break;
176 }
177 remaining -= 1;
178 }
179
180 if end == 0 {
182 return Ok(buf.len());
183 }
184
185 let buf = &buf[..end];
186 match self.w.write(buf) {
187 Ok(len) => {
188 if len == end {
189 self.remaining = remaining;
190 } else {
191 self.remaining -= char_starts(&buf[..len]);
192 }
193 Ok(len)
194 }
195 Err(e) => Err(e),
196 }
197 }
198
199 fn flush(&mut self) -> io::Result<()> {
200 self.w.flush()
201 }
202}
203
204impl<'a> encode::Write for MaxWidthWriter<'a> {
205 fn set_style(&mut self, style: &Style) -> io::Result<()> {
206 self.w.set_style(style)
207 }
208}
209
210struct LeftAlignWriter<W> {
211 to_fill: usize,
212 fill: char,
213 w: W,
214}
215
216impl<W: encode::Write> LeftAlignWriter<W> {
217 fn finish(mut self) -> io::Result<()> {
218 for _ in 0..self.to_fill {
219 write!(self.w, "{}", self.fill)?;
220 }
221 Ok(())
222 }
223}
224
225impl<W: encode::Write> io::Write for LeftAlignWriter<W> {
226 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
227 match self.w.write(buf) {
228 Ok(len) => {
229 self.to_fill = self.to_fill.saturating_sub(char_starts(&buf[..len]));
230 Ok(len)
231 }
232 Err(e) => Err(e),
233 }
234 }
235
236 fn flush(&mut self) -> io::Result<()> {
237 self.w.flush()
238 }
239}
240
241impl<W: encode::Write> encode::Write for LeftAlignWriter<W> {
242 fn set_style(&mut self, style: &Style) -> io::Result<()> {
243 self.w.set_style(style)
244 }
245}
246
247enum BufferedOutput {
248 Data(Vec<u8>),
249 Style(Style),
250}
251
252struct RightAlignWriter<W> {
253 to_fill: usize,
254 fill: char,
255 w: W,
256 buf: Vec<BufferedOutput>,
257}
258
259impl<W: encode::Write> RightAlignWriter<W> {
260 fn finish(mut self) -> io::Result<()> {
261 for _ in 0..self.to_fill {
262 write!(self.w, "{}", self.fill)?;
263 }
264 for out in self.buf {
265 match out {
266 BufferedOutput::Data(ref buf) => self.w.write_all(buf)?,
267 BufferedOutput::Style(ref style) => self.w.set_style(style)?,
268 }
269 }
270 Ok(())
271 }
272}
273
274impl<W: encode::Write> io::Write for RightAlignWriter<W> {
275 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
276 self.to_fill = self.to_fill.saturating_sub(char_starts(buf));
277
278 let mut pushed = false;
279 if let Some(&mut BufferedOutput::Data(ref mut data)) = self.buf.last_mut() {
280 data.extend_from_slice(buf);
281 pushed = true;
282 };
283
284 if !pushed {
285 self.buf.push(BufferedOutput::Data(buf.to_owned()));
286 }
287 Ok(buf.len())
288 }
289
290 fn flush(&mut self) -> io::Result<()> {
291 Ok(())
292 }
293}
294
295impl<W: encode::Write> encode::Write for RightAlignWriter<W> {
296 fn set_style(&mut self, style: &Style) -> io::Result<()> {
297 self.buf.push(BufferedOutput::Style(style.clone()));
298 Ok(())
299 }
300}
301
302#[derive(Clone, Eq, PartialEq, Hash, Debug)]
303enum Chunk {
304 Text(String),
305 Formatted {
306 chunk: FormattedChunk,
307 params: Parameters,
308 },
309 Error(String),
310}
311
312impl Chunk {
313 fn encode(&self, w: &mut dyn encode::Write, record: &Record) -> io::Result<()> {
314 match *self {
315 Chunk::Text(ref s) => w.write_all(s.as_bytes()),
316 Chunk::Formatted {
317 ref chunk,
318 ref params,
319 } => match (params.min_width, params.max_width, params.align) {
320 (None, None, _) => chunk.encode(w, record),
321 (None, Some(max_width), _) => {
322 let mut w = MaxWidthWriter {
323 remaining: max_width,
324 w,
325 };
326 chunk.encode(&mut w, record)
327 }
328 (Some(min_width), None, Alignment::Left) => {
329 let mut w = LeftAlignWriter {
330 to_fill: min_width,
331 fill: params.fill,
332 w,
333 };
334 chunk.encode(&mut w, record)?;
335 w.finish()
336 }
337 (Some(min_width), None, Alignment::Right) => {
338 let mut w = RightAlignWriter {
339 to_fill: min_width,
340 fill: params.fill,
341 w,
342 buf: vec![],
343 };
344 chunk.encode(&mut w, record)?;
345 w.finish()
346 }
347 (Some(min_width), Some(max_width), Alignment::Left) => {
348 let mut w = LeftAlignWriter {
349 to_fill: min_width,
350 fill: params.fill,
351 w: MaxWidthWriter {
352 remaining: max_width,
353 w,
354 },
355 };
356 chunk.encode(&mut w, record)?;
357 w.finish()
358 }
359 (Some(min_width), Some(max_width), Alignment::Right) => {
360 let mut w = RightAlignWriter {
361 to_fill: min_width,
362 fill: params.fill,
363 w: MaxWidthWriter {
364 remaining: max_width,
365 w,
366 },
367 buf: vec![],
368 };
369 chunk.encode(&mut w, record)?;
370 w.finish()
371 }
372 },
373 Chunk::Error(ref s) => write!(w, "{{ERROR: {}}}", s),
374 }
375 }
376}
377
378impl<'a> From<Piece<'a>> for Chunk {
379 fn from(piece: Piece<'a>) -> Chunk {
380 match piece {
381 Piece::Text(text) => Chunk::Text(text.to_owned()),
382 Piece::Argument {
383 mut formatter,
384 parameters,
385 } => match formatter.name {
386 "d" | "date" => {
387 if formatter.args.len() > 2 {
388 return Chunk::Error("expected at most two arguments".to_owned());
389 }
390
391 let format = match formatter.args.get(0) {
392 Some(arg) => {
393 let mut format = String::new();
394 for piece in arg {
395 match *piece {
396 Piece::Text(text) => format.push_str(text),
397 Piece::Argument { .. } => {
398 format.push_str("{ERROR: unexpected formatter}");
399 }
400 Piece::Error(ref err) => {
401 format.push_str("{ERROR: ");
402 format.push_str(err);
403 format.push('}');
404 }
405 }
406 }
407 format
408 }
409 None => "%+".to_owned(),
410 };
411
412 let timezone = match formatter.args.get(1) {
413 Some(arg) => {
414 if let Some(arg) = arg.get(0) {
415 match *arg {
416 Piece::Text(z) if z == "utc" => Timezone::Utc,
417 Piece::Text(z) if z == "local" => Timezone::Local,
418 Piece::Text(z) => {
419 return Chunk::Error(format!("invalid timezone `{}`", z));
420 }
421 _ => return Chunk::Error("invalid timezone".to_owned()),
422 }
423 } else {
424 return Chunk::Error("invalid timezone".to_owned());
425 }
426 }
427 None => Timezone::Local,
428 };
429
430 Chunk::Formatted {
431 chunk: FormattedChunk::Time(format, timezone),
432 params: parameters,
433 }
434 }
435 "h" | "highlight" => {
436 if formatter.args.len() != 1 {
437 return Chunk::Error("expected exactly one argument".to_owned());
438 }
439
440 let chunks = formatter
441 .args
442 .pop()
443 .unwrap()
444 .into_iter()
445 .map(From::from)
446 .collect();
447 Chunk::Formatted {
448 chunk: FormattedChunk::Highlight(chunks),
449 params: parameters,
450 }
451 }
452 "l" | "level" => no_args(&formatter.args, parameters, FormattedChunk::Level),
453 "m" | "message" => no_args(&formatter.args, parameters, FormattedChunk::Message),
454 "M" | "module" => no_args(&formatter.args, parameters, FormattedChunk::Module),
455 "n" => no_args(&formatter.args, parameters, FormattedChunk::Newline),
456 "f" | "file" => no_args(&formatter.args, parameters, FormattedChunk::File),
457 "L" | "line" => no_args(&formatter.args, parameters, FormattedChunk::Line),
458 "T" | "thread" => no_args(&formatter.args, parameters, FormattedChunk::Thread),
459 "I" | "thread_id" => no_args(&formatter.args, parameters, FormattedChunk::ThreadId),
460 "P" | "pid" => no_args(&formatter.args, parameters, FormattedChunk::ProcessId),
461 "i" | "tid" => no_args(&formatter.args, parameters, FormattedChunk::SystemThreadId),
462 "t" | "target" => no_args(&formatter.args, parameters, FormattedChunk::Target),
463 "X" | "mdc" => {
464 if formatter.args.len() > 2 {
465 return Chunk::Error("expected at most two arguments".to_owned());
466 }
467
468 let key = match formatter.args.get(0) {
469 Some(arg) => {
470 if let Some(arg) = arg.get(0) {
471 match arg {
472 Piece::Text(key) => key.to_owned(),
473 Piece::Error(ref e) => return Chunk::Error(e.clone()),
474 _ => return Chunk::Error("invalid MDC key".to_owned()),
475 }
476 } else {
477 return Chunk::Error("invalid MDC key".to_owned());
478 }
479 }
480 None => return Chunk::Error("missing MDC key".to_owned()),
481 };
482
483 let default = match formatter.args.get(1) {
484 Some(arg) => {
485 if let Some(arg) = arg.get(0) {
486 match arg {
487 Piece::Text(key) => key.to_owned(),
488 Piece::Error(ref e) => return Chunk::Error(e.clone()),
489 _ => return Chunk::Error("invalid MDC default".to_owned()),
490 }
491 } else {
492 return Chunk::Error("invalid MDC default".to_owned());
493 }
494 }
495 None => "",
496 };
497
498 Chunk::Formatted {
499 chunk: FormattedChunk::Mdc(key.into(), default.into()),
500 params: parameters,
501 }
502 }
503 "" => {
504 if formatter.args.len() != 1 {
505 return Chunk::Error("expected exactly one argument".to_owned());
506 }
507
508 let chunks = formatter
509 .args
510 .pop()
511 .unwrap()
512 .into_iter()
513 .map(From::from)
514 .collect();
515 Chunk::Formatted {
516 chunk: FormattedChunk::Align(chunks),
517 params: parameters,
518 }
519 }
520 name => Chunk::Error(format!("unknown formatter `{}`", name)),
521 },
522 Piece::Error(err) => Chunk::Error(err),
523 }
524 }
525}
526
527fn no_args(arg: &[Vec<Piece>], params: Parameters, chunk: FormattedChunk) -> Chunk {
528 if arg.is_empty() {
529 Chunk::Formatted { chunk, params }
530 } else {
531 Chunk::Error("unexpected arguments".to_owned())
532 }
533}
534
535#[derive(Clone, Eq, PartialEq, Hash, Debug)]
536enum Timezone {
537 Utc,
538 Local,
539}
540
541#[derive(Clone, Eq, PartialEq, Hash, Debug)]
542enum FormattedChunk {
543 Time(String, Timezone),
544 Level,
545 Message,
546 Module,
547 File,
548 Line,
549 Thread,
550 ThreadId,
551 ProcessId,
552 SystemThreadId,
553 Target,
554 Newline,
555 Align(Vec<Chunk>),
556 Highlight(Vec<Chunk>),
557 Mdc(String, String),
558}
559
560impl FormattedChunk {
561 fn encode(&self, w: &mut dyn encode::Write, record: &Record) -> io::Result<()> {
562 match *self {
563 FormattedChunk::Time(ref fmt, Timezone::Utc) => write!(w, "{}", Utc::now().format(fmt)),
564 FormattedChunk::Time(ref fmt, Timezone::Local) => {
565 write!(w, "{}", Local::now().format(fmt))
566 }
567 FormattedChunk::Level => write!(w, "{}", record.level()),
568 FormattedChunk::Message => w.write_fmt(*record.args()),
569 FormattedChunk::Module => w.write_all(record.module_path().unwrap_or("???").as_bytes()),
570 FormattedChunk::File => w.write_all(record.file().unwrap_or("???").as_bytes()),
571 FormattedChunk::Line => match record.line() {
572 Some(line) => write!(w, "{}", line),
573 None => w.write_all(b"???"),
574 },
575 FormattedChunk::Thread => {
576 w.write_all(thread::current().name().unwrap_or("unnamed").as_bytes())
577 }
578 FormattedChunk::ThreadId => w.write_all(thread_id::get().to_string().as_bytes()),
579 FormattedChunk::ProcessId => w.write_all(process::id().to_string().as_bytes()),
580 FormattedChunk::SystemThreadId => {
581 TID.with(|tid| w.write_all(tid.to_string().as_bytes()))
582 }
583 FormattedChunk::Target => w.write_all(record.target().as_bytes()),
584 FormattedChunk::Newline => w.write_all(NEWLINE.as_bytes()),
585 FormattedChunk::Align(ref chunks) => {
586 for chunk in chunks {
587 chunk.encode(w, record)?;
588 }
589 Ok(())
590 }
591 FormattedChunk::Highlight(ref chunks) => {
592 match record.level() {
593 Level::Error => {
594 w.set_style(Style::new().text(Color::Red).intense(true))?;
595 }
596 Level::Warn => w.set_style(Style::new().text(Color::Yellow))?,
597 Level::Info => w.set_style(Style::new().text(Color::Green))?,
598 Level::Trace => w.set_style(Style::new().text(Color::Cyan))?,
599 _ => {}
600 }
601 for chunk in chunks {
602 chunk.encode(w, record)?;
603 }
604 match record.level() {
605 Level::Error | Level::Warn | Level::Info | Level::Trace => {
606 w.set_style(&Style::new())?
607 }
608 _ => {}
609 }
610 Ok(())
611 }
612 FormattedChunk::Mdc(ref key, ref default) => {
613 log_mdc::get(key, |v| write!(w, "{}", v.unwrap_or(default)))
614 }
615 }
616 }
617}
618
619#[derive(Derivative)]
621#[derivative(Debug)]
622#[derive(Clone, Eq, PartialEq, Hash)]
623pub struct PatternEncoder {
624 #[derivative(Debug = "ignore")]
625 chunks: Vec<Chunk>,
626 pattern: String,
627}
628
629impl Default for PatternEncoder {
631 fn default() -> PatternEncoder {
632 PatternEncoder::new("{d} {l} {t} - {m}{n}")
633 }
634}
635
636impl Encode for PatternEncoder {
637 fn encode(&self, w: &mut dyn encode::Write, record: &Record) -> anyhow::Result<()> {
638 for chunk in &self.chunks {
639 chunk.encode(w, record)?;
640 }
641 Ok(())
642 }
643}
644
645impl PatternEncoder {
646 pub fn new(pattern: &str) -> PatternEncoder {
650 PatternEncoder {
651 chunks: Parser::new(pattern).map(From::from).collect(),
652 pattern: pattern.to_owned(),
653 }
654 }
655}
656
657#[cfg(feature = "config_parsing")]
669#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
670pub struct PatternEncoderDeserializer;
671
672#[cfg(feature = "config_parsing")]
673impl Deserialize for PatternEncoderDeserializer {
674 type Trait = dyn Encode;
675
676 type Config = PatternEncoderConfig;
677
678 fn deserialize(
679 &self,
680 config: PatternEncoderConfig,
681 _: &Deserializers,
682 ) -> anyhow::Result<Box<dyn Encode>> {
683 let encoder = match config.pattern {
684 Some(pattern) => PatternEncoder::new(&pattern),
685 None => PatternEncoder::default(),
686 };
687 Ok(Box::new(encoder))
688 }
689}
690
691#[cfg(test)]
692mod tests {
693 #[cfg(feature = "simple_writer")]
694 use log::{Level, Record};
695 #[cfg(feature = "simple_writer")]
696 use std::process;
697 #[cfg(feature = "simple_writer")]
698 use std::thread;
699
700 use super::{Chunk, PatternEncoder};
701 #[cfg(feature = "simple_writer")]
702 use crate::encode::writer::simple::SimpleWriter;
703 #[cfg(feature = "simple_writer")]
704 use crate::encode::Encode;
705
706 fn error_free(encoder: &PatternEncoder) -> bool {
707 encoder.chunks.iter().all(|c| match *c {
708 Chunk::Error(_) => false,
709 _ => true,
710 })
711 }
712
713 #[test]
714 fn invalid_formatter() {
715 assert!(!error_free(&PatternEncoder::new("{x}")));
716 }
717
718 #[test]
719 fn unclosed_delimiter() {
720 assert!(!error_free(&PatternEncoder::new("{d(%Y-%m-%d)")));
721 }
722
723 #[test]
724 #[cfg(feature = "simple_writer")]
725 fn log() {
726 let pw = PatternEncoder::new("{l} {m} at {M} in {f}:{L}");
727 let mut buf = vec![];
728 pw.encode(
729 &mut SimpleWriter(&mut buf),
730 &Record::builder()
731 .level(Level::Debug)
732 .args(format_args!("the message"))
733 .module_path(Some("path"))
734 .file(Some("file"))
735 .line(Some(132))
736 .build(),
737 )
738 .unwrap();
739
740 assert_eq!(buf, &b"DEBUG the message at path in file:132"[..]);
741 }
742
743 #[test]
744 #[cfg(feature = "simple_writer")]
745 fn unnamed_thread() {
746 thread::spawn(|| {
747 let pw = PatternEncoder::new("{T}");
748 let mut buf = vec![];
749 pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
750 .unwrap();
751 assert_eq!(buf, b"unnamed");
752 })
753 .join()
754 .unwrap();
755 }
756
757 #[test]
758 #[cfg(feature = "simple_writer")]
759 fn named_thread() {
760 thread::Builder::new()
761 .name("foobar".to_string())
762 .spawn(|| {
763 let pw = PatternEncoder::new("{T}");
764 let mut buf = vec![];
765 pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
766 .unwrap();
767 assert_eq!(buf, b"foobar");
768 })
769 .unwrap()
770 .join()
771 .unwrap();
772 }
773
774 #[test]
775 #[cfg(feature = "simple_writer")]
776 fn thread_id_field() {
777 thread::spawn(|| {
778 let pw = PatternEncoder::new("{I}");
779 let mut buf = vec![];
780 pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
781 .unwrap();
782 assert_eq!(buf, thread_id::get().to_string().as_bytes());
783 })
784 .join()
785 .unwrap();
786 }
787
788 #[test]
789 #[cfg(feature = "simple_writer")]
790 fn process_id() {
791 let pw = PatternEncoder::new("{P}");
792 let mut buf = vec![];
793
794 pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
795 .unwrap();
796
797 assert_eq!(buf, process::id().to_string().as_bytes());
798 }
799
800 #[test]
801 #[cfg(feature = "simple_writer")]
802 fn system_thread_id() {
803 let pw = PatternEncoder::new("{i}");
804 let mut buf = vec![];
805
806 pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
807 .unwrap();
808
809 assert_eq!(buf, thread_id::get().to_string().as_bytes());
810 }
811
812 #[test]
813 #[cfg(feature = "simple_writer")]
814 fn default_okay() {
815 assert!(error_free(&PatternEncoder::default()));
816 }
817
818 #[test]
819 #[cfg(feature = "simple_writer")]
820 fn left_align() {
821 let pw = PatternEncoder::new("{m:~<5.6}");
822
823 let mut buf = vec![];
824 pw.encode(
825 &mut SimpleWriter(&mut buf),
826 &Record::builder().args(format_args!("foo")).build(),
827 )
828 .unwrap();
829 assert_eq!(buf, b"foo~~");
830
831 buf.clear();
832 pw.encode(
833 &mut SimpleWriter(&mut buf),
834 &Record::builder().args(format_args!("foobar!")).build(),
835 )
836 .unwrap();
837 assert_eq!(buf, b"foobar");
838 }
839
840 #[test]
841 #[cfg(feature = "simple_writer")]
842 fn right_align() {
843 let pw = PatternEncoder::new("{m:~>5.6}");
844
845 let mut buf = vec![];
846 pw.encode(
847 &mut SimpleWriter(&mut buf),
848 &Record::builder().args(format_args!("foo")).build(),
849 )
850 .unwrap();
851 assert_eq!(buf, b"~~foo");
852
853 buf.clear();
854 pw.encode(
855 &mut SimpleWriter(&mut buf),
856 &Record::builder().args(format_args!("foobar!")).build(),
857 )
858 .unwrap();
859 assert_eq!(buf, b"foobar");
860 }
861
862 #[test]
863 #[cfg(feature = "simple_writer")]
864 fn left_align_formatter() {
865 let pw = PatternEncoder::new("{({l} {m}):15}");
866
867 let mut buf = vec![];
868 pw.encode(
869 &mut SimpleWriter(&mut buf),
870 &Record::builder()
871 .level(Level::Info)
872 .args(format_args!("foobar!"))
873 .build(),
874 )
875 .unwrap();
876 assert_eq!(buf, b"INFO foobar! ");
877 }
878
879 #[test]
880 #[cfg(feature = "simple_writer")]
881 fn right_align_formatter() {
882 let pw = PatternEncoder::new("{({l} {m}):>15}");
883
884 let mut buf = vec![];
885 pw.encode(
886 &mut SimpleWriter(&mut buf),
887 &Record::builder()
888 .level(Level::Info)
889 .args(format_args!("foobar!"))
890 .build(),
891 )
892 .unwrap();
893 assert_eq!(buf, b" INFO foobar!");
894 }
895
896 #[test]
897 fn custom_date_format() {
898 assert!(error_free(&PatternEncoder::new(
899 "{d(%Y-%m-%d %H:%M:%S)} {m}{n}"
900 )));
901 }
902
903 #[test]
904 fn timezones() {
905 assert!(error_free(&PatternEncoder::new("{d(%+)(utc)}")));
906 assert!(error_free(&PatternEncoder::new("{d(%+)(local)}")));
907 assert!(!error_free(&PatternEncoder::new("{d(%+)(foo)}")));
908 }
909
910 #[test]
911 fn unescaped_parens() {
912 assert!(!error_free(&PatternEncoder::new("(hi)")));
913 }
914
915 #[test]
916 #[cfg(feature = "simple_writer")]
917 fn escaped_chars() {
918 let pw = PatternEncoder::new("{{{m}(())}}");
919
920 let mut buf = vec![];
921 pw.encode(
922 &mut SimpleWriter(&mut buf),
923 &Record::builder().args(format_args!("foobar!")).build(),
924 )
925 .unwrap();
926 assert_eq!(buf, b"{foobar!()}");
927 }
928
929 #[test]
930 #[cfg(feature = "simple_writer")]
931 fn quote_braces_with_backslash() {
932 let pw = PatternEncoder::new(r"\{\({l}\)\}\\");
933
934 let mut buf = vec![];
935 pw.encode(
936 &mut SimpleWriter(&mut buf),
937 &Record::builder().level(Level::Info).build(),
938 )
939 .unwrap();
940 assert_eq!(buf, br"{(INFO)}\");
941 }
942
943 #[test]
944 #[cfg(feature = "simple_writer")]
945 fn mdc() {
946 let pw = PatternEncoder::new("{X(user_id)}");
947 log_mdc::insert("user_id", "mdc value");
948
949 let mut buf = vec![];
950 pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
951 .unwrap();
952
953 assert_eq!(buf, b"mdc value");
954 }
955
956 #[test]
957 #[cfg(feature = "simple_writer")]
958 fn mdc_missing_default() {
959 let pw = PatternEncoder::new("{X(user_id)}");
960
961 let mut buf = vec![];
962 pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
963 .unwrap();
964
965 assert_eq!(buf, b"");
966 }
967
968 #[test]
969 #[cfg(feature = "simple_writer")]
970 fn mdc_missing_custom() {
971 let pw = PatternEncoder::new("{X(user_id)(missing value)}");
972
973 let mut buf = vec![];
974 pw.encode(&mut SimpleWriter(&mut buf), &Record::builder().build())
975 .unwrap();
976
977 assert_eq!(buf, b"missing value");
978 }
979}