From 6bd739853706bd26a8f4071bb03c3b71b77fc7f9 Mon Sep 17 00:00:00 2001 From: Sprite Date: Wed, 15 Nov 2023 01:54:37 +0800 Subject: [PATCH 1/4] WIP: Implement `config` feature --- spdlog/Cargo.toml | 4 + spdlog/src/config/mod.rs | 24 ++ spdlog/src/config/parse.rs | 53 ++++ spdlog/src/config/registry.rs | 276 ++++++++++++++++++ spdlog/src/config/source.rs | 18 ++ spdlog/src/error.rs | 11 + spdlog/src/formatter/full_formatter.rs | 24 +- spdlog/src/formatter/mod.rs | 17 ++ .../formatter/pattern_formatter/runtime.rs | 222 +++++++++++++- spdlog/src/level.rs | 2 + spdlog/src/lib.rs | 1 + spdlog/src/sink/async_sink/async_pool_sink.rs | 2 +- spdlog/src/sink/file_sink.rs | 133 ++++++++- spdlog/src/sink/helper.rs | 21 +- spdlog/src/sink/mod.rs | 9 + 15 files changed, 796 insertions(+), 21 deletions(-) create mode 100644 spdlog/src/config/mod.rs create mode 100644 spdlog/src/config/parse.rs create mode 100644 spdlog/src/config/registry.rs create mode 100644 spdlog/src/config/source.rs diff --git a/spdlog/Cargo.toml b/spdlog/Cargo.toml index cbce1f8c..b27aacea 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"] source-location = [] native = [] libsystemd = ["libsystemd-sys"] @@ -45,11 +46,13 @@ 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" @@ -81,6 +84,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/mod.rs b/spdlog/src/config/mod.rs new file mode 100644 index 00000000..5443666a --- /dev/null +++ b/spdlog/src/config/mod.rs @@ -0,0 +1,24 @@ +mod registry; +mod source; + +pub(crate) mod parse; + +pub use registry::*; +use serde::{de::DeserializeOwned, Deserialize}; +pub use source::*; + +use crate::{sync::*, Result}; + +// TODO: Force `'static` on name? +// Builder? +#[derive(PartialEq, Eq, Hash)] +pub struct ComponentMetadata<'a> { + pub(crate) name: &'a str, +} + +pub trait Configurable: Sized { + type Params: DeserializeOwned + Default + Send; + + fn metadata() -> ComponentMetadata<'static>; + fn build(params: Self::Params) -> Result; +} diff --git a/spdlog/src/config/parse.rs b/spdlog/src/config/parse.rs new file mode 100644 index 00000000..da02f223 --- /dev/null +++ b/spdlog/src/config/parse.rs @@ -0,0 +1,53 @@ +use erased_serde::Deserializer as ErasedDeserializer; +use serde::{ + de::{ + value::{MapAccessDeserializer, UnitDeserializer}, + Error as SerdeDeError, MapAccess, Visitor, + }, + Deserializer, +}; + +use crate::{config, formatter::*}; + +pub fn formatter_deser<'de, D>(de: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + struct ParseVisitor; + + impl<'de> Visitor<'de> for ParseVisitor { + type Value = Box; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a spdlog-rs component") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let name = map + .next_entry::()? + .filter(|(key, _)| key == "name") + .map(|(_, value)| value) + .ok_or_else(|| SerdeDeError::missing_field("name"))?; + + let remaining_args = map.size_hint().unwrap(); // I don't know what situation it will be `None`` + + let formatter = if remaining_args == 0 { + let mut erased_de = + ::erase(UnitDeserializer::::new()); + config::registry().build_formatter(&name, &mut erased_de) + } else { + let mut erased_de = + ::erase(MapAccessDeserializer::new(map)); + config::registry().build_formatter(&name, &mut erased_de) + } + .map_err(|err| SerdeDeError::custom(err))?; + + Ok(formatter) + } + } + + Ok(Some(de.deserialize_map(ParseVisitor)?)) +} diff --git a/spdlog/src/config/registry.rs b/spdlog/src/config/registry.rs new file mode 100644 index 00000000..69f5adad --- /dev/null +++ b/spdlog/src/config/registry.rs @@ -0,0 +1,276 @@ +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, 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 { + sink: Mutex>>, + formatter: Mutex>>, + + // TODO: Consider make them compile-time + builtin_sink: Mutex>>, + builtin_formatter: Mutex>>, +} + +impl Registry { + pub fn register_sink(&mut self) -> Result<()> + where + S: Sink + Configurable + 'static, + { + self.register_sink_inner::() + } + + pub fn register_formatter(&mut self) -> Result<()> + where + F: Formatter + Configurable + 'static, + { + self.register_formatter_inner::() + } +} + +impl Registry { + pub(crate) fn with_builtin() -> Self { + let mut registry = Self { + sink: Mutex::new(HashMap::new()), + formatter: Mutex::new(HashMap::new()), + builtin_sink: Mutex::new(HashMap::new()), + builtin_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> { + self.builtin_sink + .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> { + self.builtin_formatter + .lock_expect() + .get(name) + .ok_or_else(|| Error::Config(ConfigError::UnknownComponent(name.to_string()))) + .and_then(|f| f(de)) + } + + impl_registers! { + fn register_sink_inner => sink, Sink, + fn register_formatter_inner => formatter, Formatter, + pub(crate) fn register_builtin_sink => builtin_sink, Sink, + pub(crate) fn register_builtin_formatter => builtin_formatter, Formatter, + } +} + +// TODO: Append prefix '$' for custom components +macro_rules! impl_registers { + ( $($vis:vis fn $fn_name:ident => $var:ident, $trait:ident),+ $(,)? ) => { + $($vis fn $fn_name(&mut self) -> Result<()> + where + C: $trait + Configurable + 'static, + { + let f = |de: &mut dyn ErasedDeserializer| -> Result> { + let mut params = C::Params::default(); + params.erased_deserialize_in_place(de).unwrap(); // TODO: Wrong input will trigger a Err, handle it! + Ok(Box::new(C::build(params)?)) + }; + + self.$var + .lock_expect() + .insert(C::metadata().name, f) + .map_or(Ok(()), |_| Err(Error::Config(ConfigError::MultipleRegistration))) + })+ + }; +} +use impl_registers; + +// 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 std::fmt::Write; + + use serde::Deserializer; + + use super::*; + use crate::{formatter::FmtExtraInfo, prelude::*, ErrorHandler, Record, StringBuf}; + + pub struct MockSink(i32); + + impl Sink for MockSink { + fn log(&self, _record: &Record) -> Result<()> { + unimplemented!() + } + + fn flush(&self) -> Result<()> { + unimplemented!() + } + + 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) { + handler.unwrap()(Error::__ForInternalTestsUseOnly(self.0)) + } + } + + #[derive(Default, serde::Deserialize)] + pub struct MockParams { + arg: i32, + } + + impl Configurable for MockSink { + type Params = MockParams; + + fn metadata() -> ComponentMetadata<'static> { + ComponentMetadata { name: "MockSink" } + } + + fn build(params: Self::Params) -> Result { + Ok(Self(params.arg)) + } + } + + #[derive(Clone)] + pub struct MockFormatter(i32); + + impl Formatter for MockFormatter { + 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 MockFormatter { + type Params = MockParams; + + fn metadata() -> ComponentMetadata<'static> { + ComponentMetadata { + name: "MockFormatter", + } + } + + fn build(params: Self::Params) -> Result { + Ok(Self(params.arg)) + } + } + + fn registry_for_test() -> Registry { + let mut registry = Registry::with_builtin(); + registry.register_sink::().unwrap(); + registry.register_formatter::().unwrap(); + registry + } + + #[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("MockSink", &mut erased_de).unwrap(); + sink.set_error_handler(Some(|err| { + assert!(matches!(err, Error::__ForInternalTestsUseOnly(114514))) + })); + + // TODO: test wrong kind + } + + #[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("MockFormatter", &mut erased_de) + .unwrap(); + let mut dest = StringBuf::new(); + formatter + .format(&Record::new(Level::Info, ""), &mut dest) + .unwrap(); + assert_eq!(dest, "1919810") + + // TODO: test wrong kind + } + + // 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..0c2a331f 100644 --- a/spdlog/src/error.rs +++ b/spdlog/src/error.rs @@ -104,6 +104,9 @@ pub enum Error { #[error("{0:?}")] Multiple(Vec), + #[error("TODO: {0}")] + Config(ConfigError), // TODO: Better name? + #[cfg(test)] #[error("{0}")] __ForInternalTestsUseOnly(i32), @@ -257,6 +260,14 @@ impl SendToChannelErrorDropped { #[error("{0}")] pub struct BuildPatternError(pub(crate) spdlog_internal::pattern_parser::Error); +#[derive(Error, Debug)] +pub enum ConfigError { + #[error("TODO: MultipleRegistration")] + MultipleRegistration, + #[error("TODO: UnknownComponent ({0})")] + UnknownComponent(String), // TODO: Better name? +} + /// 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..b9a5ab0c 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,20 @@ impl Default for FullFormatter { } } +impl Configurable for FullFormatter { + type Params = (); + + fn metadata() -> ComponentMetadata<'static> { + ComponentMetadata { + name: "FullFormatter", + } + } + + 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..9cce1ab7 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,118 @@ impl Pattern for RuntimePattern { } } +#[derive(Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +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 +374,105 @@ fn build_builtin_pattern(builtin: &BuiltInFormatter) -> Box { Eol ) } + +impl Configurable for PatternFormatter { + type Params = PatternFormatterRuntimePatternParams; + + fn metadata() -> ComponentMetadata<'static> { + ComponentMetadata { + name: "PatternFormatter", + } + } + + 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..15d17004 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::Serialize, serde::Deserialize))] pub enum Level { /// Designates critical errors. Critical = 0, @@ -201,6 +202,7 @@ impl FromStr for Level { /// ``` #[repr(align(4))] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum LevelFilter { /// Disables all levels. Off, 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/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..df227660 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)] +pub struct FileSinkParams(FileSinkParamsInner); + +impl Configurable for FileSink { + type Params = FileSinkParams; + + fn metadata() -> ComponentMetadata<'static> { + ComponentMetadata { name: "FileSink" } + } + + 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..e350bd04 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::parse::formatter_deser")] pub(crate) formatter: Option>, + #[serde(skip)] 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() + } +} From 514408d116ba55db93fc8ae47e598687d5b7afb3 Mon Sep 17 00:00:00 2001 From: Asuna Date: Mon, 4 Dec 2023 02:00:42 +0800 Subject: [PATCH 2/4] WIP: Implement custom components and logger --- spdlog/Cargo.toml | 1 + spdlog/src/config/deser.rs | 145 +++++++++++ spdlog/src/config/mod.rs | 102 +++++++- spdlog/src/config/parse.rs | 53 ---- spdlog/src/config/registry.rs | 239 ++++++++---------- spdlog/src/error.rs | 8 +- spdlog/src/formatter/full_formatter.rs | 8 +- .../formatter/pattern_formatter/runtime.rs | 10 +- spdlog/src/logger.rs | 101 ++++++-- spdlog/src/sink/file_sink.rs | 6 +- spdlog/src/sink/helper.rs | 4 +- spdlog/src/test_utils/unit_test.rs | 124 ++++++++- 12 files changed, 564 insertions(+), 237 deletions(-) create mode 100644 spdlog/src/config/deser.rs delete mode 100644 spdlog/src/config/parse.rs diff --git a/spdlog/Cargo.toml b/spdlog/Cargo.toml index b27aacea..20563b8b 100644 --- a/spdlog/Cargo.toml +++ b/spdlog/Cargo.toml @@ -57,6 +57,7 @@ spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal", optional = spdlog-macros = { version = "0.1.0", path = "../spdlog-macros" } spin = "0.9.8" thiserror = "1.0.37" +toml = "0.8.8" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["consoleapi", "debugapi", "handleapi", "processenv", "processthreadsapi", "winbase", "wincon"] } 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 index 5443666a..f31d1753 100644 --- a/spdlog/src/config/mod.rs +++ b/spdlog/src/config/mod.rs @@ -1,7 +1,9 @@ mod registry; mod source; -pub(crate) mod parse; +pub(crate) mod deser; + +use std::{collections::HashMap, convert::Infallible}; pub use registry::*; use serde::{de::DeserializeOwned, Deserialize}; @@ -9,16 +11,104 @@ pub use source::*; use crate::{sync::*, Result}; -// TODO: Force `'static` on name? -// Builder? +// TODO: Builder? #[derive(PartialEq, Eq, Hash)] -pub struct ComponentMetadata<'a> { - pub(crate) name: &'a str, +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<'static>; + fn metadata() -> ComponentMetadata; fn build(params: Self::Params) -> Result; } + +mod storage { + use serde::Deserialize; + + use super::*; + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + pub(super) struct Logger( + #[serde(deserialize_with = "crate::config::deser::logger")] crate::Logger, + ); + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + pub(super) struct Config { + loggers: HashMap, + } +} + +pub struct Config(storage::Config); + +impl Config { + // TODO: Remember to remove me + pub fn new_for_test(inputs: &str) -> Result { + let config = toml::from_str(inputs).unwrap(); + Ok(Self(config)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{config::*, TEST_LOGS_PATH}; + + #[test] + fn full() { + 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 = "114514 {{payload}}{{eol}}" }} }} +] +# flush_level_filter = "all" # TODO: design the syntax +# TODO: flush_period = "10s" + "#, + TEST_LOGS_PATH.join("unit_test_config_full.log").display() + ); + + register_global(); + + Config::new_for_test(&inputs).unwrap(); + + // TODO + } +} diff --git a/spdlog/src/config/parse.rs b/spdlog/src/config/parse.rs deleted file mode 100644 index da02f223..00000000 --- a/spdlog/src/config/parse.rs +++ /dev/null @@ -1,53 +0,0 @@ -use erased_serde::Deserializer as ErasedDeserializer; -use serde::{ - de::{ - value::{MapAccessDeserializer, UnitDeserializer}, - Error as SerdeDeError, MapAccess, Visitor, - }, - Deserializer, -}; - -use crate::{config, formatter::*}; - -pub fn formatter_deser<'de, D>(de: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - struct ParseVisitor; - - impl<'de> Visitor<'de> for ParseVisitor { - type Value = Box; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a spdlog-rs component") - } - - fn visit_map(self, mut map: A) -> Result - where - A: MapAccess<'de>, - { - let name = map - .next_entry::()? - .filter(|(key, _)| key == "name") - .map(|(_, value)| value) - .ok_or_else(|| SerdeDeError::missing_field("name"))?; - - let remaining_args = map.size_hint().unwrap(); // I don't know what situation it will be `None`` - - let formatter = if remaining_args == 0 { - let mut erased_de = - ::erase(UnitDeserializer::::new()); - config::registry().build_formatter(&name, &mut erased_de) - } else { - let mut erased_de = - ::erase(MapAccessDeserializer::new(map)); - config::registry().build_formatter(&name, &mut erased_de) - } - .map_err(|err| SerdeDeError::custom(err))?; - - Ok(formatter) - } - } - - Ok(Some(de.deserialize_map(ParseVisitor)?)) -} diff --git a/spdlog/src/config/registry.rs b/spdlog/src/config/registry.rs index 69f5adad..9fc47f42 100644 --- a/spdlog/src/config/registry.rs +++ b/spdlog/src/config/registry.rs @@ -9,7 +9,7 @@ use crate::{ formatter::{Formatter, FullFormatter, PatternFormatter, RuntimePattern}, sink::*, sync::*, - Error, Result, Sink, + Error, Logger, Result, Sink, }; type StdResult = std::result::Result>; @@ -43,28 +43,28 @@ mod erased_serde_ext { } use erased_serde_ext::*; -type ComponentDeser = fn(de: &mut dyn ErasedDeserializer) -> Result; +type ComponentDeser = fn(de: &mut dyn ErasedDeserializer) -> Result; -type RegisteredComponents = HashMap<&'static str, ComponentDeser>; +type RegisteredComponents = HashMap<&'static str, ComponentDeser>; pub struct Registry { - sink: Mutex>>, - formatter: Mutex>>, - // TODO: Consider make them compile-time - builtin_sink: Mutex>>, + builtin_sink: Mutex>>, builtin_formatter: Mutex>>, + + custom_sink: Mutex>>, + custom_formatter: Mutex>>, } impl Registry { - pub fn register_sink(&mut self) -> Result<()> + pub fn register_sink(&self) -> Result<()> where S: Sink + Configurable + 'static, { self.register_sink_inner::() } - pub fn register_formatter(&mut self) -> Result<()> + pub fn register_formatter(&self) -> Result<()> where F: Formatter + Configurable + 'static, { @@ -72,13 +72,28 @@ impl Registry { } } +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 { - sink: Mutex::new(HashMap::new()), - formatter: Mutex::new(HashMap::new()), 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 @@ -95,8 +110,13 @@ impl Registry { &self, name: &str, de: &mut dyn ErasedDeserializer, - ) -> Result> { - self.builtin_sink + ) -> 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()))) @@ -108,42 +128,66 @@ impl Registry { name: &str, de: &mut dyn ErasedDeserializer, ) -> Result> { - self.builtin_formatter + 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)) } - impl_registers! { - fn register_sink_inner => sink, Sink, - fn register_formatter_inner => formatter, Formatter, - pub(crate) fn register_builtin_sink => builtin_sink, Sink, - pub(crate) fn register_builtin_formatter => builtin_formatter, Formatter, + 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)) + }) } -} -// TODO: Append prefix '$' for custom components -macro_rules! impl_registers { - ( $($vis:vis fn $fn_name:ident => $var:ident, $trait:ident),+ $(,)? ) => { - $($vis fn $fn_name(&mut self) -> Result<()> - where - C: $trait + Configurable + 'static, - { - let f = |de: &mut dyn ErasedDeserializer| -> Result> { - let mut params = C::Params::default(); - params.erased_deserialize_in_place(de).unwrap(); // TODO: Wrong input will trigger a Err, handle it! - Ok(Box::new(C::build(params)?)) - }; - - self.$var - .lock_expect() - .insert(C::metadata().name, f) - .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)) + }) + } } -use impl_registers; // TODO: Consider removing the `'static` lifetime. Maybe using `Arc<>`? pub(crate) fn registry() -> &'static Registry { @@ -153,92 +197,8 @@ pub(crate) fn registry() -> &'static Registry { #[cfg(test)] mod tests { - use std::fmt::Write; - - use serde::Deserializer; - use super::*; - use crate::{formatter::FmtExtraInfo, prelude::*, ErrorHandler, Record, StringBuf}; - - pub struct MockSink(i32); - - impl Sink for MockSink { - fn log(&self, _record: &Record) -> Result<()> { - unimplemented!() - } - - fn flush(&self) -> Result<()> { - unimplemented!() - } - - 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) { - handler.unwrap()(Error::__ForInternalTestsUseOnly(self.0)) - } - } - - #[derive(Default, serde::Deserialize)] - pub struct MockParams { - arg: i32, - } - - impl Configurable for MockSink { - type Params = MockParams; - - fn metadata() -> ComponentMetadata<'static> { - ComponentMetadata { name: "MockSink" } - } - - fn build(params: Self::Params) -> Result { - Ok(Self(params.arg)) - } - } - - #[derive(Clone)] - pub struct MockFormatter(i32); - - impl Formatter for MockFormatter { - 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 MockFormatter { - type Params = MockParams; - - fn metadata() -> ComponentMetadata<'static> { - ComponentMetadata { - name: "MockFormatter", - } - } - - fn build(params: Self::Params) -> Result { - Ok(Self(params.arg)) - } - } - - fn registry_for_test() -> Registry { - let mut registry = Registry::with_builtin(); - registry.register_sink::().unwrap(); - registry.register_formatter::().unwrap(); - registry - } + use crate::{prelude::*, test_utils::config::*, Record, StringBuf}; #[test] fn build_sink_from_params() { @@ -246,12 +206,27 @@ mod tests { let mut erased_de = ::erase(toml::Deserializer::new("arg = 114514")); - let sink = registry.build_sink("MockSink", &mut erased_de).unwrap(); - sink.set_error_handler(Some(|err| { - assert!(matches!(err, Error::__ForInternalTestsUseOnly(114514))) - })); + let sink = registry + .build_sink("$ConfigMockSink2", &mut erased_de) + .unwrap(); + assert!(matches!( + sink.flush(), + Err(Error::__ForInternalTestsUseOnly(2, 114514)) + )); - // TODO: test wrong kind + 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] @@ -261,15 +236,13 @@ mod tests { let mut erased_de = ::erase(toml::Deserializer::new("arg = 1919810")); let formatter = registry - .build_formatter("MockFormatter", &mut erased_de) + .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 wrong kind } // TODO: Test custom components diff --git a/spdlog/src/error.rs b/spdlog/src/error.rs index 0c2a331f..b4afdd7d 100644 --- a/spdlog/src/error.rs +++ b/spdlog/src/error.rs @@ -109,7 +109,7 @@ pub enum Error { #[cfg(test)] #[error("{0}")] - __ForInternalTestsUseOnly(i32), + __ForInternalTestsUseOnly(i32, i32), } /// This error type contains a variety of possible invalid arguments. @@ -263,9 +263,11 @@ pub struct BuildPatternError(pub(crate) spdlog_internal::pattern_parser::Error); #[derive(Error, Debug)] pub enum ConfigError { #[error("TODO: MultipleRegistration")] - MultipleRegistration, + MultipleRegistration, // TODO: arg? + #[error("TODO: BuildComponent: {0}")] + BuildComponent(String /* TODO: other type? */), #[error("TODO: UnknownComponent ({0})")] - UnknownComponent(String), // TODO: Better name? + UnknownComponent(String), // TODO: Better name? Distinguish builtin and custom } /// The result type of this crate. diff --git a/spdlog/src/formatter/full_formatter.rs b/spdlog/src/formatter/full_formatter.rs index b9a5ab0c..274d2dcc 100644 --- a/spdlog/src/formatter/full_formatter.rs +++ b/spdlog/src/formatter/full_formatter.rs @@ -127,13 +127,11 @@ impl Default for FullFormatter { impl Configurable for FullFormatter { type Params = (); - fn metadata() -> ComponentMetadata<'static> { - ComponentMetadata { - name: "FullFormatter", - } + fn metadata() -> ComponentMetadata { + ComponentMetadata::builder().name("FullFormatter").build() } - fn build(params: Self::Params) -> Result { + fn build(_params: Self::Params) -> Result { Ok(FullFormatter::new()) } } diff --git a/spdlog/src/formatter/pattern_formatter/runtime.rs b/spdlog/src/formatter/pattern_formatter/runtime.rs index 9cce1ab7..bffafd01 100644 --- a/spdlog/src/formatter/pattern_formatter/runtime.rs +++ b/spdlog/src/formatter/pattern_formatter/runtime.rs @@ -165,6 +165,8 @@ impl Pattern for RuntimePattern { #[derive(Default, Deserialize)] #[cfg_attr(test, derive(PartialEq))] +#[serde(deny_unknown_fields)] +#[doc(hidden)] pub struct PatternFormatterRuntimePatternParams { template: String, } @@ -378,10 +380,10 @@ fn build_builtin_pattern(builtin: &BuiltInFormatter) -> Box { impl Configurable for PatternFormatter { type Params = PatternFormatterRuntimePatternParams; - fn metadata() -> ComponentMetadata<'static> { - ComponentMetadata { - name: "PatternFormatter", - } + fn metadata() -> ComponentMetadata { + ComponentMetadata::builder() + .name("PatternFormatter") + .build() } fn build(params: Self::Params) -> Result { diff --git a/spdlog/src/logger.rs b/spdlog/src/logger.rs index 8f61f512..c94b6dc5 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}, @@ -75,13 +77,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 +441,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 +513,7 @@ impl LoggerBuilder { where S: Into, { - self.name = Some(name.into()); + self.0.name = Some(name.into()); self } @@ -491,13 +522,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 +537,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 +553,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 +564,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 +589,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 +631,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/file_sink.rs b/spdlog/src/sink/file_sink.rs index df227660..2462b924 100644 --- a/spdlog/src/sink/file_sink.rs +++ b/spdlog/src/sink/file_sink.rs @@ -114,14 +114,14 @@ struct FileSinkParamsInner { #[derive(Default, Deserialize)] #[cfg_attr(test, derive(PartialEq))] -#[doc(hidden)] +#[doc(hidden)] // TODO: Any other way to hide it? pub struct FileSinkParams(FileSinkParamsInner); impl Configurable for FileSink { type Params = FileSinkParams; - fn metadata() -> ComponentMetadata<'static> { - ComponentMetadata { name: "FileSink" } + fn metadata() -> ComponentMetadata { + ComponentMetadata::builder().name("FileSink").build() } fn build(params: Self::Params) -> Result { diff --git a/spdlog/src/sink/helper.rs b/spdlog/src/sink/helper.rs index e350bd04..7fa13083 100644 --- a/spdlog/src/sink/helper.rs +++ b/spdlog/src/sink/helper.rs @@ -70,9 +70,9 @@ impl CommonImpl { pub(crate) struct CommonBuilderImpl { #[serde(default = "sink_default_level_filter")] pub(crate) level_filter: LevelFilter, - #[serde(default, deserialize_with = "crate::config::parse::formatter_deser")] + #[serde(default, deserialize_with = "crate::config::deser::formatter")] pub(crate) formatter: Option>, - #[serde(skip)] + #[serde(skip)] // Set `error_handler` from config is not supported pub(crate) error_handler: Option, } 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())); + } +} From 15ba14c69d50b46322a5b1bfea1b30aa184a0a4e Mon Sep 17 00:00:00 2001 From: Asuna Date: Mon, 4 Dec 2023 19:18:03 +0800 Subject: [PATCH 3/4] 1 --- spdlog/Cargo.toml | 4 +- spdlog/src/config/mod.rs | 129 ++++++++++++++++++++++++++++++++------- spdlog/src/level.rs | 106 +++++++++++++++++++++++++++++++- 3 files changed, 212 insertions(+), 27 deletions(-) diff --git a/spdlog/Cargo.toml b/spdlog/Cargo.toml index 20563b8b..c4ca533c 100644 --- a/spdlog/Cargo.toml +++ b/spdlog/Cargo.toml @@ -32,7 +32,7 @@ release-level-info = [] release-level-debug = [] release-level-trace = [] -config = ["serde", "erased-serde"] +config = ["serde", "erased-serde", "toml"] source-location = [] native = [] libsystemd = ["libsystemd-sys"] @@ -57,7 +57,7 @@ spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal", optional = spdlog-macros = { version = "0.1.0", path = "../spdlog-macros" } spin = "0.9.8" thiserror = "1.0.37" -toml = "0.8.8" +toml = { version = "0.8.8", optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["consoleapi", "debugapi", "handleapi", "processenv", "processthreadsapi", "winbase", "wincon"] } diff --git a/spdlog/src/config/mod.rs b/spdlog/src/config/mod.rs index f31d1753..f9516496 100644 --- a/spdlog/src/config/mod.rs +++ b/spdlog/src/config/mod.rs @@ -55,31 +55,27 @@ pub trait Configurable: Sized { fn build(params: Self::Params) -> Result; } -mod storage { - use serde::Deserialize; - - use super::*; - - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - pub(super) struct Logger( - #[serde(deserialize_with = "crate::config::deser::logger")] crate::Logger, - ); - - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - pub(super) struct Config { - loggers: HashMap, - } +// #[derive(Deserialize)] +// #[serde(deny_unknown_fields)] +// pub(super) struct Logger(#[serde(deserialize_with = +// "crate::config::deser::logger")] crate::Logger); + +#[derive(PartialEq, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct ConfigView { + loggers: HashMap, } -pub struct Config(storage::Config); +#[derive(PartialEq, Debug)] +pub struct Config { + view: ConfigView, // Stores the config values only, build lazily +} impl Config { // TODO: Remember to remove me pub fn new_for_test(inputs: &str) -> Result { - let config = toml::from_str(inputs).unwrap(); - Ok(Self(config)) + let view = toml::from_str(inputs).unwrap(); + Ok(Self { view }) } } @@ -90,6 +86,7 @@ mod tests { #[test] fn full() { + let path = TEST_LOGS_PATH.join("unit_test_config_full.log"); let inputs = format!( r#" [loggers.default] @@ -97,17 +94,103 @@ sinks = [ {{ name = "$ConfigMockSink1", arg = 114 }}, {{ name = "$ConfigMockSink2", arg = 514 }}, {{ name = "$ConfigMockSink3", arg = 1919 }}, - {{ name = "FileSink", path = "{}", formatter = {{ name = "PatternFormatter", template = "114514 {{payload}}{{eol}}" }} }} + {{ name = "FileSink", path = "{}", formatter = {{ name = "PatternFormatter", template = "Meow! {{payload}}{{eol}}" }} }} ] -# flush_level_filter = "all" # TODO: design the syntax +flush_level_filter = "Equal(Info)" # TODO: reconsider the syntax + +[loggers.network] +sinks = [ {{ name = "$ConfigMockSink2", arg = 810 }} ] # TODO: flush_period = "10s" "#, - TEST_LOGS_PATH.join("unit_test_config_full.log").display() + path.display() ); register_global(); - Config::new_for_test(&inputs).unwrap(); + 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/level.rs b/spdlog/src/level.rs index 15d17004..ba6f17ed 100644 --- a/spdlog/src/level.rs +++ b/spdlog/src/level.rs @@ -55,7 +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::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] pub enum Level { /// Designates critical errors. Critical = 0, @@ -202,7 +202,6 @@ impl FromStr for Level { /// ``` #[repr(align(4))] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum LevelFilter { /// Disables all levels. Off, @@ -224,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}; @@ -276,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")] @@ -373,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(); From b0452a2100b16795693de275d725fc44a89d6709 Mon Sep 17 00:00:00 2001 From: Asuna Date: Wed, 13 Dec 2023 03:25:55 +0800 Subject: [PATCH 4/4] 2 --- spdlog/src/config/mod.rs | 75 ++++++++++++++++++++++++++++++----- spdlog/src/config/registry.rs | 1 - spdlog/src/logger.rs | 26 ++++++------ spdlog/tests/config.rs | 48 ++++++++++++++++++++++ 4 files changed, 128 insertions(+), 22 deletions(-) create mode 100644 spdlog/tests/config.rs diff --git a/spdlog/src/config/mod.rs b/spdlog/src/config/mod.rs index f9516496..5539b6a8 100644 --- a/spdlog/src/config/mod.rs +++ b/spdlog/src/config/mod.rs @@ -3,13 +3,17 @@ mod source; pub(crate) mod deser; -use std::{collections::HashMap, convert::Infallible}; +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::*, Result}; +use crate::{sync::*, Logger, LoggerBuilder, LoggerParams, Result}; // TODO: Builder? #[derive(PartialEq, Eq, Hash)] @@ -57,25 +61,78 @@ pub trait Configurable: Sized { // #[derive(Deserialize)] // #[serde(deny_unknown_fields)] -// pub(super) struct Logger(#[serde(deserialize_with = +// struct Logger(#[serde(deserialize_with = // "crate::config::deser::logger")] crate::Logger); -#[derive(PartialEq, Debug, Deserialize)] +#[derive(Deserialize)] #[serde(deny_unknown_fields)] -pub(super) struct ConfigView { - loggers: HashMap, +struct ConfigView { + loggers: HashMap, +} + +#[derive(Debug, Hash, Eq, PartialEq)] +enum LoggerKind { + Default, + Named(String), } -#[derive(PartialEq, Debug)] pub struct Config { - view: ConfigView, // Stores the config values only, build lazily + 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 }) + Ok(Self { + view, + built: RefCell::new(HashMap::new()), + }) } } diff --git a/spdlog/src/config/registry.rs b/spdlog/src/config/registry.rs index 9fc47f42..c05ed8f8 100644 --- a/spdlog/src/config/registry.rs +++ b/spdlog/src/config/registry.rs @@ -44,7 +44,6 @@ mod erased_serde_ext { use erased_serde_ext::*; type ComponentDeser = fn(de: &mut dyn ErasedDeserializer) -> Result; - type RegisteredComponents = HashMap<&'static str, ComponentDeser>; pub struct Registry { diff --git a/spdlog/src/logger.rs b/spdlog/src/logger.rs index c94b6dc5..312b989b 100644 --- a/spdlog/src/logger.rs +++ b/spdlog/src/logger.rs @@ -16,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)) 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(); +}