diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cef262f9..95394e4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] - fn_features: ['', 'log native libsystemd multi-thread'] + fn_features: ['', 'log native libsystemd multi-thread runtime-pattern'] cfg_feature: ['', 'flexible-string', 'source-location'] runs-on: ${{ matrix.os }} steps: diff --git a/spdlog/Cargo.toml b/spdlog/Cargo.toml index 13760a24..09393bb7 100644 --- a/spdlog/Cargo.toml +++ b/spdlog/Cargo.toml @@ -36,6 +36,7 @@ source-location = [] native = [] libsystemd = ["libsystemd-sys"] multi-thread = ["crossbeam"] +runtime-pattern = ["spdlog-internal"] [dependencies] arc-swap = "1.5.1" @@ -43,11 +44,13 @@ atomic = "0.5.1" cfg-if = "1.0.0" chrono = "0.4.22" crossbeam = { version = "0.8.2", optional = true } +dyn-clone = "1.0.14" flexible-string = { version = "0.1.0", optional = true } if_chain = "1.0.2" is-terminal = "0.4" log = { version = "0.4.8", optional = true } once_cell = "1.16.0" +spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal", optional = true } spdlog-macros = { version = "0.1.0", path = "../spdlog-macros" } spin = "0.9.8" static_assertions = "1.1.0" @@ -77,6 +80,7 @@ flexi_logger = "=0.24.1" tracing = "=0.1.37" tracing-subscriber = "=0.3.16" tracing-appender = "=0.2.2" +paste = "1.0.14" [build-dependencies] rustc_version = "0.4.0" diff --git a/spdlog/benches/pattern.rs b/spdlog/benches/pattern.rs index f5b1ea05..9f6fb552 100644 --- a/spdlog/benches/pattern.rs +++ b/spdlog/benches/pattern.rs @@ -4,8 +4,9 @@ extern crate test; use std::{cell::RefCell, sync::Arc}; +use paste::paste; use spdlog::{ - formatter::{pattern, Formatter, FullFormatter, Pattern, PatternFormatter}, + formatter::{pattern, Formatter, FullFormatter, Pattern, PatternFormatter, RuntimePattern}, prelude::*, sink::Sink, Record, StringBuf, @@ -76,15 +77,7 @@ fn bench_pattern(bencher: &mut Bencher, pattern: impl Pattern + Clone + 'static) bench_formatter(bencher, PatternFormatter::new(pattern)); } -#[bench] -fn bench_1_full_formatter(bencher: &mut Bencher) { - bench_formatter(bencher, FullFormatter::new()) -} - -#[bench] -fn bench_2_full_pattern(bencher: &mut Bencher) { - let pattern = pattern!("[{date} {time}.{millisecond}] [{level}] {payload}{eol}"); - +fn bench_full_pattern(bencher: &mut Bencher, pattern: impl Pattern + Clone + 'static) { let full_formatter = Arc::new(StringSink::with(|b| { b.formatter(Box::new(FullFormatter::new())) })); @@ -103,192 +96,81 @@ fn bench_2_full_pattern(bencher: &mut Bencher) { bench_pattern(bencher, pattern) } -#[bench] -fn bench_weekday_name(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{weekday_name}")) -} - -#[bench] -fn bench_weekday_name_full(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{weekday_name_full}")) -} - -#[bench] -fn bench_month_name(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{month_name}")) -} - -#[bench] -fn bench_month_name_full(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{month_name_full}")) -} - -#[bench] -fn bench_datetime(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{datetime}")) -} - -#[bench] -fn bench_year_short(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{year_short}")) -} - -#[bench] -fn bench_year(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{year}")) -} - -#[bench] -fn bench_date_short(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{date_short}")) -} - -#[bench] -fn bench_date(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{date}")) -} - -#[bench] -fn bench_month(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{month}")) -} - -#[bench] -fn bench_day(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{day}")) -} - -#[bench] -fn bench_hour(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{hour}")) -} - -#[bench] -fn bench_hour_12(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{hour_12}")) -} - -#[bench] -fn bench_minute(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{minute}")) -} - -#[bench] -fn bench_second(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{second}")) -} - -#[bench] -fn bench_millsecond(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{millisecond}")) -} - -#[bench] -fn bench_microsecond(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{microsecond}")) -} - -#[bench] -fn bench_nanosecond(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{nanosecond}")) -} - -#[bench] -fn bench_am_pm(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{am_pm}")) -} - -#[bench] -fn bench_time_12(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{time_12}")) -} - -#[bench] -fn bench_time_short(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{time_short}")) -} - -#[bench] -fn bench_time(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{time}")) -} - -#[bench] -fn bench_tz_offset(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{tz_offset}")) -} - -#[bench] -fn bench_unix_timestamp(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{unix_timestamp}")) -} - -#[bench] -fn bench_full(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{full}")) -} - -#[bench] -fn bench_level(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{level}")) -} - -#[bench] -fn bench_level_short(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{level_short}")) -} +// #[bench] -fn bench_source(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{source}")) -} - -#[bench] -fn bench_file_name(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{file_name}")) -} - -#[bench] -fn bench_file(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{file}")) -} - -#[bench] -fn bench_line(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{line}")) -} - -#[bench] -fn bench_column(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{column}")) -} - -#[bench] -fn bench_module_path(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{module_path}")) -} - -#[bench] -fn bench_logger(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{logger}")) -} - -#[bench] -fn bench_payload(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{payload}")) -} - -#[bench] -fn bench_pid(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{pid}")) -} - -#[bench] -fn bench_tid(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{tid}")) +fn bench_1_full_formatter(bencher: &mut Bencher) { + bench_formatter(bencher, FullFormatter::new()) } #[bench] -fn bench_eol(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{eol}")) +fn bench_2_full_pattern_ct(bencher: &mut Bencher) { + bench_full_pattern( + bencher, + pattern!("[{date} {time}.{millisecond}] [{level}] {payload}{eol}"), + ) +} + +#[bench] +fn bench_3_full_pattern_rt(bencher: &mut Bencher) { + bench_full_pattern( + bencher, + RuntimePattern::new("[{date} {time}.{millisecond}] [{level}] {payload}{eol}").unwrap(), + ) +} + +macro_rules! bench_patterns { + ( $(($name:ident, $placeholder:literal)),+ $(,)? ) => { + $(paste! { + #[bench] + fn [](bencher: &mut Bencher) { + bench_pattern(bencher, pattern!($placeholder)) + } + #[bench] + fn [](bencher: &mut Bencher) { + bench_pattern(bencher, RuntimePattern::new($placeholder).unwrap()) + } + })+ + }; +} + +bench_patterns! { + (weekday_name, "{weekday_name}"), + (weekday_name_full, "{weekday_name_full}"), + (month_name, "{month_name}"), + (month_name_full, "{month_name_full}"), + (datetime, "{datetime}"), + (year_short, "{year_short}"), + (year, "{year}"), + (date_short, "{date_short}"), + (date, "{date}"), + (month, "{month}"), + (day, "{day}"), + (hour, "{hour}"), + (hour_12, "{hour_12}"), + (minute, "{minute}"), + (second, "{second}"), + (millsecond, "{millisecond}"), + (microsecond, "{microsecond}"), + (nanosecond, "{nanosecond}"), + (am_pm, "{am_pm}"), + (time_12, "{time_12}"), + (time_short, "{time_short}"), + (time, "{time}"), + (tz_offset, "{tz_offset}"), + (unix_timestamp, "{unix_timestamp}"), + (full, "{full}"), + (level, "{level}"), + (level_short, "{level_short}"), + (source, "{source}"), + (file_name, "{file_name}"), + (file, "{file}"), + (line, "{line}"), + (column, "{column}"), + (module_path, "{module_path}"), + (logger, "{logger}"), + (payload, "{payload}"), + (pid, "{pid}"), + (tid, "{tid}"), + (eol, "{eol}"), } diff --git a/spdlog/src/error.rs b/spdlog/src/error.rs index 98b84b64..17e0fde1 100644 --- a/spdlog/src/error.rs +++ b/spdlog/src/error.rs @@ -91,6 +91,14 @@ pub enum Error { #[cfg(feature = "multi-thread")] #[error("failed to send message to channel: {0}")] SendToChannel(SendToChannelError, SendToChannelErrorDropped), + + /// The variant returned by [`RuntimePatternBuilder::build`] when the + /// pattern is failed to be built at runtime. + /// + /// [`RuntimePatternBuilder::build`]: crate::formatter::RuntimePatternBuilder::build + #[cfg(feature = "runtime-pattern")] + #[error("failed to build pattern at runtime: {0}")] + BuildPattern(BuildPatternError), } /// This error type contains a variety of possible invalid arguments. @@ -119,6 +127,16 @@ pub enum InvalidArgumentError { ThreadPoolCapacity(String), } +#[cfg(feature = "runtime-pattern")] +impl Error { + pub(crate) fn err_build_pattern(err: BuildPatternErrorInner) -> Self { + Self::BuildPattern(BuildPatternError(err)) + } + pub(crate) fn err_build_pattern_internal(err: spdlog_internal::pattern_parser::Error) -> Self { + Self::BuildPattern(BuildPatternError(BuildPatternErrorInner::Internal(err))) + } +} + /// This error indicates that an invalid logger name was set. /// /// See the documentation of [`LoggerBuilder::name`] for the name requirements. @@ -214,6 +232,22 @@ impl SendToChannelErrorDropped { } } +/// This error indicates that an error occurred while building a pattern at +/// compile-time. +#[cfg(feature = "runtime-pattern")] +#[derive(Error, Debug)] +#[error("{0}")] +pub struct BuildPatternError(BuildPatternErrorInner); + +#[cfg(feature = "runtime-pattern")] +#[derive(Error, Debug)] +pub(crate) enum BuildPatternErrorInner { + #[error("{0}")] + Internal(spdlog_internal::pattern_parser::Error), + #[error("invalid placeholder for custom pattern '{0}'")] + InvalidCustomPlaceholder(String), +} + /// The result type of this crate. pub type Result = result::Result; diff --git a/spdlog/src/formatter/pattern_formatter/mod.rs b/spdlog/src/formatter/pattern_formatter/mod.rs index 5aa5dd4f..2bb2ebbf 100644 --- a/spdlog/src/formatter/pattern_formatter/mod.rs +++ b/spdlog/src/formatter/pattern_formatter/mod.rs @@ -14,8 +14,15 @@ #[path = "pattern/mod.rs"] pub mod __pattern; +#[cfg(feature = "runtime-pattern")] +mod runtime; + use std::{fmt::Write, ops::Range, sync::Arc}; +use dyn_clone::*; +#[cfg(feature = "runtime-pattern")] +pub use runtime::*; + use crate::{ formatter::{FmtExtraInfo, FmtExtraInfoBuilder, Formatter}, Error, Record, StringBuf, @@ -41,10 +48,14 @@ use crate::{ /// # #[derive(Default)] /// # struct MyPattern; /// pattern!("text"); -/// pattern!("current line: {line}"); -/// pattern!("custom: {$my_pattern}", {$my_pattern} => MyPattern::default); +/// pattern!("current line: {line}{eol}"); +/// pattern!("custom: {$my_pattern}{eol}", {$my_pattern} => MyPattern::default); /// ``` /// +/// Its first argument accepts only a literal string that is known at compile-time. +/// If you want to build a pattern from a runtime string, use +/// [`RuntimePattern`] instead. +/// /// # Note /// /// The value returned by this macro is implementation details and users should @@ -53,12 +64,12 @@ use crate::{ /// /// # Basic Usage /// -/// In its simplest form, `pattern` receives a **literal** pattern string and +/// In its simplest form, `pattern` receives a **literal** template string and /// converts it into a zero-cost pattern: /// ``` /// use spdlog::formatter::{pattern, PatternFormatter}; /// -/// let formatter = PatternFormatter::new(pattern!("pattern string")); +/// let formatter = PatternFormatter::new(pattern!("template string")); /// ``` /// /// # Using Built-in Patterns @@ -73,7 +84,7 @@ use crate::{ /// use spdlog::info; #[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] /// -/// let formatter = PatternFormatter::new(pattern!("[{level}] {payload}")); +/// let formatter = PatternFormatter::new(pattern!("[{level}] {payload}{eol}")); /// # let (doctest, sink) = test_utils::echo_logger_from_formatter( /// # Box::new(formatter), /// # None @@ -81,8 +92,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[info] Interesting log message" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message\n" /// # ); /// ``` /// @@ -98,7 +109,7 @@ use crate::{ /// # info, /// # }; #[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] -/// let formatter = PatternFormatter::new(pattern!("[{{escaped}}] {payload}")); +/// let formatter = PatternFormatter::new(pattern!("[{{escaped}}] {payload}{eol}")); /// # let (doctest, sink) = test_utils::echo_logger_from_formatter( /// # Box::new(formatter), /// # None @@ -106,8 +117,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[{escaped}] Interesting log message" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[{escaped}] Interesting log message\n" /// # ); /// ``` /// @@ -127,7 +138,7 @@ use crate::{ /// # info, /// # }; #[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] -/// let formatter = PatternFormatter::new(pattern!("{^[{level}]} {payload}")); +/// let formatter = PatternFormatter::new(pattern!("{^[{level}]} {payload}{eol}")); /// # let (doctest, sink) = test_utils::echo_logger_from_formatter( /// # Box::new(formatter), /// # None @@ -135,8 +146,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[info] Interesting log message" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message\n" /// // ^^^^^^ <- style range /// # ); /// ``` @@ -161,17 +172,12 @@ use crate::{ /// struct MyPattern; /// /// impl Pattern for MyPattern { -/// fn format( -/// &self, -/// record: &Record, -/// dest: &mut StringBuf, -/// _ctx: &mut PatternContext, -/// ) -> spdlog::Result<()> { +/// fn format(&self, record: &Record, dest: &mut StringBuf, _: &mut PatternContext) -> spdlog::Result<()> { /// write!(dest, "My own pattern").map_err(spdlog::Error::FormatRecord) /// } /// } /// -/// let pat = pattern!("[{level}] {payload} - {$mypat}", +/// let pat = pattern!("[{level}] {payload} - {$mypat}{eol}", /// {$mypat} => MyPattern::default, /// ); /// let formatter = PatternFormatter::new(pat); @@ -182,8 +188,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[info] Interesting log message - My own pattern" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message - My own pattern\n" /// # ); /// ``` /// @@ -230,17 +236,12 @@ use crate::{ /// } /// /// impl Pattern for MyPattern { -/// fn format( -/// &self, -/// record: &Record, -/// dest: &mut StringBuf, -/// _ctx: &mut PatternContext, -/// ) -> spdlog::Result<()> { +/// fn format(&self, record: &Record, dest: &mut StringBuf, _: &mut PatternContext) -> spdlog::Result<()> { /// write!(dest, "{}", self.id).map_err(spdlog::Error::FormatRecord) /// } /// } /// -/// let pat = pattern!("[{level}] {payload} - {$mypat} {$mypat} {$mypat}", +/// let pat = pattern!("[{level}] {payload} - {$mypat} {$mypat} {$mypat}{eol}", /// {$mypat} => MyPattern::new, /// ); /// let formatter = PatternFormatter::new(pat); @@ -251,8 +252,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[info] Interesting log message - 0 1 2" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message - 0 1 2\n" /// # ); /// ``` /// @@ -265,7 +266,7 @@ use crate::{ /// # #[derive(Default)] /// # struct MyOtherPattern; /// # -/// let pat = pattern!("[{level}] {payload} - {$mypat} {$myotherpat}", +/// let pat = pattern!("[{level}] {payload} - {$mypat} {$myotherpat}{eol}", /// {$mypat} => MyPattern::default, /// {$myotherpat} => MyOtherPattern::default, /// ); @@ -284,7 +285,7 @@ use crate::{ /// # #[derive(Default)] /// # struct MyOtherPattern; /// # -/// let pattern = pattern!("[{level}] {payload} - {$mypat}", +/// let pattern = pattern!("[{level}] {payload} - {$mypat}{eol}", /// {$mypat} => MyPattern::new, /// // Error: name conflicts with another custom pattern /// {$mypat} => MyOtherPattern::new, @@ -297,7 +298,7 @@ use crate::{ /// # #[derive(Default)] /// # struct MyPattern; /// # -/// let pattern = pattern!("[{level}] {payload} - {$day}", +/// let pattern = pattern!("[{level}] {payload} - {$day}{eol}", /// // Error: name conflicts with a built-in pattern /// {$day} => MyPattern::new, /// ); @@ -349,6 +350,7 @@ use crate::{ /// [^1]: Patterns related to source location require that feature /// `source-location` is enabled, otherwise the output is empty. /// +/// [`RuntimePattern`]: crate::formatter::RuntimePattern /// [`FullFormatter`]: crate::formatter::FullFormatter pub use ::spdlog_macros::pattern; @@ -364,8 +366,10 @@ where { /// Creates a new `PatternFormatter` object with the given pattern. /// - /// Currently users can only create a `pattern` object at compile-time by - /// calling [`pattern!`] macro. + /// Currently users can only create a `pattern` object by using: + /// + /// - Macro [`pattern!`] to build a pattern at compile-time. + /// - Struct [`RuntimePattern::builder`] to build a pattern at runtime. #[must_use] pub fn new(pattern: P) -> Self { Self { pattern } @@ -428,7 +432,7 @@ impl PatternContext { /// There are 2 approaches to create your own pattern: /// - Define a new type and implements this trait; /// - Use the [`pattern`] macro to create a pattern from a template string. -pub trait Pattern: Send + Sync { +pub trait Pattern: Send + Sync + DynClone { /// Format this pattern against the given log record and write the formatted /// message into the output buffer. /// @@ -441,6 +445,7 @@ pub trait Pattern: Send + Sync { ctx: &mut PatternContext, ) -> crate::Result<()>; } +clone_trait_object!(Pattern); impl Pattern for String { fn format( @@ -478,23 +483,10 @@ where } } -impl<'a, T> Pattern for &'a mut T -where - T: ?Sized + Pattern, -{ - fn format( - &self, - record: &Record, - dest: &mut StringBuf, - ctx: &mut PatternContext, - ) -> crate::Result<()> { - ::format(*self, record, dest, ctx) - } -} - impl Pattern for Box where T: ?Sized + Pattern, + Self: Clone, { fn format( &self, @@ -509,6 +501,7 @@ where impl Pattern for Arc where T: ?Sized + Pattern, + Self: Clone, { fn format( &self, @@ -520,9 +513,10 @@ where } } -impl Pattern for [T] +impl Pattern for &[T] where T: Pattern, + Self: Clone, { fn format( &self, @@ -530,7 +524,7 @@ where dest: &mut StringBuf, ctx: &mut PatternContext, ) -> crate::Result<()> { - for p in self { + for p in *self { ::format(p, record, dest, ctx)?; } Ok(()) @@ -540,6 +534,7 @@ where impl Pattern for [T; N] where T: Pattern, + Self: Clone, { fn format( &self, @@ -547,13 +542,17 @@ where dest: &mut StringBuf, ctx: &mut PatternContext, ) -> crate::Result<()> { - <[T] as Pattern>::format(self, record, dest, ctx) + for p in self { + ::format(p, record, dest, ctx)?; + } + Ok(()) } } impl Pattern for Vec where T: Pattern, + Self: Clone, { fn format( &self, @@ -561,7 +560,10 @@ where dest: &mut StringBuf, ctx: &mut PatternContext, ) -> crate::Result<()> { - <[T] as Pattern>::format(self, record, dest, ctx) + for p in self { + ::format(p, record, dest, ctx)?; + } + Ok(()) } } @@ -585,6 +587,7 @@ macro_rules! tuple_pattern { where $($T : Pattern,)+ last!($($T,)+) : ?Sized, + Self: Clone, { fn format(&self, record: &Record, dest: &mut StringBuf, ctx: &mut PatternContext) -> crate::Result<()> { $( @@ -1255,8 +1258,12 @@ pub mod tests { #[test] fn test_pattern_mut_as_pattern() { - #[allow(clippy::needless_borrow)] - test_pattern(&mut String::from("literal"), "literal", None); + // Since we now require `T: Pattern` to implement `Clone`, there is no way to + // accept an `&mut T` as a `Pattern` anymore, since `&mut T` is not cloneable. + // + // test_pattern(&mut String::from("literal"), "literal", None); + #[allow(clippy::deref_addrof)] + test_pattern(&*&mut String::from("literal"), "literal", None); } #[test] diff --git a/spdlog/src/formatter/pattern_formatter/pattern/style_range.rs b/spdlog/src/formatter/pattern_formatter/pattern/style_range.rs index 1183cde8..82c26d79 100644 --- a/spdlog/src/formatter/pattern_formatter/pattern/style_range.rs +++ b/spdlog/src/formatter/pattern_formatter/pattern/style_range.rs @@ -23,7 +23,7 @@ where impl

Pattern for StyleRange

where - P: Pattern, + P: Pattern + Clone, { fn format( &self, diff --git a/spdlog/src/formatter/pattern_formatter/runtime.rs b/spdlog/src/formatter/pattern_formatter/runtime.rs new file mode 100644 index 00000000..4ca49157 --- /dev/null +++ b/spdlog/src/formatter/pattern_formatter/runtime.rs @@ -0,0 +1,418 @@ +use std::convert::Infallible; + +use spdlog_internal::pattern_parser::{ + error::TemplateError, + parse::{Template, TemplateToken}, + BuiltInFormatter, BuiltInFormatterInner, Error as PatternParserError, + PatternKind as GenericPatternKind, PatternRegistry as GenericPatternRegistry, + Result as PatternParserResult, +}; + +use super::{Pattern, PatternContext, __pattern as pattern}; +use crate::{ + error::{BuildPatternErrorInner, Error}, + Record, Result, StringBuf, +}; +type Patterns = Vec>; +type PatternCreator = Box Box>; +type PatternRegistry = GenericPatternRegistry; +type PatternKind = GenericPatternKind; + +#[rustfmt::skip] // rustfmt currently breaks some empty lines if `#[doc = include_str!("xxx")]` exists +/// A pattern built at runtime. +/// +/// If your pattern is known at compile-time and you don't need to build new +/// patterns from runtime input, consider using [`pattern!`] macro. +/// +/// # Usage +/// +/// The template string format and built-in patterns are consistent with +/// [`pattern!`] macro. The only difference is the way they are built and that +/// one of them is built at compile-time and the other at runtime. +/// +/// For other usage, please see the documentation of [`pattern!`] macro. +/// +/// ## Basic Usage +/// +/// ``` +/// # use spdlog::formatter::{PatternFormatter, RuntimePattern}; +/// use spdlog::info; +/// +/// # +#[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] +/// # fn main() -> Result<(), Box> { +/// let formatter = PatternFormatter::new(RuntimePattern::new("[{level}] {payload}{eol}")?); +/// # let (doctest, sink) = test_utils::echo_logger_from_formatter( +/// # Box::new(formatter), +/// # None +/// # ); +/// +/// info!(logger: doctest, "Interesting log message"); +/// # assert_eq!( +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message\n" +/// # ); +/// # Ok(()) } +/// ``` +/// +/// ## With Custom Patterns +/// +/// ``` +/// use std::fmt::Write; +/// +/// use spdlog::{ +/// formatter::{pattern, Pattern, PatternContext, PatternFormatter, RuntimePattern}, +/// Record, StringBuf, info +/// }; +/// +/// #[derive(Default, Clone)] +/// struct MyPattern; +/// +/// impl Pattern for MyPattern { +/// fn format(&self, record: &Record, dest: &mut StringBuf, _: &mut PatternContext) -> spdlog::Result<()> { +/// write!(dest, "My own pattern").map_err(spdlog::Error::FormatRecord) +/// } +/// } +/// +#[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] +/// # fn main() -> Result<(), Box> { +/// let formatter = PatternFormatter::new( +/// RuntimePattern::builder() +/// .template("[{level}] {payload} - {$mypat1} {$mypat2}{eol}") +/// .custom_pattern("mypat1", MyPattern::default) +/// .custom_pattern("mypat2", || pattern!("[{level_short}-{level}]")) +/// .build()? +/// ); +/// # let (doctest, sink) = test_utils::echo_logger_from_formatter( +/// # Box::new(formatter), +/// # None +/// # ); +/// +/// info!(logger: doctest, "Interesting log message"); +/// # assert_eq!( +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message - My own pattern [I-info]\n" +/// # ); +/// # Ok(()) } +/// ``` +/// +/// [`pattern!`]: crate::formatter::pattern +#[derive(Clone)] +pub struct RuntimePattern(Patterns); + +impl RuntimePattern { + /// Constructs a runtime pattern from a template string. + /// + /// About the template string format, please see the documentation of + /// [`pattern!`] macro. + /// + /// [`pattern!`]: crate::formatter::pattern + pub fn new(template: T) -> Result + where + T: Into, + { + Self::builder().template(template).build() + } + + /// Constructs a [`RuntimePatternBuilder`] to build a runtime pattern with + /// advanced parameters. + pub fn builder() -> RuntimePatternBuilder<()> { + RuntimePatternBuilder { + template: (), + custom_patterns: Vec::new(), + } + } +} + +impl Pattern for RuntimePattern { + fn format( + &self, + record: &Record, + dest: &mut StringBuf, + ctx: &mut PatternContext, + ) -> Result<()> { + for pattern in &self.0 { + pattern.format(record, dest, ctx)?; + } + Ok(()) + } +} + +#[rustfmt::skip] // rustfmt currently breaks some empty lines if `#[doc = include_str!("xxx")]` exists +/// The builder of [`RuntimePattern`]. +#[doc = include_str!("../../include/doc/generic-builder-note.md")] +/// +/// # Example +/// +/// See the documentation of [`RuntimePattern`]. +pub struct RuntimePatternBuilder { + template: ArgT, + custom_patterns: Vec<(String, PatternCreator)>, +} + +impl RuntimePatternBuilder { + /// Specifies the template string. + /// + /// This parameter is **required**. + /// + /// About the template string format, please see the documentation of + /// [`pattern!`] macro. + /// + /// [`pattern!`]: crate::formatter::pattern + pub fn template(self, template: S) -> RuntimePatternBuilder + where + S: Into, + { + RuntimePatternBuilder { + template: template.into(), + custom_patterns: self.custom_patterns, + } + } + + /// Specifies a creator for a custom pattern that appears in the template + /// string. + /// + /// This parameter is **optional** if there is no reference to a custom + /// pattern in the template string, otherwise it's **required**. + /// + /// It is conceptually equivalent to `{$my_pat} => MyPattern::new` in + /// [`pattern!`] macro. + /// + /// The placeholder argument must be an identifier, e.g. `"my_pat"`, + /// `"_my_pat"`, etc., it cannot be `"2my_pat"`, `"r#my_pat"`, `"3"`, etc. + /// + /// [`pattern!`]: crate::formatter::pattern + pub fn custom_pattern(mut self, placeholder: S, pattern_creator: F) -> Self + where + S: Into, + P: Pattern + 'static, + F: Fn() -> P + 'static, + { + self.custom_patterns.push(( + placeholder.into(), + Box::new(move || Box::new(pattern_creator())), + )); + self + } +} + +impl RuntimePatternBuilder<()> { + #[doc(hidden)] + #[deprecated(note = "\n\n\ + builder compile-time error:\n\ + - missing required field `template`\n\n\ + ")] + pub fn build(self, _: Infallible) {} +} + +impl RuntimePatternBuilder { + /// Builds a runtime pattern. + pub fn build(self) -> Result { + self.build_inner() + } + + fn build_inner(self) -> Result { + let mut registry = PatternRegistry::with_builtin(); + for (name, formatter) in self.custom_patterns { + if !(!name.is_empty() + && name + .chars() + .next() + .map(|ch| ch.is_ascii_alphabetic() || ch == '_') + .unwrap() + && name + .chars() + .skip(1) + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')) + { + return Err(Error::err_build_pattern( + BuildPatternErrorInner::InvalidCustomPlaceholder(name), + )); + } + registry + .register_custom(name, formatter) + .map_err(Error::err_build_pattern_internal)?; + } + + let template = + Template::parse(&self.template).map_err(Error::err_build_pattern_internal)?; + + Synthesiser::new(registry) + .synthesize(template) + .map_err(Error::err_build_pattern_internal) + .map(RuntimePattern) + } +} + +struct Synthesiser { + registry: PatternRegistry, +} + +impl Synthesiser { + fn new(registry: PatternRegistry) -> Self { + Self { registry } + } + + fn synthesize(&self, template: Template) -> PatternParserResult { + self.build_patterns(template, false) + } + + fn build_patterns( + &self, + template: Template, + mut style_range_seen: bool, + ) -> PatternParserResult { + let mut patterns = Patterns::new(); + + for token in template.tokens { + let pattern = match token { + TemplateToken::Literal(t) => Box::new(t.literal), + TemplateToken::Formatter(t) => { + let pattern = self.registry.find(t.has_custom_prefix, t.placeholder)?; + match pattern { + PatternKind::BuiltIn(builtin) => build_builtin_pattern(builtin), + PatternKind::Custom { factory, .. } => factory(), + } + } + TemplateToken::StyleRange(style_range) => { + if style_range_seen { + return Err(PatternParserError::Template( + TemplateError::MultipleStyleRange, + )); + } + style_range_seen = true; + Box::new(pattern::StyleRange::new( + self.build_patterns(style_range.body, true)?, + )) + } + }; + patterns.push(pattern); + } + + Ok(patterns) + } +} + +fn build_builtin_pattern(builtin: &BuiltInFormatter) -> Box { + macro_rules! match_builtin { + ( $($name:ident),+ $(,)? ) => { + match builtin.inner() { + $(BuiltInFormatterInner::$name => Box::::default()),+ + } + }; + } + + match_builtin!( + AbbrWeekdayName, + WeekdayName, + AbbrMonthName, + MonthName, + FullDateTime, + ShortYear, + Year, + ShortDate, + Date, + Month, + Day, + Hour, + Hour12, + Minute, + Second, + Millisecond, + Microsecond, + Nanosecond, + AmPm, + Time12, + ShortTime, + Time, + TzOffset, + UnixTimestamp, + Full, + Level, + ShortLevel, + Source, + SourceFilename, + SourceFile, + SourceLine, + SourceColumn, + SourceModulePath, + LoggerName, + Payload, + ProcessId, + ThreadId, + Eol + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn builder(template: &str) -> RuntimePatternBuilder { + RuntimePattern::builder().template(template) + } + + fn new(template: &str) -> Result { + RuntimePattern::new(template) + } + + fn custom_pat_creator() -> impl Pattern { + pattern::Level + } + + #[test] + fn valid() { + assert!(new("").is_ok()); + assert!(new("{logger}").is_ok()); + assert!(builder("{logger} {$custom_pat}") + .custom_pattern("custom_pat", custom_pat_creator) + .build() + .is_ok()); + assert!(builder("{logger} {$_custom_pat}") + .custom_pattern("_custom_pat", custom_pat_creator) + .build() + .is_ok()); + assert!(builder("{logger} {$_2custom_pat}") + .custom_pattern("_2custom_pat", custom_pat_creator) + .build() + .is_ok()); + } + + #[test] + fn invalid() { + assert!(matches!(new("{logger-name}"), Err(Error::BuildPattern(_)))); + assert!(matches!(new("{nonexistent}"), Err(Error::BuildPattern(_)))); + assert!(matches!(new("{}"), Err(Error::BuildPattern(_)))); + assert!(matches!( + new("{logger} {$custom_pat_no_ref}"), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + builder("{logger} {$custom_pat}") + .custom_pattern("custom_pat", custom_pat_creator) + .custom_pattern("", custom_pat_creator) + .build(), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + builder("{logger} {$custom_pat}") + .custom_pattern("custom_pat", custom_pat_creator) + .custom_pattern("custom-pat2", custom_pat_creator) + .build(), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + builder("{logger} {$custom_pat}") + .custom_pattern("custom_pat", custom_pat_creator) + .custom_pattern("2custom_pat", custom_pat_creator) + .build(), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + builder("{logger} {$r#custom_pat}") + .custom_pattern("r#custom_pat", custom_pat_creator) + .build(), + Err(Error::BuildPattern(_)) + )); + } +} diff --git a/spdlog/src/lib.rs b/spdlog/src/lib.rs index 24daf7c7..0007b62b 100644 --- a/spdlog/src/lib.rs +++ b/spdlog/src/lib.rs @@ -58,6 +58,16 @@ //! [open a discussion]. For feature requests or bug reports, please [open an //! issue]. //! +//! # Overview of features +//! +//! - [Compatible with log crate](#compatible-with-log-crate) +//! - [Asynchronous support](#asynchronous-support) +//! - [Configured via environment +//! variable](#configured-via-environment-variable) +//! - [Compile-time and runtime pattern +//! formatter](#compile-time-and-runtime-pattern-formatter) +//! - [Compile-time filters](#compile-time-filters) +//! //! # Compatible with log crate //! //! This is optional and is controlled by crate feature `log`. @@ -85,7 +95,35 @@ //! //! For more details, see the documentation of [`init_env_level`]. //! -//! # Compile time filters +//! # Compile-time and runtime pattern formatter +//! +//! spdlog-rs supports formatting your log records according to a pattern +//! string. There are 2 ways to construct a pattern: +//! +//! - Macro [`pattern!`]: Builds a pattern at compile-time. +//! - Struct [`RuntimePattern`]: A pattern built at runtime. +//! +//! ``` +//! use spdlog::formatter::{pattern, PatternFormatter}; +//! #[cfg(feature = "runtime-pattern")] +//! use spdlog::formatter::RuntimePattern; +//! # use spdlog::sink::{Sink, WriteSink}; +//! +//! # fn main() -> Result<(), Box> { +//! // This pattern is built at compile-time, the template accepts only a literal string. +//! let pattern = pattern!("[{date} {time}.{millisecond}] [{level}] {payload}{eol}"); +//! +//! // This pattern is built at runtime, the template accepts a runtime string. +//! #[cfg(feature = "runtime-pattern")] +//! let pattern = RuntimePattern::new("[{date} {time}.{millisecond}] [{level}] {payload}{eol}")?; +//! +//! // Use the compile-time or runtime pattern. +//! # let your_sink = WriteSink::builder().target(vec![]).build()?; +//! your_sink.set_formatter(Box::new(PatternFormatter::new(pattern))); +//! # Ok(()) } +//! ``` +//! +//! # Compile-time filters //! //! Log levels can be statically disabled at compile time via Cargo features. //! Log invocations at disabled levels will be skipped and will not even be @@ -139,6 +177,8 @@ //! features need to be enabled as well. See the documentation of the //! component for these details. //! +//! - `runtime-pattern` +//! //! # Supported Rust Versions //! //!