diff --git a/spdlog/Cargo.toml b/spdlog/Cargo.toml index cbce1f8c..c4ca533c 100644 --- a/spdlog/Cargo.toml +++ b/spdlog/Cargo.toml @@ -32,6 +32,7 @@ release-level-info = [] release-level-debug = [] release-level-trace = [] +config = ["serde", "erased-serde", "toml"] source-location = [] native = [] libsystemd = ["libsystemd-sys"] @@ -45,15 +46,18 @@ cfg-if = "1.0.0" chrono = "0.4.22" crossbeam = { version = "0.8.2", optional = true } dyn-clone = "1.0.14" +erased-serde = { version = "0.3.31", optional = true } 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" +serde = { version = "1.0.163", optional = true } spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal", optional = true } spdlog-macros = { version = "0.1.0", path = "../spdlog-macros" } spin = "0.9.8" thiserror = "1.0.37" +toml = { version = "0.8.8", optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["consoleapi", "debugapi", "handleapi", "processenv", "processthreadsapi", "winbase", "wincon"] } @@ -81,6 +85,7 @@ tracing-subscriber = "=0.3.16" tracing-appender = "=0.2.2" paste = "1.0.14" trybuild = "1.0.90" +toml = "0.8.6" [build-dependencies] rustc_version = "0.4.0" diff --git a/spdlog/src/config/deser.rs b/spdlog/src/config/deser.rs new file mode 100644 index 00000000..bf35e5ed --- /dev/null +++ b/spdlog/src/config/deser.rs @@ -0,0 +1,145 @@ +use std::{marker::PhantomData, result::Result as StdResult}; + +use erased_serde::Deserializer as ErasedDeserializer; +use serde::{ + de::{ + value::{MapAccessDeserializer, UnitDeserializer}, + Error as SerdeDeError, MapAccess, Visitor, + }, + Deserialize, Deserializer, +}; + +use crate::{config, formatter::*, sync::*, Logger, LoggerBuilder, LoggerParams, Result, Sink}; + +trait Component { + type Value; + + fn expecting(formatter: &mut std::fmt::Formatter) -> std::fmt::Result; + fn build(name: &str, de: &mut dyn ErasedDeserializer) -> Result; +} + +struct ComponentFormatter; + +impl Component for ComponentFormatter { + type Value = Box; + + fn expecting(formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a spdlog-rs formatter") + } + + fn build(name: &str, de: &mut dyn ErasedDeserializer) -> Result { + config::registry().build_formatter(&name, de) + } +} + +struct ComponentSink; + +impl Component for ComponentSink { + type Value = Arc; + + fn expecting(formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a spdlog-rs sink") + } + + fn build(name: &str, de: &mut dyn ErasedDeserializer) -> Result { + config::registry().build_sink(&name, de) + } +} + +// Unit for 0 parameter components, map for components with parameters +struct UnitOrMapDeserializer { + map: A, +} + +impl<'de, A> Deserializer<'de> for UnitOrMapDeserializer +where + A: MapAccess<'de>, +{ + type Error = A::Error; + + fn deserialize_any(self, visitor: V) -> StdResult + where + V: Visitor<'de>, + { + visitor.visit_map(self.map) + } + + fn deserialize_unit(self, visitor: V) -> StdResult + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_newtype_struct( + self, + name: &'static str, + visitor: V, + ) -> StdResult + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option enum unit_struct seq tuple + tuple_struct map struct identifier ignored_any + } +} + +struct ComponentVisitor(PhantomData); + +impl<'de, C> Visitor<'de> for ComponentVisitor +where + C: Component, +{ + type Value = C::Value; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + C::expecting(formatter) + } + + fn visit_map(self, mut map: A) -> StdResult + where + A: MapAccess<'de>, + { + let name = map + .next_entry::()? + .filter(|(key, _)| key == "name") + .map(|(_, value)| value) + .ok_or_else(|| SerdeDeError::missing_field("name"))?; + + let mut erased_de = ::erase(UnitOrMapDeserializer { map }); + let component = C::build(&name, &mut erased_de).map_err(SerdeDeError::custom)?; + + Ok(component) + } +} + +pub fn formatter<'de, D>(de: D) -> StdResult>, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Some(de.deserialize_map(ComponentVisitor::< + ComponentFormatter, + >(PhantomData))?)) +} + +pub fn sink<'de, D>(de: D) -> StdResult>, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Some(de.deserialize_map( + ComponentVisitor::(PhantomData), + )?)) +} + +pub fn logger<'de, D>(de: D) -> StdResult +where + D: Deserializer<'de>, +{ + let params = LoggerParams::deserialize(de)?; + LoggerBuilder::build_config(params).map_err(SerdeDeError::custom) +} diff --git a/spdlog/src/config/mod.rs b/spdlog/src/config/mod.rs new file mode 100644 index 00000000..5539b6a8 --- /dev/null +++ b/spdlog/src/config/mod.rs @@ -0,0 +1,254 @@ +mod registry; +mod source; + +pub(crate) mod deser; + +use std::{ + cell::RefCell, + collections::{hash_map::Entry, HashMap}, + convert::Infallible, +}; + +pub use registry::*; +use serde::{de::DeserializeOwned, Deserialize}; +pub use source::*; + +use crate::{sync::*, Logger, LoggerBuilder, LoggerParams, Result}; + +// TODO: Builder? +#[derive(PartialEq, Eq, Hash)] +pub struct ComponentMetadata { + name: &'static str, +} + +impl ComponentMetadata { + pub fn builder() -> ComponentMetadataBuilder<()> { + ComponentMetadataBuilder { name: () } + } +} + +pub struct ComponentMetadataBuilder { + name: ArgName, +} + +impl ComponentMetadataBuilder { + pub fn name(self, name: &'static str) -> ComponentMetadataBuilder<&'static str> { + ComponentMetadataBuilder { name } + } +} + +impl ComponentMetadataBuilder<()> { + #[doc(hidden)] + #[deprecated(note = "\n\n\ + builder compile-time error:\n\ + - missing required field `name`\n\n\ + ")] + pub fn build(self, _: Infallible) {} +} + +impl ComponentMetadataBuilder<&'static str> { + pub fn build(self) -> ComponentMetadata { + ComponentMetadata { name: self.name } + } +} + +pub trait Configurable: Sized { + type Params: DeserializeOwned + Default + Send; + + fn metadata() -> ComponentMetadata; + fn build(params: Self::Params) -> Result; +} + +// #[derive(Deserialize)] +// #[serde(deny_unknown_fields)] +// struct Logger(#[serde(deserialize_with = +// "crate::config::deser::logger")] crate::Logger); + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ConfigView { + loggers: HashMap, +} + +#[derive(Debug, Hash, Eq, PartialEq)] +enum LoggerKind { + Default, + Named(String), +} + +pub struct Config { + view: ConfigView, // Stores the config values only, build loggers lazily + built: RefCell>>, +} +// TODO: But only build once! For later acquires, return the built `Arc` +// Stores `Weak`? + +impl Config { + pub fn acquire_default_logger(&self) -> Option>> { + self.acquire_logger_inner(LoggerKind::Default) + } + + pub fn acquire_logger(&self, name: S) -> Option>> + where + S: AsRef, + { + self.acquire_logger_inner(LoggerKind::Named(name.as_ref().into())) + } +} + +impl Config { + fn acquire_logger_inner(&self, logger_kind: LoggerKind) -> Option>> { + let logger_name = match &logger_kind { + LoggerKind::Default => "default", + LoggerKind::Named(name) => name, + }; + let logger_params = self.view.loggers.get(logger_name)?; + + // TODO: Factually unnecessary clone in the argument of `build_config`, could be + // avoided with some effort + Some((|| match self.built.borrow_mut().entry(logger_kind) { + Entry::Occupied(mut entry) => match entry.get().upgrade() { + None => { + let new = Arc::new(LoggerBuilder::build_config(logger_params.clone())?); + entry.insert(Arc::downgrade(&new)); + Ok(new) + } + Some(built) => Ok(built), + }, + Entry::Vacant(entry) => { + let new = Arc::new(LoggerBuilder::build_config(logger_params.clone())?); + entry.insert(Arc::downgrade(&new)); + Ok(new) + } + })()) + } +} + +// TODO: temp code +impl Config { + // TODO: Remember to remove me + pub fn new_for_test(inputs: &str) -> Result { + let view = toml::from_str(inputs).unwrap(); + Ok(Self { + view, + built: RefCell::new(HashMap::new()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{config::*, TEST_LOGS_PATH}; + + #[test] + fn full() { + let path = TEST_LOGS_PATH.join("unit_test_config_full.log"); + let inputs = format!( + r#" +[loggers.default] +sinks = [ + {{ name = "$ConfigMockSink1", arg = 114 }}, + {{ name = "$ConfigMockSink2", arg = 514 }}, + {{ name = "$ConfigMockSink3", arg = 1919 }}, + {{ name = "FileSink", path = "{}", formatter = {{ name = "PatternFormatter", template = "Meow! {{payload}}{{eol}}" }} }} +] +flush_level_filter = "Equal(Info)" # TODO: reconsider the syntax + +[loggers.network] +sinks = [ {{ name = "$ConfigMockSink2", arg = 810 }} ] +# TODO: flush_period = "10s" + "#, + path.display() + ); + + register_global(); + + let config = Config::new_for_test(&inputs).unwrap(); + + assert_eq!( + config.view, + ConfigView { + loggers: HashMap::from([ + ( + "default".to_string(), + toml::Value::Table(toml::Table::from_iter([ + ( + "sinks".to_string(), + toml::Value::Array(vec![ + toml::Value::Table(toml::Table::from_iter([ + ( + "name".to_string(), + toml::Value::String("$ConfigMockSink1".to_string()) + ), + ("arg".to_string(), toml::Value::Integer(114)) + ])), + toml::Value::Table(toml::Table::from_iter([ + ( + "name".to_string(), + toml::Value::String("$ConfigMockSink2".to_string()) + ), + ("arg".to_string(), toml::Value::Integer(514)) + ])), + toml::Value::Table(toml::Table::from_iter([ + ( + "name".to_string(), + toml::Value::String("$ConfigMockSink3".to_string()) + ), + ("arg".to_string(), toml::Value::Integer(1919)) + ])), + toml::Value::Table(toml::Table::from_iter([ + ( + "name".to_string(), + toml::Value::String("FileSink".to_string()) + ), + ( + "path".to_string(), + toml::Value::String(path.display().to_string()) + ), + ( + "formatter".to_string(), + toml::Value::Table(toml::Table::from_iter([ + ( + "name".to_string(), + toml::Value::String( + "PatternFormatter".to_string() + ), + ), + ( + "template".to_string(), + toml::Value::String( + "Meow! {payload}{eol}".to_string() + ), + ) + ])) + ) + ])) + ]) + ), + ( + "flush_level_filter".to_string(), + toml::Value::String("Equal(Info)".to_string()) + ) + ])) + ), + ( + "network".to_string(), + toml::Value::Table(toml::Table::from_iter([( + "sinks".to_string(), + toml::Value::Array(vec![toml::Value::Table(toml::Table::from_iter([ + ( + "name".to_string(), + toml::Value::String("$ConfigMockSink2".to_string()) + ), + ("arg".to_string(), toml::Value::Integer(810)) + ]))]) + )])) + ) + ]) + } + ); + + // TODO + } +} diff --git a/spdlog/src/config/registry.rs b/spdlog/src/config/registry.rs new file mode 100644 index 00000000..c05ed8f8 --- /dev/null +++ b/spdlog/src/config/registry.rs @@ -0,0 +1,248 @@ +use std::collections::HashMap; + +use erased_serde::Deserializer as ErasedDeserializer; + +use super::ComponentMetadata; +use crate::{ + config::Configurable, + error::ConfigError, + formatter::{Formatter, FullFormatter, PatternFormatter, RuntimePattern}, + sink::*, + sync::*, + Error, Logger, Result, Sink, +}; + +type StdResult = std::result::Result>; + +// https://github.com/dtolnay/erased-serde/issues/97 +mod erased_serde_ext { + use erased_serde::Result; + use serde::de::Deserialize; + + use super::*; + + pub trait ErasedDeserialize<'a> { + fn erased_deserialize_in_place( + &mut self, + de: &mut dyn ErasedDeserializer<'a>, + ) -> Result<()>; + } + + pub trait ErasedDeserializeOwned: for<'a> ErasedDeserialize<'a> {} + + impl ErasedDeserialize<'a>> ErasedDeserializeOwned for T {} + + impl<'a, T: Deserialize<'a>> ErasedDeserialize<'a> for T { + fn erased_deserialize_in_place( + &mut self, + de: &mut dyn ErasedDeserializer<'a>, + ) -> Result<()> { + Deserialize::deserialize_in_place(de, self) + } + } +} +use erased_serde_ext::*; + +type ComponentDeser = fn(de: &mut dyn ErasedDeserializer) -> Result; +type RegisteredComponents = HashMap<&'static str, ComponentDeser>; + +pub struct Registry { + // TODO: Consider make them compile-time + builtin_sink: Mutex>>, + builtin_formatter: Mutex>>, + + custom_sink: Mutex>>, + custom_formatter: Mutex>>, +} + +impl Registry { + pub fn register_sink(&self) -> Result<()> + where + S: Sink + Configurable + 'static, + { + self.register_sink_inner::() + } + + pub fn register_formatter(&self) -> Result<()> + where + F: Formatter + Configurable + 'static, + { + self.register_formatter_inner::() + } +} + +macro_rules! deser_closure { + ( $wrap:ident ) => { + deser_closure!(@INNER, $wrap, $wrap::new) + }; + ( @INNER, $ret_ty:ty, $ret_expr:expr ) => { + |de: &mut dyn ErasedDeserializer| -> Result<$ret_ty> { + let mut params = C::Params::default(); + params + .erased_deserialize_in_place(de) + .map_err(|err| Error::Config(ConfigError::BuildComponent(err.to_string())))?; + Ok($ret_expr(C::build(params)?)) + } + }; +} + +impl Registry { + pub(crate) fn with_builtin() -> Self { + let mut registry = Self { + builtin_sink: Mutex::new(HashMap::new()), + builtin_formatter: Mutex::new(HashMap::new()), + custom_sink: Mutex::new(HashMap::new()), + custom_formatter: Mutex::new(HashMap::new()), + }; + registry.register_builtin().unwrap(); // Builtin components should not fail to register + registry + } + + fn register_builtin(&mut self) -> Result<()> { + self.register_builtin_sink::()?; + self.register_builtin_formatter::()?; + self.register_builtin_formatter::>()?; + Ok(()) + } + + pub(crate) fn build_sink( + &self, + name: &str, + de: &mut dyn ErasedDeserializer, + ) -> Result> { + let (registered, name) = if !name.starts_with('$') { + (&self.builtin_sink, name) + } else { + (&self.custom_sink, name.strip_prefix('$').unwrap()) + }; + registered + .lock_expect() + .get(name) + .ok_or_else(|| Error::Config(ConfigError::UnknownComponent(name.to_string()))) + .and_then(|f| f(de)) + } + + pub(crate) fn build_formatter( + &self, + name: &str, + de: &mut dyn ErasedDeserializer, + ) -> Result> { + let (registered, name) = if !name.starts_with('$') { + (&self.builtin_formatter, name) + } else { + (&self.custom_formatter, name.strip_prefix('$').unwrap()) + }; + registered + .lock_expect() + .get(name) + .ok_or_else(|| Error::Config(ConfigError::UnknownComponent(name.to_string()))) + .and_then(|f| f(de)) + } + + fn register_sink_inner(&self) -> Result<()> + where + C: Sink + Configurable + 'static, + { + self.custom_sink + .lock_expect() + .insert(C::metadata().name, deser_closure!(Arc)) + .map_or(Ok(()), |_| { + Err(Error::Config(ConfigError::MultipleRegistration)) + }) + } + + fn register_formatter_inner(&self) -> Result<()> + where + C: Formatter + Configurable + 'static, + { + self.custom_formatter + .lock_expect() + .insert(C::metadata().name, deser_closure!(Box)) + .map_or(Ok(()), |_| { + Err(Error::Config(ConfigError::MultipleRegistration)) + }) + } + + fn register_builtin_sink(&self) -> Result<()> + where + C: Sink + Configurable + 'static, + { + self.builtin_sink + .lock_expect() + .insert(C::metadata().name, deser_closure!(Arc)) + .map_or(Ok(()), |_| { + Err(Error::Config(ConfigError::MultipleRegistration)) + }) + } + + fn register_builtin_formatter(&self) -> Result<()> + where + C: Formatter + Configurable + 'static, + { + self.builtin_formatter + .lock_expect() + .insert(C::metadata().name, deser_closure!(Box)) + .map_or(Ok(()), |_| { + Err(Error::Config(ConfigError::MultipleRegistration)) + }) + } +} + +// TODO: Consider removing the `'static` lifetime. Maybe using `Arc<>`? +pub(crate) fn registry() -> &'static Registry { + static REGISTRY: Lazy = Lazy::new(Registry::with_builtin); + ®ISTRY +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{prelude::*, test_utils::config::*, Record, StringBuf}; + + #[test] + fn build_sink_from_params() { + let registry = registry_for_test(); + + let mut erased_de = + ::erase(toml::Deserializer::new("arg = 114514")); + let sink = registry + .build_sink("$ConfigMockSink2", &mut erased_de) + .unwrap(); + assert!(matches!( + sink.flush(), + Err(Error::__ForInternalTestsUseOnly(2, 114514)) + )); + + let mut erased_de = + ::erase(toml::Deserializer::new("unmatched_arg = 114514")); + assert!(matches!( + registry.build_sink("$ConfigMockSink2", &mut erased_de), + Err(Error::Config(ConfigError::BuildComponent(_))) + )); + + let mut erased_de = + ::erase(toml::Deserializer::new("arg = 114514")); + assert!(matches!( + registry.build_sink("$ConfigMockSinkUnregistered", &mut erased_de), + Err(Error::Config(ConfigError::UnknownComponent(_))) + )); + } + + #[test] + fn build_formatter_from_params() { + let registry = registry_for_test(); + + let mut erased_de = + ::erase(toml::Deserializer::new("arg = 1919810")); + let formatter = registry + .build_formatter("$ConfigMockFormatter", &mut erased_de) + .unwrap(); + let mut dest = StringBuf::new(); + formatter + .format(&Record::new(Level::Info, ""), &mut dest) + .unwrap(); + assert_eq!(dest, "1919810") + } + + // TODO: Test custom components +} diff --git a/spdlog/src/config/source.rs b/spdlog/src/config/source.rs new file mode 100644 index 00000000..9478fe8c --- /dev/null +++ b/spdlog/src/config/source.rs @@ -0,0 +1,18 @@ +use std::{marker::PhantomData, path::Path}; + +pub trait Source { + fn file

(path: P) + where + P: AsRef, + { + } +} + +// TODO: place it into a format mod? +pub struct Toml { + phantom: PhantomData<()>, +} + +impl Source for Toml { + // +} diff --git a/spdlog/src/error.rs b/spdlog/src/error.rs index 65fd60fd..b4afdd7d 100644 --- a/spdlog/src/error.rs +++ b/spdlog/src/error.rs @@ -104,9 +104,12 @@ pub enum Error { #[error("{0:?}")] Multiple(Vec), + #[error("TODO: {0}")] + Config(ConfigError), // TODO: Better name? + #[cfg(test)] #[error("{0}")] - __ForInternalTestsUseOnly(i32), + __ForInternalTestsUseOnly(i32, i32), } /// This error type contains a variety of possible invalid arguments. @@ -257,6 +260,16 @@ impl SendToChannelErrorDropped { #[error("{0}")] pub struct BuildPatternError(pub(crate) spdlog_internal::pattern_parser::Error); +#[derive(Error, Debug)] +pub enum ConfigError { + #[error("TODO: MultipleRegistration")] + MultipleRegistration, // TODO: arg? + #[error("TODO: BuildComponent: {0}")] + BuildComponent(String /* TODO: other type? */), + #[error("TODO: UnknownComponent ({0})")] + UnknownComponent(String), // TODO: Better name? Distinguish builtin and custom +} + /// The result type of this crate. pub type Result = result::Result; diff --git a/spdlog/src/formatter/full_formatter.rs b/spdlog/src/formatter/full_formatter.rs index b919408d..274d2dcc 100644 --- a/spdlog/src/formatter/full_formatter.rs +++ b/spdlog/src/formatter/full_formatter.rs @@ -1,12 +1,16 @@ //! Provides a full info formatter. -use std::fmt::{self, Write}; +use std::{ + fmt::{self, Write}, + result::Result as StdResult, +}; use cfg_if::cfg_if; use crate::{ + config::{ComponentMetadata, Configurable}, formatter::{FmtExtraInfo, Formatter, LOCAL_TIME_CACHER}, - Error, Record, StringBuf, __EOL, + Error, Record, Result, StringBuf, EOL, __EOL, }; #[rustfmt::skip] @@ -54,7 +58,7 @@ impl FullFormatter { &self, record: &Record, dest: &mut StringBuf, - ) -> Result { + ) -> StdResult { cfg_if! { if #[cfg(not(feature = "flexible-string"))] { dest.reserve(crate::string_buf::RESERVE_SIZE); @@ -120,6 +124,18 @@ impl Default for FullFormatter { } } +impl Configurable for FullFormatter { + type Params = (); + + fn metadata() -> ComponentMetadata { + ComponentMetadata::builder().name("FullFormatter").build() + } + + fn build(_params: Self::Params) -> Result { + Ok(FullFormatter::new()) + } +} + #[cfg(test)] mod tests { use chrono::prelude::*; diff --git a/spdlog/src/formatter/mod.rs b/spdlog/src/formatter/mod.rs index 878812d6..e900c63b 100644 --- a/spdlog/src/formatter/mod.rs +++ b/spdlog/src/formatter/mod.rs @@ -112,3 +112,20 @@ impl FmtExtraInfoBuilder { self.info } } + +/// There is no easy way to implement `PartialEq` for `dyn T`. we just do it for +/// testing, so we implement it this way +#[cfg(test)] +impl PartialEq for dyn Formatter { + fn eq(&self, other: &Self) -> bool { + let record = Record::new(crate::Level::Critical, "this is a mock record"); + + let (mut self_result, mut other_result) = (StringBuf::new(), StringBuf::new()); + let (self_extra, other_extra) = ( + self.format(&record, &mut self_result).unwrap(), + other.format(&record, &mut other_result).unwrap(), + ); + + (self_result, self_extra) == (other_result, other_extra) + } +} diff --git a/spdlog/src/formatter/pattern_formatter/runtime.rs b/spdlog/src/formatter/pattern_formatter/runtime.rs index ef768e7e..bffafd01 100644 --- a/spdlog/src/formatter/pattern_formatter/runtime.rs +++ b/spdlog/src/formatter/pattern_formatter/runtime.rs @@ -1,3 +1,6 @@ +use std::convert::Infallible; + +use serde::Deserialize; use spdlog_internal::pattern_parser::{ error::TemplateError, parse::{Template, TemplateToken}, @@ -6,9 +9,10 @@ use spdlog_internal::pattern_parser::{ Result as PatternParserResult, }; -use super::{Pattern, PatternContext, __pattern as pattern}; +use super::{Pattern, PatternContext, PatternFormatter, __pattern as pattern}; use crate::{ - error::{BuildPatternError, Error}, + config::{ComponentMetadata, Configurable}, + error::{BuildPatternError, BuildPatternErrorInner, Error}, Record, Result, StringBuf, }; @@ -159,6 +163,120 @@ impl Pattern for RuntimePattern { } } +#[derive(Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +#[serde(deny_unknown_fields)] +#[doc(hidden)] +pub struct PatternFormatterRuntimePatternParams { + template: String, +} + +#[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, } @@ -258,3 +376,105 @@ fn build_builtin_pattern(builtin: &BuiltInFormatter) -> Box { Eol ) } + +impl Configurable for PatternFormatter { + type Params = PatternFormatterRuntimePatternParams; + + fn metadata() -> ComponentMetadata { + ComponentMetadata::builder() + .name("PatternFormatter") + .build() + } + + fn build(params: Self::Params) -> Result { + Ok(Self::new(RuntimePattern::new(params.template)?)) + } +} + +#[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(_)) + )); + } + + #[test] + fn deser_params() { + assert!( + toml::from_str::( + r#"template = "[{level}] {payload}""#, + ) + .unwrap() + == PatternFormatterRuntimePatternParams { + template: "[{level}] {payload}".to_string() + } + ); + + // TODO: Test ill-formed template string err + } +} diff --git a/spdlog/src/level.rs b/spdlog/src/level.rs index 92afaff4..ba6f17ed 100644 --- a/spdlog/src/level.rs +++ b/spdlog/src/level.rs @@ -55,6 +55,7 @@ const LOG_LEVEL_SHORT_NAMES: [&str; Level::count()] = ["C", "E", "W", "I", "D", /// [`log!`]: crate::log! #[repr(u16)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] pub enum Level { /// Designates critical errors. Critical = 0, @@ -222,6 +223,33 @@ pub enum LevelFilter { All, } +impl<'de> serde::Deserialize<'de> for LevelFilter { + fn deserialize(de: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct LevelFilterExprVisitor; + + impl<'de> serde::de::Visitor<'de> for LevelFilterExprVisitor { + type Value = LevelFilter; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a spdlog-rs level filter expression") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + LevelFilter::from_config_str(v) + .ok_or_else(|| E::custom(format!("unknown level filter expression '{}'", v))) + } + } + + de.deserialize_str(LevelFilterExprVisitor) + } +} + cfg_if! { if #[cfg(test)] { use std::mem::{align_of, size_of}; @@ -274,6 +302,35 @@ impl LevelFilter { None } } + + // TODO: cfg + fn from_config_str(v: &str) -> Option { + let value = + if v.ends_with(')') && v.matches('(').count() == 1 && v.matches(')').count() == 1 { + let (lpa, rpa) = (v.find('(').unwrap(), v.find(')').unwrap()); + assert!(lpa < rpa); + + let condition = &v[..lpa]; + let level = v[lpa + 1..rpa].parse::().ok()?; + + match condition { + "Equal" => Self::Equal(level), + "NotEqual" => Self::NotEqual(level), + "MoreSevere" => Self::MoreSevere(level), + "MoreSevereEqual" => Self::MoreSevereEqual(level), + "MoreVerbose" => Self::MoreVerbose(level), + "MoreVerboseEqual" => Self::MoreVerboseEqual(level), + _ => return None, + } + } else { + match v { + "Off" => Self::Off, + "All" => Self::All, + _ => return None, + } + }; + Some(value) + } } #[cfg(feature = "log")] @@ -371,6 +428,53 @@ mod tests { ); } + #[test] + fn level_filter_from_str_for_config() { + assert_eq!( + LevelFilter::Off, + LevelFilter::from_config_str("Off").unwrap() + ); + assert_eq!( + LevelFilter::Equal(Level::Trace), + LevelFilter::from_config_str("Equal(trace)").unwrap() + ); + assert_eq!( + LevelFilter::NotEqual(Level::Debug), + LevelFilter::from_config_str("NotEqual(debug)").unwrap() + ); + assert_eq!( + LevelFilter::MoreSevere(Level::Info), + LevelFilter::from_config_str("MoreSevere(info)").unwrap() + ); + assert_eq!( + LevelFilter::MoreSevereEqual(Level::Warn), + LevelFilter::from_config_str("MoreSevereEqual(warn)").unwrap() + ); + assert_eq!( + LevelFilter::MoreVerbose(Level::Error), + LevelFilter::from_config_str("MoreVerbose(Error)").unwrap() + ); + assert_eq!( + LevelFilter::MoreVerboseEqual(Level::Critical), + LevelFilter::from_config_str("MoreVerboseEqual(Critical)").unwrap() + ); + assert_eq!( + LevelFilter::All, + LevelFilter::from_config_str("All").unwrap() + ); + + assert!(LevelFilter::from_config_str("Unknown").is_none()); + assert!(LevelFilter::from_config_str("Equal(info").is_none()); + assert!(LevelFilter::from_config_str("Equal)info(").is_none()); + assert!(LevelFilter::from_config_str("Equal)info").is_none()); + assert!(LevelFilter::from_config_str("(Equal)info").is_none()); + assert!(LevelFilter::from_config_str("Equal(info) ").is_none()); + assert!(LevelFilter::from_config_str(" Equal(info)").is_none()); + assert!(LevelFilter::from_config_str("Equal (info)").is_none()); + assert!(LevelFilter::from_config_str("Equal( info)").is_none()); + assert!(LevelFilter::from_config_str("Equal(info )").is_none()); + } + #[test] fn iter() { let mut iter = Level::iter(); diff --git a/spdlog/src/lib.rs b/spdlog/src/lib.rs index b290ebb4..506874f9 100644 --- a/spdlog/src/lib.rs +++ b/spdlog/src/lib.rs @@ -254,6 +254,7 @@ #![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] #![warn(missing_docs)] +pub mod config; mod env_level; pub mod error; pub mod formatter; diff --git a/spdlog/src/logger.rs b/spdlog/src/logger.rs index 8f61f512..312b989b 100644 --- a/spdlog/src/logger.rs +++ b/spdlog/src/logger.rs @@ -2,6 +2,8 @@ use std::{result::Result as StdResult, time::Duration}; +use serde::Deserialize; + use crate::{ env_level, error::{Error, ErrorHandler, InvalidArgumentError, SetLoggerNameError}, @@ -14,18 +16,20 @@ use crate::{ fn check_logger_name(name: impl AsRef) -> StdResult<(), SetLoggerNameError> { let name = name.as_ref(); - if name.chars().any(|ch| { - ch == ',' - || ch == '=' - || ch == '*' - || ch == '?' - || ch == '$' - || ch == '{' - || ch == '}' - || ch == '"' - || ch == '\'' - || ch == ';' - }) || name.starts_with(' ') + if name.to_ascii_lowercase() == "default" + || name.chars().any(|ch| { + ch == ',' + || ch == '=' + || ch == '*' + || ch == '?' + || ch == '$' + || ch == '{' + || ch == '}' + || ch == '"' + || ch == '\'' + || ch == ';' + }) + || name.starts_with(' ') || name.ends_with(' ') { Err(SetLoggerNameError::new(name)) @@ -75,13 +79,7 @@ impl Logger { /// Constructs a [`LoggerBuilder`]. #[must_use] pub fn builder() -> LoggerBuilder { - LoggerBuilder { - name: None, - level_filter: LevelFilter::MoreSevereEqual(Level::Info), - sinks: vec![], - flush_level_filter: LevelFilter::Off, - error_handler: None, - } + LoggerBuilder(LoggerParams::default()) } /// Gets the logger name. @@ -445,15 +443,50 @@ impl Clone for Logger { } } -/// The builder of [`Logger`]. -#[derive(Clone)] -pub struct LoggerBuilder { - name: Option, +#[derive(Clone, Deserialize)] +pub(crate) struct ArcSinkWrapper( + // `Option` is used to support `Default` trait required in config deserialization, it should + // never be `None`, it's fine to `unwrap` it anywhere. + #[serde(default, deserialize_with = "crate::config::deser::sink")] Option>, +); + +pub(crate) const fn logger_default_level_filter() -> LevelFilter { + LevelFilter::MoreSevereEqual(Level::Info) +} + +pub(crate) const fn logger_default_flush_level_filter() -> LevelFilter { + LevelFilter::Off +} + +#[derive(Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LoggerParams { + name: Option, // TODO: maybe conflict with `match` key + #[serde(default = "logger_default_level_filter")] level_filter: LevelFilter, - sinks: Sinks, + sinks: Vec, + #[serde(default = "logger_default_flush_level_filter")] flush_level_filter: LevelFilter, + #[serde(skip)] // Set `error_handler` from config is not supported error_handler: Option, } +// TODO: Is it possible and necessary to support `periodic_flusher` for config? + +impl Default for LoggerParams { + fn default() -> Self { + Self { + name: None, + level_filter: logger_default_level_filter(), + sinks: vec![], + flush_level_filter: logger_default_flush_level_filter(), + error_handler: None, + } + } +} + +/// The builder of [`Logger`]. +#[derive(Clone)] +pub struct LoggerBuilder(pub(crate) LoggerParams); impl LoggerBuilder { /// Constructs a `LoggerBuilder`. @@ -482,7 +515,7 @@ impl LoggerBuilder { where S: Into, { - self.name = Some(name.into()); + self.0.name = Some(name.into()); self } @@ -491,13 +524,13 @@ impl LoggerBuilder { /// This parameter is **optional**, and defaults to /// `LevelFilter::MoreSevereEqual(Level::Info)`. pub fn level_filter(&mut self, level_filter: LevelFilter) -> &mut Self { - self.level_filter = level_filter; + self.0.level_filter = level_filter; self } /// Add a [`Sink`]. pub fn sink(&mut self, sink: Arc) -> &mut Self { - self.sinks.push(sink); + self.0.sinks.push(ArcSinkWrapper(Some(sink))); self } @@ -506,7 +539,12 @@ impl LoggerBuilder { where I: IntoIterator>, { - self.sinks.append(&mut sinks.into_iter().collect()); + self.0.sinks.append( + &mut sinks + .into_iter() + .map(|sink| ArcSinkWrapper(Some(sink))) + .collect(), + ); self } @@ -517,7 +555,7 @@ impl LoggerBuilder { /// See the documentation of [`Logger::set_flush_level_filter`] for the /// description of this parameter. pub fn flush_level_filter(&mut self, level_filter: LevelFilter) -> &mut Self { - self.flush_level_filter = level_filter; + self.0.flush_level_filter = level_filter; self } @@ -528,15 +566,22 @@ impl LoggerBuilder { /// See the documentation of [`Logger::set_error_handler`] for the /// description of this parameter. pub fn error_handler(&mut self, handler: ErrorHandler) -> &mut Self { - self.error_handler = Some(handler); + self.0.error_handler = Some(handler); self } + // Always do checks in the `build` function! + // Since the field setters will not be called for config + /// Builds a [`Logger`]. pub fn build(&mut self) -> Result { self.build_inner(self.preset_level(false)) } + pub(crate) fn build_config(params: LoggerParams) -> Result { + Self(params).build_inner(None) + } + pub(crate) fn build_default(&mut self) -> Result { self.build_inner(self.preset_level(true)) } @@ -546,21 +591,27 @@ impl LoggerBuilder { if is_default { env_level::logger_level(env_level::LoggerKind::Default) } else { - env_level::logger_level(env_level::LoggerKind::Other(self.name.as_deref())) + env_level::logger_level(env_level::LoggerKind::Other(self.0.name.as_deref())) } } fn build_inner(&mut self, preset_level: Option) -> Result { - if let Some(name) = &self.name { + if let Some(name) = &self.0.name { check_logger_name(name).map_err(InvalidArgumentError::from)?; } let logger = Logger { - name: self.name.clone(), - level_filter: Atomic::new(self.level_filter), - sinks: self.sinks.clone(), - flush_level_filter: Atomic::new(self.flush_level_filter), - error_handler: SpinRwLock::new(self.error_handler), + name: self.0.name.clone(), + level_filter: Atomic::new(self.0.level_filter), + sinks: self + .0 + .sinks + .clone() + .into_iter() + .map(|sink| sink.0.unwrap()) + .collect(), + flush_level_filter: Atomic::new(self.0.flush_level_filter), + error_handler: SpinRwLock::new(self.0.error_handler), periodic_flusher: Mutex::new(None), }; @@ -582,7 +633,7 @@ impl LoggerBuilder { } else { env_level::logger_level_inner( &env_level::from_str_inner(env_level).unwrap(), - env_level::LoggerKind::Other(self.name.as_deref()), + env_level::LoggerKind::Other(self.0.name.as_deref()), ) }; diff --git a/spdlog/src/sink/async_sink/async_pool_sink.rs b/spdlog/src/sink/async_sink/async_pool_sink.rs index 4e214439..17befea8 100644 --- a/spdlog/src/sink/async_sink/async_pool_sink.rs +++ b/spdlog/src/sink/async_sink/async_pool_sink.rs @@ -37,7 +37,7 @@ impl AsyncPoolSink { #[must_use] pub fn builder() -> AsyncPoolSinkBuilder { AsyncPoolSinkBuilder { - level_filter: helper::SINK_DEFAULT_LEVEL_FILTER, + level_filter: helper::sink_default_level_filter(), overflow_policy: OverflowPolicy::Block, sinks: Sinks::new(), thread_pool: None, diff --git a/spdlog/src/sink/file_sink.rs b/spdlog/src/sink/file_sink.rs index d7dc1e64..2462b924 100644 --- a/spdlog/src/sink/file_sink.rs +++ b/spdlog/src/sink/file_sink.rs @@ -7,7 +7,10 @@ use std::{ path::{Path, PathBuf}, }; +use serde::Deserialize; + use crate::{ + config::{ComponentMetadata, Configurable}, sink::{helper, Sink}, sync::*, utils, Error, Record, Result, StringBuf, @@ -30,9 +33,11 @@ impl FileSink { #[must_use] pub fn builder() -> FileSinkBuilder<()> { FileSinkBuilder { - path: (), - truncate: false, - common_builder_impl: helper::CommonBuilderImpl::new(), + inner: FileSinkParamsInner { + path: (), + truncate: false, + common_builder_impl: helper::CommonBuilderImpl::new(), + }, } } @@ -97,6 +102,41 @@ impl Drop for FileSink { } } +#[derive(Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +struct FileSinkParamsInner { + #[serde(flatten)] + common_builder_impl: helper::CommonBuilderImpl, + path: ArgPath, + #[serde(default)] + truncate: bool, +} + +#[derive(Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +#[doc(hidden)] // TODO: Any other way to hide it? +pub struct FileSinkParams(FileSinkParamsInner); + +impl Configurable for FileSink { + type Params = FileSinkParams; + + fn metadata() -> ComponentMetadata { + ComponentMetadata::builder().name("FileSink").build() + } + + fn build(params: Self::Params) -> Result { + let mut builder = FileSink::builder() + .level_filter(params.0.common_builder_impl.level_filter) + // .error_handler(params.0.common_builder_impl.error_handler) + .path(params.0.path) + .truncate(params.0.truncate); + if let Some(formatter) = params.0.common_builder_impl.formatter { + builder = builder.formatter(formatter); + } + builder.build() + } +} + // -------------------------------------------------- /// The builder of [`FileSink`]. @@ -130,9 +170,7 @@ impl Drop for FileSink { /// # Ok(()) } /// ``` pub struct FileSinkBuilder { - common_builder_impl: helper::CommonBuilderImpl, - path: ArgPath, - truncate: bool, + inner: FileSinkParamsInner, } impl FileSinkBuilder { @@ -145,9 +183,11 @@ impl FileSinkBuilder { P: Into, { FileSinkBuilder { - common_builder_impl: self.common_builder_impl, - path: path.into(), - truncate: self.truncate, + inner: FileSinkParamsInner { + common_builder_impl: self.inner.common_builder_impl, + path: path.into(), + truncate: self.inner.truncate, + }, } } @@ -156,11 +196,11 @@ impl FileSinkBuilder { /// This parameter is **optional**, and defaults to `false`. #[must_use] pub fn truncate(mut self, truncate: bool) -> Self { - self.truncate = truncate; + self.inner.truncate = truncate; self } - helper::common_impl!(@SinkBuilder: common_builder_impl); + helper::common_impl!(@SinkBuilder: inner.common_builder_impl); } impl FileSinkBuilder<()> { @@ -180,13 +220,80 @@ impl FileSinkBuilder { /// If an error occurs opening the file, [`Error::CreateDirectory`] or /// [`Error::OpenFile`] will be returned. pub fn build(self) -> Result { - let file = utils::open_file(self.path, self.truncate)?; + let file = utils::open_file(self.inner.path, self.inner.truncate)?; let sink = FileSink { - common_impl: helper::CommonImpl::from_builder(self.common_builder_impl), + common_impl: helper::CommonImpl::from_builder(self.inner.common_builder_impl), file: SpinMutex::new(BufWriter::new(file)), }; Ok(sink) } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use crate::{ + formatter::{FullFormatter, PatternFormatter, RuntimePattern}, + LevelFilter, + }; + + #[test] + fn deser_params() { + assert!( + toml::from_str::(r#"path = "/path/to/log_file""#,).unwrap() + == FileSinkParams(FileSinkParamsInner { + common_builder_impl: helper::CommonBuilderImpl { + level_filter: LevelFilter::All, + formatter: None, + error_handler: None + }, + path: PathBuf::from_str("/path/to/log_file").unwrap(), + truncate: false + }) + ); + assert!( + toml::from_str::( + r#" + path = "/path/to/app.log" + truncate = true + formatter = { name = "FullFormatter" } + "#, + ) + .unwrap() + == FileSinkParams(FileSinkParamsInner { + common_builder_impl: helper::CommonBuilderImpl { + level_filter: LevelFilter::All, + formatter: Some(Box::new(FullFormatter::new())), + error_handler: None + }, + path: PathBuf::from_str("/path/to/app.log").unwrap(), + truncate: true + }) + ); + assert!( + toml::from_str::( + r#" + path = "/path/to/app.log" + truncate = true + formatter = { name = "PatternFormatter", template = "[{level}] >w< {payload}{eol}" } + "#, + ) + .unwrap() + == FileSinkParams(FileSinkParamsInner { + common_builder_impl: helper::CommonBuilderImpl { + level_filter: LevelFilter::All, + formatter: Some(Box::new(PatternFormatter::new( + RuntimePattern::new("[{level}] >w< {payload}{eol}").unwrap() + ))), + error_handler: None + }, + path: PathBuf::from_str("/path/to/app.log").unwrap(), + truncate: true + }) + ); + } +} diff --git a/spdlog/src/sink/helper.rs b/spdlog/src/sink/helper.rs index 71bd374d..7fa13083 100644 --- a/spdlog/src/sink/helper.rs +++ b/spdlog/src/sink/helper.rs @@ -1,6 +1,10 @@ +use std::result::Result as StdResult; + use cfg_if::cfg_if; +use serde::Deserialize; use crate::{ + config::Configurable, formatter::{Formatter, FullFormatter}, prelude::*, sync::*, @@ -15,7 +19,9 @@ cfg_if! { } } -pub(crate) const SINK_DEFAULT_LEVEL_FILTER: LevelFilter = LevelFilter::All; +pub(crate) const fn sink_default_level_filter() -> LevelFilter { + LevelFilter::All +} pub(crate) struct CommonImpl { pub(crate) level_filter: Atomic, @@ -59,17 +65,28 @@ impl CommonImpl { } } +#[derive(Deserialize)] +#[cfg_attr(test, derive(PartialEq))] pub(crate) struct CommonBuilderImpl { + #[serde(default = "sink_default_level_filter")] pub(crate) level_filter: LevelFilter, + #[serde(default, deserialize_with = "crate::config::deser::formatter")] pub(crate) formatter: Option>, + #[serde(skip)] // Set `error_handler` from config is not supported pub(crate) error_handler: Option, } +impl Default for CommonBuilderImpl { + fn default() -> Self { + Self::new() + } +} + impl CommonBuilderImpl { #[must_use] pub(crate) fn new() -> Self { Self { - level_filter: SINK_DEFAULT_LEVEL_FILTER, + level_filter: sink_default_level_filter(), formatter: None, error_handler: None, } diff --git a/spdlog/src/sink/mod.rs b/spdlog/src/sink/mod.rs index d5049e21..dbf6f318 100644 --- a/spdlog/src/sink/mod.rs +++ b/spdlog/src/sink/mod.rs @@ -120,3 +120,12 @@ pub trait Sink: Sync + Send { /// A container for [`Sink`]s. pub type Sinks = Vec>; + +/// There is no easy way to implement `PartialEq` for `dyn T`. we just do it for +/// testing, so we implement it this way +#[cfg(test)] +impl PartialEq for dyn Sink { + fn eq(&self, other: &Self) -> bool { + self.level_filter() == other.level_filter() + } +} diff --git a/spdlog/src/test_utils/unit_test.rs b/spdlog/src/test_utils/unit_test.rs index 3f0d54b8..6fb2f81d 100644 --- a/spdlog/src/test_utils/unit_test.rs +++ b/spdlog/src/test_utils/unit_test.rs @@ -3,9 +3,16 @@ // In this file, you can use public or private items from spdlog-rs as you wish, // as they will be used from unit tests only. -use std::{env, fs, path::PathBuf}; +use std::{env, fmt::Write, fs, path::PathBuf}; -use crate::sync::*; +use crate::{ + config::{ComponentMetadata, Configurable, Registry}, + error::{ConfigError, Error}, + formatter::{FmtExtraInfo, Formatter}, + prelude::*, + sync::*, + ErrorHandler, Record, Result, Sink, StringBuf, +}; pub static TEST_LOGS_PATH: Lazy = Lazy::new(|| { let path = env::current_exe() @@ -16,3 +23,116 @@ pub static TEST_LOGS_PATH: Lazy = Lazy::new(|| { fs::create_dir_all(&path).unwrap(); path }); + +pub mod config { + use std::sync::Once; + + use super::*; + + pub struct ConfigMockSink(i32); + + impl Sink for ConfigMockSink { + fn log(&self, _record: &Record) -> Result<()> { + unimplemented!() + } + + fn flush(&self) -> Result<()> { + Err(Error::__ForInternalTestsUseOnly(ID, self.0)) + } + + fn level_filter(&self) -> LevelFilter { + unimplemented!() + } + + fn set_level_filter(&self, _level_filter: LevelFilter) { + unimplemented!() + } + + fn set_formatter(&self, _formatter: Box) { + unimplemented!() + } + + fn set_error_handler(&self, _handler: Option) {} + } + + #[derive(Default, serde::Deserialize)] + #[serde(deny_unknown_fields)] + pub struct MockParams { + arg: i32, + } + + macro_rules! impl_multiple_mock_sinks { + ( $(($id:expr, $name:literal)),+ $(,)? ) => { + $(impl Configurable for ConfigMockSink<$id> { + type Params = MockParams; + + fn metadata() -> ComponentMetadata { + ComponentMetadata::builder().name($name).build() + } + + fn build(params: Self::Params) -> Result { + Ok(Self(params.arg)) + } + })+ + }; + } + + impl_multiple_mock_sinks![ + (1, "ConfigMockSink1"), + (2, "ConfigMockSink2"), + (3, "ConfigMockSink3") + ]; + + #[derive(Clone)] + pub struct ConfigMockFormatter(i32); + + impl Formatter for ConfigMockFormatter { + fn format(&self, _record: &Record, dest: &mut StringBuf) -> Result { + write!(dest, "{}", self.0).unwrap(); + Ok(FmtExtraInfo::new()) + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + } + + impl Configurable for ConfigMockFormatter { + type Params = MockParams; + + fn metadata() -> ComponentMetadata { + ComponentMetadata::builder() + .name("ConfigMockFormatter") + .build() + } + + fn build(params: Self::Params) -> Result { + Ok(Self(params.arg)) + } + } + + fn register_mock_components(registry: &Registry) { + registry.register_sink::>().unwrap(); + registry.register_sink::>().unwrap(); + assert!(matches!( + registry.register_sink::>(), + Err(Error::Config(ConfigError::MultipleRegistration)) + )); + registry.register_sink::>().unwrap(); + registry + .register_formatter::() + .unwrap(); + } + + pub fn registry_for_test() -> Registry { + let registry = Registry::with_builtin(); + register_mock_components(®istry); + registry + } + + pub fn register_global() { + static INIT: Once = Once::new(); + + INIT.call_once(|| register_mock_components(crate::config::registry())); + } +} diff --git a/spdlog/tests/config.rs b/spdlog/tests/config.rs new file mode 100644 index 00000000..ef96fbe7 --- /dev/null +++ b/spdlog/tests/config.rs @@ -0,0 +1,48 @@ +use std::{ + fs, + path::{Path, PathBuf}, + sync::Arc, +}; + +use once_cell::sync::Lazy; +use spdlog::{ + config::{self, Config}, + formatter::{pattern, PatternFormatter}, +}; + +include!(concat!( + env!("OUT_DIR"), + "/test_utils/common_for_integration_test.rs" +)); +use test_utils::*; + +static TEMP_DIR: Lazy = Lazy::new(|| { + let temp_dir = PathBuf::from(env!("OUT_DIR")) + .join("dev") + .join("integration-test-config"); + fs::create_dir_all(&temp_dir).unwrap(); + temp_dir +}); + +#[test] +fn test_config_full() { + let path = TEMP_DIR.join("file-sink.log"); + let inputs = format!( + r#" +[loggers.default] +sinks = [ + {{ name = "$ConfigMockSink1", arg = 114 }}, + {{ name = "$ConfigMockSink2", arg = 514 }}, + {{ name = "$ConfigMockSink3", arg = 1919 }}, + {{ name = "FileSink", path = "{}", formatter = {{ name = "PatternFormatter", template = "Meow! {{payload}}{{eol}}" }} }} +] +flush_level_filter = "Equal(Info)" # TODO: reconsider the syntax + +[loggers.network] +sinks = [ {{ name = "$ConfigMockSink2", arg = 810 }} ] +# TODO: flush_period = "10s" + "#, + path.display() + ); + let config = Config::new_for_test(&inputs).unwrap(); +}