From 0d9407cc922125d2ba46729baf26d45e33cd4ad2 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 15 Oct 2024 09:24:41 +0800 Subject: [PATCH 1/9] refactor: layouts and encoders should be nested to appenders Signed-off-by: tison --- README.md | 3 +- examples/env_filter.rs | 3 +- examples/fn_layout_filter.rs | 7 +- examples/json_stdio.rs | 5 +- examples/rolling_file.rs | 7 +- examples/simple_stdio.rs | 6 +- src/append/mod.rs | 8 -- src/append/opentelemetry.rs | 32 +++---- src/append/rolling_file/append.rs | 12 ++- src/append/stdio.rs | 37 ++++++-- src/{layout/identical.rs => encoder/json.rs} | 31 +++---- src/encoder/layout_wrapping.rs | 47 +++++++++++ src/encoder/mod.rs | 38 +++++++++ src/format/json.rs | 89 ++++++++++++++++++++ src/format/mod.rs | 18 ++++ src/layout/custom.rs | 35 +++----- src/layout/json.rs | 85 +++---------------- src/layout/mod.rs | 25 ++---- src/layout/text.rs | 16 ++-- src/lib.rs | 8 +- src/logger.rs | 45 +++------- tests/recursive_logging.rs | 14 ++- 22 files changed, 337 insertions(+), 234 deletions(-) rename src/{layout/identical.rs => encoder/json.rs} (50%) create mode 100644 src/encoder/layout_wrapping.rs create mode 100644 src/encoder/mod.rs create mode 100644 src/format/json.rs create mode 100644 src/format/mod.rs diff --git a/README.md b/README.md index b8d2a5b..00ebcaf 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,7 @@ fn main() { Logger::new().dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .layout(TextLayout::default()) - .append(append::Stdout), + .append(append::Stdout::new(TextLayout::default())), ) .apply() .unwrap(); diff --git a/examples/env_filter.rs b/examples/env_filter.rs index 7aa8458..a61439d 100644 --- a/examples/env_filter.rs +++ b/examples/env_filter.rs @@ -23,8 +23,7 @@ fn main() { .dispatch( Dispatch::new() .filter(EnvFilter::from_default_env()) - .layout(TextLayout::default()) - .append(append::Stdout), + .append(append::Stdout::new(TextLayout::default())), ) .apply() .unwrap(); diff --git a/examples/fn_layout_filter.rs b/examples/fn_layout_filter.rs index bfd64f7..30de206 100644 --- a/examples/fn_layout_filter.rs +++ b/examples/fn_layout_filter.rs @@ -31,10 +31,9 @@ fn main() { FilterResult::Reject } })) - .layout(CustomLayout::new(|record, f| { - f(format_args!("[system alert] {}", record.args())) - })) - .append(append::Stdout), + .append(append::Stdout::new(CustomLayout::new(|record| { + Ok(format!("[system alert] {}", record.args())) + }))), ) .apply() .unwrap(); diff --git a/examples/json_stdio.rs b/examples/json_stdio.rs index 2420ed5..c92730a 100644 --- a/examples/json_stdio.rs +++ b/examples/json_stdio.rs @@ -14,7 +14,7 @@ use log::LevelFilter; use logforth::append; -use logforth::layout::JsonLayout; +use logforth::encoder::JsonEncoder; use logforth::Dispatch; use logforth::Logger; @@ -23,8 +23,7 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .layout(JsonLayout::default()) - .append(append::Stdout), + .append(append::Stdout::new(JsonEncoder::default())), ) .apply() .unwrap(); diff --git a/examples/rolling_file.rs b/examples/rolling_file.rs index 3571f77..21776c2 100644 --- a/examples/rolling_file.rs +++ b/examples/rolling_file.rs @@ -18,7 +18,7 @@ use logforth::append::rolling_file::RollingFile; use logforth::append::rolling_file::RollingFileWriter; use logforth::append::rolling_file::Rotation; use logforth::append::Stdout; -use logforth::layout::JsonLayout; +use logforth::encoder::JsonEncoder; use logforth::layout::TextLayout; use logforth::Dispatch; use logforth::Logger; @@ -38,10 +38,9 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .layout(JsonLayout::default()) - .append(RollingFile::new(writer)), + .append(RollingFile::new(JsonEncoder::default(), writer)) + .append(Stdout::new(TextLayout::default())), ) - .dispatch(Dispatch::new().layout(TextLayout::default()).append(Stdout)) .apply() .unwrap(); diff --git a/examples/simple_stdio.rs b/examples/simple_stdio.rs index e29136b..ad62b08 100644 --- a/examples/simple_stdio.rs +++ b/examples/simple_stdio.rs @@ -23,14 +23,12 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .layout(TextLayout::default()) - .append(append::Stdout), + .append(append::Stdout::new(TextLayout::default())), ) .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .layout(TextLayout::default().no_color()) - .append(append::Stderr), + .append(append::Stderr::new(TextLayout::default().no_color())), ) .apply() .unwrap(); diff --git a/src/append/mod.rs b/src/append/mod.rs index 2a05fef..7375a73 100644 --- a/src/append/mod.rs +++ b/src/append/mod.rs @@ -24,8 +24,6 @@ pub use self::opentelemetry::OpentelemetryLog; pub use self::rolling_file::RollingFile; pub use self::stdio::Stderr; pub use self::stdio::Stdout; -use crate::layout::IdenticalLayout; -use crate::layout::Layout; #[cfg(feature = "fastrace")] mod fastrace; @@ -41,10 +39,4 @@ pub trait Append: fmt::Debug + Send + Sync + 'static { /// Flushes any buffered records. fn flush(&self) {} - - /// Default layout to use when [`Dispatch`][crate::logger::Dispatch] does not configure a - /// preferred layout. - fn default_layout(&self) -> Layout { - Layout::Identical(IdenticalLayout) - } } diff --git a/src/append/opentelemetry.rs b/src/append/opentelemetry.rs index 3dba414..3818d80 100644 --- a/src/append/opentelemetry.rs +++ b/src/append/opentelemetry.rs @@ -155,25 +155,25 @@ pub struct OpentelemetryLog { } impl Append for OpentelemetryLog { - fn append(&self, log_record: &Record) -> anyhow::Result<()> { + fn append(&self, record: &Record) -> anyhow::Result<()> { let provider = self.provider.clone(); let logger = provider.library_logger(self.library.clone()); - let mut record = LogRecord::default(); - record.observed_timestamp = Some(SystemTime::now()); - record.severity_number = Some(log_level_to_otel_severity(log_record.level())); - record.severity_text = Some(log_record.level().as_str()); - record.target = Some(log_record.target().to_string().into()); - record.body = Some(AnyValue::from(log_record.args().to_string())); + let mut log_record_ = LogRecord::default(); + log_record_.observed_timestamp = Some(SystemTime::now()); + log_record_.severity_number = Some(log_level_to_otel_severity(record.level())); + log_record_.severity_text = Some(record.level().as_str()); + log_record_.target = Some(record.target().to_string().into()); + log_record_.body = Some(AnyValue::from(record.args().to_string())); - if let Some(module_path) = log_record.module_path() { - record.add_attribute("module_path", module_path.to_string()); + if let Some(module_path) = record.module_path() { + log_record_.add_attribute("module_path", module_path.to_string()); } - if let Some(file) = log_record.file() { - record.add_attribute("file", file.to_string()); + if let Some(file) = record.file() { + log_record_.add_attribute("file", file.to_string()); } - if let Some(line) = log_record.line() { - record.add_attribute("line", line); + if let Some(line) = record.line() { + log_record_.add_attribute("line", line); } struct KvExtractor<'a> { @@ -193,11 +193,11 @@ impl Append for OpentelemetryLog { } let mut extractor = KvExtractor { - record: &mut record, + record: &mut log_record_, }; - log_record.key_values().visit(&mut extractor).ok(); + record.key_values().visit(&mut extractor).ok(); - logger.emit(record); + logger.emit(log_record_); Ok(()) } diff --git a/src/append/rolling_file/append.rs b/src/append/rolling_file/append.rs index 6c7a523..0d29438 100644 --- a/src/append/rolling_file/append.rs +++ b/src/append/rolling_file/append.rs @@ -16,23 +16,29 @@ use log::Record; use crate::append::rolling_file::non_blocking::NonBlocking; use crate::append::Append; +use crate::Encoder; /// An appender that writes log records to a file that rolls over when it reaches a certain date /// time. #[derive(Debug)] pub struct RollingFile { + encoder: Encoder, writer: NonBlocking, } impl RollingFile { - pub fn new(writer: NonBlocking) -> Self { - Self { writer } + pub fn new(encoder: impl Into, writer: NonBlocking) -> Self { + Self { + encoder: encoder.into(), + writer, + } } } impl Append for RollingFile { fn append(&self, record: &Record) -> anyhow::Result<()> { - let bytes = format!("{}\n", record.args()).into_bytes(); + let mut bytes = self.encoder.format(record)?; + bytes.push(b'\n'); self.writer.send(bytes)?; Ok(()) } diff --git a/src/append/stdio.rs b/src/append/stdio.rs index 5c780eb..da3b1b9 100644 --- a/src/append/stdio.rs +++ b/src/append/stdio.rs @@ -15,14 +15,27 @@ use std::io::Write; use crate::append::Append; +use crate::Encoder; /// An appender that prints log records to stdout. -#[derive(Default, Debug)] -pub struct Stdout; +#[derive(Debug)] +pub struct Stdout { + encoder: Encoder, +} + +impl Stdout { + /// Creates a new `Stdout` appender with the given encoder. + pub fn new(encoder: impl Into) -> Self { + Self { + encoder: encoder.into(), + } + } +} impl Append for Stdout { fn append(&self, record: &log::Record) -> anyhow::Result<()> { - let bytes = format!("{}\n", record.args()).into_bytes(); + let mut bytes = self.encoder.format(record)?; + bytes.push(b'\n'); std::io::stdout().write_all(&bytes)?; Ok(()) } @@ -33,12 +46,24 @@ impl Append for Stdout { } /// An appender that prints log records to stderr. -#[derive(Default, Debug)] -pub struct Stderr; +#[derive(Debug)] +pub struct Stderr { + encoder: Encoder, +} + +impl Stderr { + /// Creates a new `Stderr` appender with the given encoder. + pub fn new(encoder: impl Into) -> Self { + Self { + encoder: encoder.into(), + } + } +} impl Append for Stderr { fn append(&self, record: &log::Record) -> anyhow::Result<()> { - let bytes = format!("{}\n", record.args()).into_bytes(); + let mut bytes = self.encoder.format(record)?; + bytes.push(b'\n'); std::io::stderr().write_all(&bytes)?; Ok(()) } diff --git a/src/layout/identical.rs b/src/encoder/json.rs similarity index 50% rename from src/layout/identical.rs rename to src/encoder/json.rs index 18efecc..1d122fe 100644 --- a/src/layout/identical.rs +++ b/src/encoder/json.rs @@ -12,28 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt::Arguments; +use jiff::tz::TimeZone; +use log::Record; -use crate::layout::Layout; +use crate::Encoder; -/// A layout that returns log record as is. -/// -/// This is mainly used as the default implementation for -/// [`Append::default_layout`][crate::append::Append::default_layout]. -#[derive(Debug, Default, Clone, Copy)] -pub struct IdenticalLayout; +#[derive(Default, Debug, Clone)] +pub struct JsonEncoder { + pub tz: Option, +} -impl IdenticalLayout { - pub(crate) fn format(&self, record: &log::Record, f: &F) -> anyhow::Result<()> - where - F: Fn(Arguments) -> anyhow::Result<()>, - { - f(*record.args()) +impl JsonEncoder { + pub(crate) fn format(&self, record: &Record) -> anyhow::Result> { + let record_line = crate::format::json::do_format(record, self.tz.clone())?; + Ok(serde_json::to_vec(&record_line)?) } } -impl From for Layout { - fn from(layout: IdenticalLayout) -> Self { - Layout::Identical(layout) +impl From for Encoder { + fn from(encoder: JsonEncoder) -> Self { + Encoder::Json(encoder) } } diff --git a/src/encoder/layout_wrapping.rs b/src/encoder/layout_wrapping.rs new file mode 100644 index 0000000..fa2e40b --- /dev/null +++ b/src/encoder/layout_wrapping.rs @@ -0,0 +1,47 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use log::Record; + +use crate::encoder::Encoder; +use crate::Layout; + +#[derive(Debug)] +pub struct LayoutWrappingEncoder { + layout: Layout, +} + +impl LayoutWrappingEncoder { + pub fn new(layout: Layout) -> Self { + Self { layout } + } +} + +impl LayoutWrappingEncoder { + pub(crate) fn format(&self, record: &Record) -> anyhow::Result> { + self.layout.format(record).map(|s| s.into_bytes()) + } +} + +impl From for Encoder { + fn from(encoder: LayoutWrappingEncoder) -> Self { + Encoder::LayoutWrapping(encoder) + } +} + +impl From for Encoder { + fn from(layout: Layout) -> Self { + LayoutWrappingEncoder::new(layout).into() + } +} diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs new file mode 100644 index 0000000..29438da --- /dev/null +++ b/src/encoder/mod.rs @@ -0,0 +1,38 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod layout_wrapping; +pub use layout_wrapping::LayoutWrappingEncoder; + +#[cfg(feature = "json")] +mod json; +#[cfg(feature = "json")] +pub use json::JsonEncoder; + +#[derive(Debug)] +pub enum Encoder { + LayoutWrapping(LayoutWrappingEncoder), + #[cfg(feature = "json")] + Json(JsonEncoder), +} + +impl Encoder { + pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result> { + match self { + Encoder::LayoutWrapping(encoder) => encoder.format(record), + #[cfg(feature = "json")] + Encoder::Json(encoder) => encoder.format(record), + } + } +} diff --git a/src/format/json.rs b/src/format/json.rs new file mode 100644 index 0000000..3b83d77 --- /dev/null +++ b/src/format/json.rs @@ -0,0 +1,89 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Arguments; + +use jiff::tz::TimeZone; +use jiff::Timestamp; +use jiff::Zoned; +use log::Record; +use serde::Serialize; +use serde_json::Map; +use serde_json::Value; + +struct KvCollector<'a> { + kvs: &'a mut Map, +} + +impl<'a, 'kvs> log::kv::Visitor<'kvs> for KvCollector<'a> { + fn visit_pair( + &mut self, + key: log::kv::Key<'kvs>, + value: log::kv::Value<'kvs>, + ) -> Result<(), log::kv::Error> { + let k = key.to_string(); + let v = value.to_string(); + self.kvs.insert(k, v.into()); + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct RecordLine<'a> { + #[serde(serialize_with = "serialize_time_zone")] + timestamp: Zoned, + level: &'a str, + module_path: &'a str, + file: &'a str, + line: u32, + #[serde(serialize_with = "serialize_args")] + message: &'a Arguments<'a>, + kvs: Map, +} + +fn serialize_time_zone(timestamp: &Zoned, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.collect_str(&format_args!("{timestamp:.6}")) +} + +fn serialize_args(args: &Arguments, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.collect_str(args) +} + +pub(crate) fn do_format<'a>( + record: &'a Record<'a>, + tz: Option, +) -> anyhow::Result> { + let mut kvs = Map::new(); + let mut visitor = KvCollector { kvs: &mut kvs }; + record.key_values().visit(&mut visitor)?; + + Ok(RecordLine { + timestamp: match tz { + Some(tz) => Timestamp::now().to_zoned(tz), + None => Zoned::now(), + }, + level: record.level().as_str(), + module_path: record.module_path().unwrap_or_default(), + file: record.file().unwrap_or_default(), + line: record.line().unwrap_or_default(), + message: record.args(), + kvs, + }) +} diff --git a/src/format/mod.rs b/src/format/mod.rs new file mode 100644 index 0000000..34a0fa9 --- /dev/null +++ b/src/format/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Shared logics between `encoder` and `layout`. + +#[cfg(feature = "json")] +pub(crate) mod json; diff --git a/src/layout/custom.rs b/src/layout/custom.rs index 0235904..9aba8bc 100644 --- a/src/layout/custom.rs +++ b/src/layout/custom.rs @@ -12,20 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt::Arguments; use std::fmt::Debug; use std::fmt::Formatter; use log::Record; +use crate::encoder::LayoutWrappingEncoder; use crate::layout::Layout; +use crate::Encoder; // TODO(tisonkun): use trait alias when it's stable - https://github.com/rust-lang/rust/issues/41517 // then we can use the alias for both `dyn` and `impl`. -type FormatFunction = dyn Fn(&Record, &dyn Fn(Arguments) -> anyhow::Result<()>) -> anyhow::Result<()> - + Send - + Sync - + 'static; +type FormatFunction = dyn Fn(&Record) -> anyhow::Result + Send + Sync + 'static; /// A layout that you can pass the custom layout function. /// @@ -38,11 +36,8 @@ type FormatFunction = dyn Fn(&Record, &dyn Fn(Arguments) -> anyhow::Result<()>) /// use log::Record; /// use logforth::layout::CustomLayout; /// -/// let layout = CustomLayout::new( -/// |record: &Record, f: &dyn Fn(Arguments) -> anyhow::Result<()>| { -/// f(format_args!("{} - {}", record.level(), record.args())) -/// }, -/// ); +/// let layout = +/// CustomLayout::new(|record: &Record| Ok(format!("{} - {}", record.level(), record.args()))); /// ``` pub struct CustomLayout { f: Box, @@ -55,22 +50,14 @@ impl Debug for CustomLayout { } impl CustomLayout { - pub fn new( - layout: impl Fn(&Record, &dyn Fn(Arguments) -> anyhow::Result<()>) -> anyhow::Result<()> - + Send - + Sync - + 'static, - ) -> Self { + pub fn new(layout: impl Fn(&Record) -> anyhow::Result + Send + Sync + 'static) -> Self { CustomLayout { f: Box::new(layout), } } - pub(crate) fn format(&self, record: &Record, f: &F) -> anyhow::Result<()> - where - F: Fn(Arguments) -> anyhow::Result<()>, - { - (self.f)(record, f) + pub(crate) fn format(&self, record: &Record) -> anyhow::Result { + (self.f)(record) } } @@ -79,3 +66,9 @@ impl From for Layout { Layout::Custom(layout) } } + +impl From for Encoder { + fn from(layout: CustomLayout) -> Self { + LayoutWrappingEncoder::new(layout.into()).into() + } +} diff --git a/src/layout/json.rs b/src/layout/json.rs index ee3ed14..78d762a 100644 --- a/src/layout/json.rs +++ b/src/layout/json.rs @@ -12,17 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt::Arguments; - use jiff::tz::TimeZone; -use jiff::Timestamp; -use jiff::Zoned; use log::Record; -use serde::Serialize; -use serde_json::Map; -use serde_json::Value; +use crate::encoder::LayoutWrappingEncoder; use crate::layout::Layout; +use crate::Encoder; /// A layout that formats log record as JSON lines. /// @@ -43,74 +38,10 @@ pub struct JsonLayout { pub tz: Option, } -struct KvCollector<'a> { - kvs: &'a mut Map, -} - -impl<'a, 'kvs> log::kv::Visitor<'kvs> for KvCollector<'a> { - fn visit_pair( - &mut self, - key: log::kv::Key<'kvs>, - value: log::kv::Value<'kvs>, - ) -> Result<(), log::kv::Error> { - let k = key.to_string(); - let v = value.to_string(); - self.kvs.insert(k, v.into()); - Ok(()) - } -} - -#[derive(Debug, Clone, Serialize)] -struct RecordLine<'a> { - #[serde(serialize_with = "serialize_time_zone")] - timestamp: Zoned, - level: &'a str, - module_path: &'a str, - file: &'a str, - line: u32, - #[serde(serialize_with = "serialize_args")] - message: &'a Arguments<'a>, - kvs: Map, -} - -fn serialize_time_zone(timestamp: &Zoned, serializer: S) -> Result -where - S: serde::Serializer, -{ - serializer.collect_str(&format_args!("{timestamp:.6}")) -} - -fn serialize_args(args: &Arguments, serializer: S) -> Result -where - S: serde::Serializer, -{ - serializer.collect_str(args) -} - impl JsonLayout { - pub(crate) fn format(&self, record: &Record, f: &F) -> anyhow::Result<()> - where - F: Fn(Arguments) -> anyhow::Result<()>, - { - let mut kvs = Map::new(); - let mut visitor = KvCollector { kvs: &mut kvs }; - record.key_values().visit(&mut visitor)?; - - let record_line = RecordLine { - timestamp: match self.tz.clone() { - Some(tz) => Timestamp::now().to_zoned(tz), - None => Zoned::now(), - }, - level: record.level().as_str(), - module_path: record.module_path().unwrap_or_default(), - file: record.file().unwrap_or_default(), - line: record.line().unwrap_or_default(), - message: record.args(), - kvs, - }; - - let text = serde_json::to_string(&record_line)?; - f(format_args!("{text}")) + pub(crate) fn format(&self, record: &Record) -> anyhow::Result { + let record_line = crate::format::json::do_format(record, self.tz.clone())?; + Ok(serde_json::to_string(&record_line)?) } } @@ -119,3 +50,9 @@ impl From for Layout { Layout::Json(layout) } } + +impl From for Encoder { + fn from(layout: JsonLayout) -> Self { + LayoutWrappingEncoder::new(layout.into()).into() + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 5ac9a21..428c02a 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -15,7 +15,6 @@ //! Describe how to format a log record. pub use custom::CustomLayout; -pub use identical::IdenticalLayout; #[cfg(feature = "json")] pub use json::JsonLayout; pub use kv::collect_kvs; @@ -24,7 +23,6 @@ pub use text::LevelColor; pub use text::TextLayout; mod custom; -mod identical; #[cfg(feature = "json")] mod json; mod kv; @@ -33,32 +31,19 @@ mod text; /// A layout describes how to format a log record. #[derive(Debug)] pub enum Layout { - Identical(IdenticalLayout), + Custom(CustomLayout), Text(TextLayout), #[cfg(feature = "json")] Json(JsonLayout), - Custom(CustomLayout), } impl Layout { - pub(crate) fn format(&self, record: &log::Record, f: &F) -> anyhow::Result<()> - where - F: Fn(&log::Record) -> anyhow::Result<()>, - { + pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result { match self { - Layout::Identical(layout) => { - layout.format(record, &|args| f(&record.to_builder().args(args).build())) - } - Layout::Text(layout) => { - layout.format(record, &|args| f(&record.to_builder().args(args).build())) - } + Layout::Custom(layout) => layout.format(record), + Layout::Text(layout) => layout.format(record), #[cfg(feature = "json")] - Layout::Json(layout) => { - layout.format(record, &|args| f(&record.to_builder().args(args).build())) - } - Layout::Custom(layout) => { - layout.format(record, &|args| f(&record.to_builder().args(args).build())) - } + Layout::Json(layout) => layout.format(record), } } } diff --git a/src/layout/text.rs b/src/layout/text.rs index 84cf72f..48a936c 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -13,7 +13,6 @@ // limitations under the License. use std::borrow::Cow; -use std::fmt::Arguments; use colored::Color; use colored::ColoredString; @@ -23,8 +22,10 @@ use jiff::Timestamp; use jiff::Zoned; use log::Level; +use crate::encoder::LayoutWrappingEncoder; use crate::layout::KvDisplay; use crate::layout::Layout; +use crate::Encoder; /// A layout that formats log record as text. /// @@ -132,10 +133,7 @@ impl Default for LevelColor { } impl TextLayout { - pub(crate) fn format(&self, record: &log::Record, f: &F) -> anyhow::Result<()> - where - F: Fn(Arguments) -> anyhow::Result<()>, - { + pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result { let time = match self.tz.clone() { Some(tz) => Timestamp::now().to_zoned(tz), None => Zoned::now(), @@ -158,7 +156,7 @@ impl TextLayout { let message = record.args(); let kvs = KvDisplay::new(record.key_values()); - f(format_args!( + Ok(format!( "{time:.6} {level:>5} {module}: {file}:{line} {message}{kvs}" )) } @@ -170,6 +168,12 @@ impl From for Layout { } } +impl From for Encoder { + fn from(layout: TextLayout) -> Self { + LayoutWrappingEncoder::new(layout.into()).into() + } +} + // obtain filename only from record's full file path // reason: the module is already logged + full file path is noisy for text layout fn filename<'a>(record: &'a log::Record<'a>) -> Cow<'a, str> { diff --git a/src/lib.rs b/src/lib.rs index 51400c9..1b70941 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,8 +38,7 @@ //! .dispatch( //! Dispatch::new() //! .filter(LevelFilter::Trace) -//! .layout(TextLayout::default()) -//! .append(append::Stdout), +//! .append(append::Stdout::new(TextLayout::default())), //! ) //! .apply() //! .unwrap(); @@ -56,11 +55,16 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod append; +pub mod encoder; pub mod filter; pub mod layout; mod logger; +// utilities +pub(crate) mod format; + pub use append::Append; +pub use encoder::Encoder; pub use filter::Filter; pub use layout::Layout; pub use logger::Dispatch; diff --git a/src/logger.rs b/src/logger.rs index 4c283be..f01a0b1 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -21,67 +21,52 @@ use log::Record; use crate::append::Append; use crate::filter::Filter; use crate::filter::FilterResult; -use crate::layout::Layout; -/// A grouped set of appenders, filters, and optional layout. +/// A grouped set of appenders and filters. /// /// The [`Logger`] facade dispatches log records to one or more [`Dispatch`] instances. -/// Each [`Dispatch`] instance contains a set of filters, appenders, and an optional layout. +/// Each [`Dispatch`] instance contains a set of filters and appenders. /// /// `filters` are used to determine whether a log record should be passed to the appenders. -/// `appends` are used to write log records to a destination. Each appender has its own -/// default layout. If the [`Dispatch`] has a layout, it will be used instead of the default layout. +/// `appends` are used to write log records to a destination. #[derive(Debug)] -pub struct Dispatch { +pub struct Dispatch { filters: Vec, appends: Vec>, - layout: Option, } -impl Default for Dispatch { - fn default() -> Dispatch { +impl Default for Dispatch { + fn default() -> Dispatch { Self::new() } } -impl Dispatch { +impl Dispatch { /// Create a new incomplete [`Dispatch`] instance. /// /// At least one append must be added to the [`Dispatch`] before it can be used. - pub fn new() -> Dispatch { + pub fn new() -> Dispatch { Self { filters: vec![], appends: vec![], - layout: None, } } /// Add a [`Filter`] to the [`Dispatch`]. - pub fn filter(mut self, filter: impl Into) -> Dispatch { + pub fn filter(mut self, filter: impl Into) -> Dispatch { self.filters.push(filter.into()); self } - - /// Add the preferred [`Layout`] to the [`Dispatch`]. At most one layout can be added to a - /// [`Dispatch`]. - pub fn layout(self, layout: impl Into) -> Dispatch { - Dispatch { - filters: self.filters, - appends: self.appends, - layout: Some(layout.into()), - } - } } -impl Dispatch { +impl Dispatch { /// Add an [`Append`] to the [`Dispatch`]. - pub fn append(mut self, append: impl Append) -> Dispatch { + pub fn append(mut self, append: impl Append) -> Dispatch { self.appends.push(Box::new(append)); Dispatch { filters: self.filters, appends: self.appends, - layout: self.layout, } } } @@ -108,14 +93,8 @@ impl Dispatch { } } - let layout = self.layout.as_ref(); for append in &self.appends { - match layout { - Some(layout) => layout.format(record, &|record| append.append(record))?, - None => append - .default_layout() - .format(record, &|record| append.append(record))?, - } + append.append(record)?; } Ok(()) } diff --git a/tests/recursive_logging.rs b/tests/recursive_logging.rs index 5953477..3cdf47e 100644 --- a/tests/recursive_logging.rs +++ b/tests/recursive_logging.rs @@ -34,19 +34,15 @@ fn test_meta_logging_in_format_works() { let (writer, _guard) = NonBlockingBuilder::default().finish(rolling); let layout = |src: &'static str| { - layout::CustomLayout::new(move |record, f| { - f(format_args!("{src} [{}] {}", record.level(), record.args())) + layout::CustomLayout::new(move |record| { + Ok(format!("{src} [{}] {}", record.level(), record.args())) }) }; Logger::new() - .dispatch(Dispatch::new().layout(layout("out")).append(append::Stdout)) - .dispatch(Dispatch::new().layout(layout("err")).append(append::Stderr)) - .dispatch( - Dispatch::new() - .layout(layout("file")) - .append(append::RollingFile::new(writer)), - ) + .dispatch(Dispatch::new().append(append::Stdout::new(layout("out")))) + .dispatch(Dispatch::new().append(append::Stderr::new(layout("err")))) + .dispatch(Dispatch::new().append(append::RollingFile::new(layout("file"), writer))) .apply() .unwrap(); From 8ccfbea1826702db093fe2a4ad6a011d45a24098 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 15 Oct 2024 09:32:33 +0800 Subject: [PATCH 2/9] Add custom encoder Signed-off-by: tison --- src/encoder/custom.rs | 67 +++++++++++++++++++++++++++++++++++++++++++ src/encoder/mod.rs | 5 ++++ src/layout/custom.rs | 6 ++-- 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 src/encoder/custom.rs diff --git a/src/encoder/custom.rs b/src/encoder/custom.rs new file mode 100644 index 0000000..9c77ba6 --- /dev/null +++ b/src/encoder/custom.rs @@ -0,0 +1,67 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::fmt::Formatter; + +use log::Record; + +use crate::Encoder; + +// TODO(tisonkun): use trait alias when it's stable - https://github.com/rust-lang/rust/issues/41517 +// then we can use the alias for both `dyn` and `impl`. +type FormatFunction = dyn Fn(&Record) -> anyhow::Result> + Send + Sync + 'static; + +/// An encoder that you can pass the custom encoder function. +/// +/// The custom encoder function accepts [`&log::Record`][Record] and formats it into [`Vec`]. +/// For example: +/// +/// ```rust +/// use log::Record; +/// use logforth::encoder::CustomEncoder; +/// +/// let layout = CustomEncoder::new(|record: &Record| { +/// Ok(format!("{} - {}", record.level(), record.args()).into_bytes()) +/// }); +/// ``` +pub struct CustomEncoder { + f: Box, +} + +impl Debug for CustomEncoder { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "CustomEncoder {{ ... }}") + } +} + +impl CustomEncoder { + pub fn new( + layout: impl Fn(&Record) -> anyhow::Result> + Send + Sync + 'static, + ) -> Self { + CustomEncoder { + f: Box::new(layout), + } + } + + pub(crate) fn format(&self, record: &Record) -> anyhow::Result> { + (self.f)(record) + } +} + +impl From for Encoder { + fn from(encoder: CustomEncoder) -> Self { + Encoder::Custom(encoder) + } +} diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 29438da..1f1b105 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod custom; +pub use custom::CustomEncoder; + mod layout_wrapping; pub use layout_wrapping::LayoutWrappingEncoder; @@ -22,6 +25,7 @@ pub use json::JsonEncoder; #[derive(Debug)] pub enum Encoder { + Custom(CustomEncoder), LayoutWrapping(LayoutWrappingEncoder), #[cfg(feature = "json")] Json(JsonEncoder), @@ -30,6 +34,7 @@ pub enum Encoder { impl Encoder { pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result> { match self { + Encoder::Custom(encoder) => encoder.format(record), Encoder::LayoutWrapping(encoder) => encoder.format(record), #[cfg(feature = "json")] Encoder::Json(encoder) => encoder.format(record), diff --git a/src/layout/custom.rs b/src/layout/custom.rs index 9aba8bc..17ff05e 100644 --- a/src/layout/custom.rs +++ b/src/layout/custom.rs @@ -27,12 +27,10 @@ type FormatFunction = dyn Fn(&Record) -> anyhow::Result + Send + Sync + /// A layout that you can pass the custom layout function. /// -/// The custom layout function accepts [`&log::Record`][Record], formats it into [Arguments], and -/// then passes to the closure. For example: +/// The custom layout function accepts [`&log::Record`][Record] and formats it into [`String`]. +/// For example: /// /// ```rust -/// use std::fmt::Arguments; -/// /// use log::Record; /// use logforth::layout::CustomLayout; /// From 14318bc2ef5b7abd72e467ff12f93d1af6fb83af Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 15 Oct 2024 09:38:51 +0800 Subject: [PATCH 3/9] tidy impl Signed-off-by: tison --- src/encoder/layout_wrapping.rs | 8 +++++--- src/layout/custom.rs | 8 -------- src/layout/json.rs | 8 -------- src/layout/text.rs | 8 -------- 4 files changed, 5 insertions(+), 27 deletions(-) diff --git a/src/encoder/layout_wrapping.rs b/src/encoder/layout_wrapping.rs index fa2e40b..6313d0b 100644 --- a/src/encoder/layout_wrapping.rs +++ b/src/encoder/layout_wrapping.rs @@ -40,8 +40,10 @@ impl From for Encoder { } } -impl From for Encoder { - fn from(layout: Layout) -> Self { - LayoutWrappingEncoder::new(layout).into() +impl> From for Encoder { + fn from(layout: L) -> Self { + let layout = layout.into(); + let encoder = LayoutWrappingEncoder::new(layout); + Encoder::LayoutWrapping(encoder) } } diff --git a/src/layout/custom.rs b/src/layout/custom.rs index 17ff05e..acf9449 100644 --- a/src/layout/custom.rs +++ b/src/layout/custom.rs @@ -17,9 +17,7 @@ use std::fmt::Formatter; use log::Record; -use crate::encoder::LayoutWrappingEncoder; use crate::layout::Layout; -use crate::Encoder; // TODO(tisonkun): use trait alias when it's stable - https://github.com/rust-lang/rust/issues/41517 // then we can use the alias for both `dyn` and `impl`. @@ -64,9 +62,3 @@ impl From for Layout { Layout::Custom(layout) } } - -impl From for Encoder { - fn from(layout: CustomLayout) -> Self { - LayoutWrappingEncoder::new(layout.into()).into() - } -} diff --git a/src/layout/json.rs b/src/layout/json.rs index 78d762a..fd3df93 100644 --- a/src/layout/json.rs +++ b/src/layout/json.rs @@ -15,9 +15,7 @@ use jiff::tz::TimeZone; use log::Record; -use crate::encoder::LayoutWrappingEncoder; use crate::layout::Layout; -use crate::Encoder; /// A layout that formats log record as JSON lines. /// @@ -50,9 +48,3 @@ impl From for Layout { Layout::Json(layout) } } - -impl From for Encoder { - fn from(layout: JsonLayout) -> Self { - LayoutWrappingEncoder::new(layout.into()).into() - } -} diff --git a/src/layout/text.rs b/src/layout/text.rs index 48a936c..0a9a229 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -22,10 +22,8 @@ use jiff::Timestamp; use jiff::Zoned; use log::Level; -use crate::encoder::LayoutWrappingEncoder; use crate::layout::KvDisplay; use crate::layout::Layout; -use crate::Encoder; /// A layout that formats log record as text. /// @@ -168,12 +166,6 @@ impl From for Layout { } } -impl From for Encoder { - fn from(layout: TextLayout) -> Self { - LayoutWrappingEncoder::new(layout.into()).into() - } -} - // obtain filename only from record's full file path // reason: the module is already logged + full file path is noisy for text layout fn filename<'a>(record: &'a log::Record<'a>) -> Cow<'a, str> { From 3ab4bef2105ea768fa8243d90fd34ac9b1324a91 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 15 Oct 2024 12:04:34 +0800 Subject: [PATCH 4/9] OpentelemetryLog can accept layout Signed-off-by: tison --- src/append/opentelemetry.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/append/opentelemetry.rs b/src/append/opentelemetry.rs index 3818d80..98e29ff 100644 --- a/src/append/opentelemetry.rs +++ b/src/append/opentelemetry.rs @@ -30,6 +30,7 @@ use opentelemetry_sdk::logs::LogRecord; use opentelemetry_sdk::logs::LoggerProvider; use crate::append::Append; +use crate::Layout; /// The communication protocol to opentelemetry that used when exporting data. /// @@ -52,6 +53,7 @@ pub struct OpentelemetryLogBuilder { endpoint: String, protocol: Protocol, labels: Vec<(Cow<'static, str>, Cow<'static, str>)>, + layout: Option, } impl OpentelemetryLogBuilder { @@ -62,6 +64,7 @@ impl OpentelemetryLogBuilder { endpoint: otlp_endpoint.into(), protocol: Protocol::Grpc, labels: vec![], + layout: None, } } @@ -100,6 +103,12 @@ impl OpentelemetryLogBuilder { self } + /// Set the layout to use when formatting log records. + pub fn layout(mut self, layout: impl Into) -> Self { + self.layout = Some(layout.into()); + self + } + /// Build the [`OpentelemetryLog`] appender. pub fn build(self) -> Result { let OpentelemetryLogBuilder { @@ -107,6 +116,7 @@ impl OpentelemetryLogBuilder { endpoint, protocol, labels, + layout, } = self; let collector_timeout = @@ -140,6 +150,7 @@ impl OpentelemetryLogBuilder { Ok(OpentelemetryLog { name, + layout, library, provider, }) @@ -150,6 +161,7 @@ impl OpentelemetryLogBuilder { #[derive(Debug)] pub struct OpentelemetryLog { name: String, + layout: Option, library: Arc, provider: LoggerProvider, } @@ -164,7 +176,10 @@ impl Append for OpentelemetryLog { log_record_.severity_number = Some(log_level_to_otel_severity(record.level())); log_record_.severity_text = Some(record.level().as_str()); log_record_.target = Some(record.target().to_string().into()); - log_record_.body = Some(AnyValue::from(record.args().to_string())); + log_record_.body = Some(AnyValue::from(match self.layout.as_ref() { + None => record.args().to_string(), + Some(layout) => layout.format(record)?, + })); if let Some(module_path) = record.module_path() { log_record_.add_attribute("module_path", module_path.to_string()); From 4dd7a2d741d316f2ea9a7f50b3107c11be6d698b Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 15 Oct 2024 12:07:26 +0800 Subject: [PATCH 5/9] simplify converters Signed-off-by: tison --- src/encoder/layout_wrapping.rs | 49 ---------------------------------- src/encoder/mod.rs | 15 +++++++---- src/layout/mod.rs | 11 ++++++++ 3 files changed, 21 insertions(+), 54 deletions(-) delete mode 100644 src/encoder/layout_wrapping.rs diff --git a/src/encoder/layout_wrapping.rs b/src/encoder/layout_wrapping.rs deleted file mode 100644 index 6313d0b..0000000 --- a/src/encoder/layout_wrapping.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2024 FastLabs Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use log::Record; - -use crate::encoder::Encoder; -use crate::Layout; - -#[derive(Debug)] -pub struct LayoutWrappingEncoder { - layout: Layout, -} - -impl LayoutWrappingEncoder { - pub fn new(layout: Layout) -> Self { - Self { layout } - } -} - -impl LayoutWrappingEncoder { - pub(crate) fn format(&self, record: &Record) -> anyhow::Result> { - self.layout.format(record).map(|s| s.into_bytes()) - } -} - -impl From for Encoder { - fn from(encoder: LayoutWrappingEncoder) -> Self { - Encoder::LayoutWrapping(encoder) - } -} - -impl> From for Encoder { - fn from(layout: L) -> Self { - let layout = layout.into(); - let encoder = LayoutWrappingEncoder::new(layout); - Encoder::LayoutWrapping(encoder) - } -} diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 1f1b105..aca9d33 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -15,18 +15,15 @@ mod custom; pub use custom::CustomEncoder; -mod layout_wrapping; -pub use layout_wrapping::LayoutWrappingEncoder; - #[cfg(feature = "json")] mod json; +use crate::Layout; #[cfg(feature = "json")] pub use json::JsonEncoder; #[derive(Debug)] pub enum Encoder { Custom(CustomEncoder), - LayoutWrapping(LayoutWrappingEncoder), #[cfg(feature = "json")] Json(JsonEncoder), } @@ -35,9 +32,17 @@ impl Encoder { pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result> { match self { Encoder::Custom(encoder) => encoder.format(record), - Encoder::LayoutWrapping(encoder) => encoder.format(record), #[cfg(feature = "json")] Encoder::Json(encoder) => encoder.format(record), } } } + +impl> From for Encoder { + fn from(layout: L) -> Self { + let layout = layout.into(); + Encoder::Custom(CustomEncoder::new(move |record| { + Ok(layout.format(record)?.into_bytes()) + })) + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 428c02a..236da98 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -14,6 +14,7 @@ //! Describe how to format a log record. +use crate::Encoder; pub use custom::CustomLayout; #[cfg(feature = "json")] pub use json::JsonLayout; @@ -47,3 +48,13 @@ impl Layout { } } } + +impl> From for Layout { + fn from(encoder: E) -> Self { + let encoder = encoder.into(); + Layout::Custom(CustomLayout::new(move |record| { + let bytes = encoder.format(record)?; + Ok(String::from_utf8_lossy(&bytes).to_string()) + })) + } +} From 62adfedb893b3917c016a6bd835894b34832bb32 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 15 Oct 2024 12:17:26 +0800 Subject: [PATCH 6/9] add default layout for appenders Signed-off-by: tison --- examples/env_filter.rs | 2 +- examples/fn_layout_filter.rs | 8 +++++--- examples/json_stdio.rs | 2 +- examples/rolling_file.rs | 4 ++-- examples/simple_stdio.rs | 8 ++------ src/append/fastrace.rs | 4 +++- src/append/rolling_file/append.rs | 14 ++++++++++++-- src/append/stdio.rs | 31 +++++++++++++++++++++++-------- src/encoder/mod.rs | 3 ++- src/layout/mod.rs | 3 ++- 10 files changed, 53 insertions(+), 26 deletions(-) diff --git a/examples/env_filter.rs b/examples/env_filter.rs index a61439d..92ff8d5 100644 --- a/examples/env_filter.rs +++ b/examples/env_filter.rs @@ -23,7 +23,7 @@ fn main() { .dispatch( Dispatch::new() .filter(EnvFilter::from_default_env()) - .append(append::Stdout::new(TextLayout::default())), + .append(append::Stdout::default()), ) .apply() .unwrap(); diff --git a/examples/fn_layout_filter.rs b/examples/fn_layout_filter.rs index 30de206..187f860 100644 --- a/examples/fn_layout_filter.rs +++ b/examples/fn_layout_filter.rs @@ -31,9 +31,11 @@ fn main() { FilterResult::Reject } })) - .append(append::Stdout::new(CustomLayout::new(|record| { - Ok(format!("[system alert] {}", record.args())) - }))), + .append( + append::Stdout::default().with_encoder(CustomLayout::new(|record| { + Ok(format!("[system alert] {}", record.args())) + })), + ), ) .apply() .unwrap(); diff --git a/examples/json_stdio.rs b/examples/json_stdio.rs index c92730a..c33516c 100644 --- a/examples/json_stdio.rs +++ b/examples/json_stdio.rs @@ -23,7 +23,7 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .append(append::Stdout::new(JsonEncoder::default())), + .append(append::Stdout::default().with_encoder(JsonEncoder::default())), ) .apply() .unwrap(); diff --git a/examples/rolling_file.rs b/examples/rolling_file.rs index 21776c2..27987aa 100644 --- a/examples/rolling_file.rs +++ b/examples/rolling_file.rs @@ -38,8 +38,8 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .append(RollingFile::new(JsonEncoder::default(), writer)) - .append(Stdout::new(TextLayout::default())), + .append(RollingFile::new(writer).with_encoder(JsonEncoder::default())) + .append(Stdout::default()), ) .apply() .unwrap(); diff --git a/examples/simple_stdio.rs b/examples/simple_stdio.rs index ad62b08..e52239a 100644 --- a/examples/simple_stdio.rs +++ b/examples/simple_stdio.rs @@ -23,12 +23,8 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .append(append::Stdout::new(TextLayout::default())), - ) - .dispatch( - Dispatch::new() - .filter(LevelFilter::Trace) - .append(append::Stderr::new(TextLayout::default().no_color())), + .append(append::Stdout::default()) + .append(append::Stderr::default()), ) .apply() .unwrap(); diff --git a/src/append/fastrace.rs b/src/append/fastrace.rs index f252464..d90a0f3 100644 --- a/src/append/fastrace.rs +++ b/src/append/fastrace.rs @@ -22,7 +22,9 @@ use crate::layout::collect_kvs; /// An appender that adds log records to fastrace as an event associated to the current span. #[derive(Default, Debug, Clone)] -pub struct FastraceEvent; +pub struct FastraceEvent { + _private: (), // suppress structure literal syntax +} impl Append for FastraceEvent { fn append(&self, record: &Record) -> anyhow::Result<()> { diff --git a/src/append/rolling_file/append.rs b/src/append/rolling_file/append.rs index 0d29438..90f7afc 100644 --- a/src/append/rolling_file/append.rs +++ b/src/append/rolling_file/append.rs @@ -16,6 +16,7 @@ use log::Record; use crate::append::rolling_file::non_blocking::NonBlocking; use crate::append::Append; +use crate::layout::TextLayout; use crate::Encoder; /// An appender that writes log records to a file that rolls over when it reaches a certain date @@ -27,12 +28,21 @@ pub struct RollingFile { } impl RollingFile { - pub fn new(encoder: impl Into, writer: NonBlocking) -> Self { + /// Creates a new `RollingFile` appender that writes log records to the given writer. + /// + /// This appender by default uses [`TextLayout`] to format log records as bytes. + pub fn new(writer: NonBlocking) -> Self { Self { - encoder: encoder.into(), + encoder: TextLayout::default().no_color().into(), writer, } } + + /// Sets the encoder used to format log records as bytes. + pub fn with_encoder(mut self, encoder: impl Into) -> Self { + self.encoder = encoder.into(); + self + } } impl Append for RollingFile { diff --git a/src/append/stdio.rs b/src/append/stdio.rs index da3b1b9..3c80f24 100644 --- a/src/append/stdio.rs +++ b/src/append/stdio.rs @@ -15,6 +15,7 @@ use std::io::Write; use crate::append::Append; +use crate::layout::TextLayout; use crate::Encoder; /// An appender that prints log records to stdout. @@ -23,15 +24,22 @@ pub struct Stdout { encoder: Encoder, } -impl Stdout { - /// Creates a new `Stdout` appender with the given encoder. - pub fn new(encoder: impl Into) -> Self { +impl Default for Stdout { + fn default() -> Self { Self { - encoder: encoder.into(), + encoder: TextLayout::default().into(), } } } +impl Stdout { + /// Creates a new `Stdout` appender with the given encoder. + pub fn with_encoder(mut self, encoder: impl Into) -> Self { + self.encoder = encoder.into(); + self + } +} + impl Append for Stdout { fn append(&self, record: &log::Record) -> anyhow::Result<()> { let mut bytes = self.encoder.format(record)?; @@ -51,15 +59,22 @@ pub struct Stderr { encoder: Encoder, } -impl Stderr { - /// Creates a new `Stderr` appender with the given encoder. - pub fn new(encoder: impl Into) -> Self { +impl Default for Stderr { + fn default() -> Self { Self { - encoder: encoder.into(), + encoder: TextLayout::default().into(), } } } +impl Stderr { + /// Creates a new `Stderr` appender with the given encoder. + pub fn with_encoder(mut self, encoder: impl Into) -> Self { + self.encoder = encoder.into(); + self + } +} + impl Append for Stderr { fn append(&self, record: &log::Record) -> anyhow::Result<()> { let mut bytes = self.encoder.format(record)?; diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index aca9d33..261b545 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -17,10 +17,11 @@ pub use custom::CustomEncoder; #[cfg(feature = "json")] mod json; -use crate::Layout; #[cfg(feature = "json")] pub use json::JsonEncoder; +use crate::Layout; + #[derive(Debug)] pub enum Encoder { Custom(CustomEncoder), diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 236da98..b0b46e5 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -14,7 +14,6 @@ //! Describe how to format a log record. -use crate::Encoder; pub use custom::CustomLayout; #[cfg(feature = "json")] pub use json::JsonLayout; @@ -23,6 +22,8 @@ pub use kv::KvDisplay; pub use text::LevelColor; pub use text::TextLayout; +use crate::Encoder; + mod custom; #[cfg(feature = "json")] mod json; From bfe8ff61db93232e0ba300c24ad49a3fdc312d32 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 15 Oct 2024 12:20:24 +0800 Subject: [PATCH 7/9] avoid conflict impls Signed-off-by: tison --- README.md | 2 +- examples/env_filter.rs | 1 - examples/rolling_file.rs | 1 - examples/simple_stdio.rs | 1 - src/layout/mod.rs | 11 ----------- src/lib.rs | 2 +- tests/recursive_logging.rs | 8 +++++--- 7 files changed, 7 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 00ebcaf..aeb78f5 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ fn main() { Logger::new().dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .append(append::Stdout::new(TextLayout::default())), + .append(append::Stdout::default()), ) .apply() .unwrap(); diff --git a/examples/env_filter.rs b/examples/env_filter.rs index 92ff8d5..3011195 100644 --- a/examples/env_filter.rs +++ b/examples/env_filter.rs @@ -14,7 +14,6 @@ use logforth::append; use logforth::filter::EnvFilter; -use logforth::layout::TextLayout; use logforth::Dispatch; use logforth::Logger; diff --git a/examples/rolling_file.rs b/examples/rolling_file.rs index 27987aa..5514dc6 100644 --- a/examples/rolling_file.rs +++ b/examples/rolling_file.rs @@ -19,7 +19,6 @@ use logforth::append::rolling_file::RollingFileWriter; use logforth::append::rolling_file::Rotation; use logforth::append::Stdout; use logforth::encoder::JsonEncoder; -use logforth::layout::TextLayout; use logforth::Dispatch; use logforth::Logger; diff --git a/examples/simple_stdio.rs b/examples/simple_stdio.rs index e52239a..c039e4a 100644 --- a/examples/simple_stdio.rs +++ b/examples/simple_stdio.rs @@ -14,7 +14,6 @@ use log::LevelFilter; use logforth::append; -use logforth::layout::TextLayout; use logforth::Dispatch; use logforth::Logger; diff --git a/src/layout/mod.rs b/src/layout/mod.rs index b0b46e5..66d1d64 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -22,7 +22,6 @@ pub use kv::KvDisplay; pub use text::LevelColor; pub use text::TextLayout; -use crate::Encoder; mod custom; #[cfg(feature = "json")] @@ -49,13 +48,3 @@ impl Layout { } } } - -impl> From for Layout { - fn from(encoder: E) -> Self { - let encoder = encoder.into(); - Layout::Custom(CustomLayout::new(move |record| { - let bytes = encoder.format(record)?; - Ok(String::from_utf8_lossy(&bytes).to_string()) - })) - } -} diff --git a/src/lib.rs b/src/lib.rs index 1b70941..b86b9f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ //! .dispatch( //! Dispatch::new() //! .filter(LevelFilter::Trace) -//! .append(append::Stdout::new(TextLayout::default())), +//! .append(append::Stdout::default()), //! ) //! .apply() //! .unwrap(); diff --git a/tests/recursive_logging.rs b/tests/recursive_logging.rs index 3cdf47e..9627b18 100644 --- a/tests/recursive_logging.rs +++ b/tests/recursive_logging.rs @@ -40,9 +40,11 @@ fn test_meta_logging_in_format_works() { }; Logger::new() - .dispatch(Dispatch::new().append(append::Stdout::new(layout("out")))) - .dispatch(Dispatch::new().append(append::Stderr::new(layout("err")))) - .dispatch(Dispatch::new().append(append::RollingFile::new(layout("file"), writer))) + .dispatch(Dispatch::new().append(append::Stdout::default().with_encoder(layout("out")))) + .dispatch(Dispatch::new().append(append::Stderr::default().with_encoder(layout("err")))) + .dispatch( + Dispatch::new().append(append::RollingFile::new(writer).with_encoder(layout("file"))), + ) .apply() .unwrap(); From 7e1f50578c1fa952689527524d651155b30cd2f1 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 15 Oct 2024 12:26:54 +0800 Subject: [PATCH 8/9] try avoid conflict Signed-off-by: tison --- src/append/rolling_file/append.rs | 3 ++- src/append/stdio.rs | 5 +++-- src/encoder/mod.rs | 16 +++++++++++++--- src/layout/mod.rs | 22 +++++++++++++++++++++- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/append/rolling_file/append.rs b/src/append/rolling_file/append.rs index 90f7afc..f160e1e 100644 --- a/src/append/rolling_file/append.rs +++ b/src/append/rolling_file/append.rs @@ -16,6 +16,7 @@ use log::Record; use crate::append::rolling_file::non_blocking::NonBlocking; use crate::append::Append; +use crate::encoder::IntoEncoder; use crate::layout::TextLayout; use crate::Encoder; @@ -39,7 +40,7 @@ impl RollingFile { } /// Sets the encoder used to format log records as bytes. - pub fn with_encoder(mut self, encoder: impl Into) -> Self { + pub fn with_encoder(mut self, encoder: impl IntoEncoder) -> Self { self.encoder = encoder.into(); self } diff --git a/src/append/stdio.rs b/src/append/stdio.rs index 3c80f24..bf61a25 100644 --- a/src/append/stdio.rs +++ b/src/append/stdio.rs @@ -15,6 +15,7 @@ use std::io::Write; use crate::append::Append; +use crate::encoder::IntoEncoder; use crate::layout::TextLayout; use crate::Encoder; @@ -34,7 +35,7 @@ impl Default for Stdout { impl Stdout { /// Creates a new `Stdout` appender with the given encoder. - pub fn with_encoder(mut self, encoder: impl Into) -> Self { + pub fn with_encoder(mut self, encoder: impl IntoEncoder) -> Self { self.encoder = encoder.into(); self } @@ -69,7 +70,7 @@ impl Default for Stderr { impl Stderr { /// Creates a new `Stderr` appender with the given encoder. - pub fn with_encoder(mut self, encoder: impl Into) -> Self { + pub fn with_encoder(mut self, encoder: impl IntoEncoder) -> Self { self.encoder = encoder.into(); self } diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 261b545..a6229a8 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -39,9 +39,19 @@ impl Encoder { } } -impl> From for Encoder { - fn from(layout: L) -> Self { - let layout = layout.into(); +pub trait IntoEncoder { + fn into(self) -> Encoder; +} + +impl> IntoEncoder for L { + fn into(self) -> Encoder { + self.into() + } +} + +impl> IntoEncoder for L { + fn into(self) -> Encoder { + let layout = self.into(); Encoder::Custom(CustomEncoder::new(move |record| { Ok(layout.format(record)?.into_bytes()) })) diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 66d1d64..b86808c 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -14,6 +14,7 @@ //! Describe how to format a log record. +use crate::Encoder; pub use custom::CustomLayout; #[cfg(feature = "json")] pub use json::JsonLayout; @@ -22,7 +23,6 @@ pub use kv::KvDisplay; pub use text::LevelColor; pub use text::TextLayout; - mod custom; #[cfg(feature = "json")] mod json; @@ -48,3 +48,23 @@ impl Layout { } } } + +pub trait IntoLayout { + fn into(self) -> Layout; +} + +impl> IntoLayout for L { + fn into(self) -> Layout { + self.into() + } +} + +impl> IntoLayout for L { + fn into(self) -> Layout { + let encoder = self.into(); + Layout::Custom(CustomLayout::new(move |record| { + let bytes = encoder.format(record)?; + Ok(String::from_utf8_lossy(&bytes).to_string()) + })) + } +} From 98bed57f3ef155aa21849ff00ce44a255d6365d7 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 15 Oct 2024 12:49:31 +0800 Subject: [PATCH 9/9] merge encoder into layout Signed-off-by: tison --- examples/fn_layout_filter.rs | 4 +- examples/json_stdio.rs | 4 +- examples/rolling_file.rs | 4 +- src/append/opentelemetry.rs | 6 +-- src/append/rolling_file/append.rs | 15 +++--- src/append/stdio.rs | 27 +++++----- src/encoder/custom.rs | 67 ----------------------- src/encoder/json.rs | 36 ------------- src/encoder/mod.rs | 59 -------------------- src/format/json.rs | 89 ------------------------------- src/format/mod.rs | 18 ------- src/layout/custom.rs | 15 +++--- src/layout/json.rs | 73 +++++++++++++++++++++++-- src/layout/mod.rs | 23 +------- src/layout/text.rs | 7 +-- src/lib.rs | 5 -- tests/recursive_logging.rs | 8 +-- 17 files changed, 115 insertions(+), 345 deletions(-) delete mode 100644 src/encoder/custom.rs delete mode 100644 src/encoder/json.rs delete mode 100644 src/encoder/mod.rs delete mode 100644 src/format/json.rs delete mode 100644 src/format/mod.rs diff --git a/examples/fn_layout_filter.rs b/examples/fn_layout_filter.rs index 187f860..06cd51a 100644 --- a/examples/fn_layout_filter.rs +++ b/examples/fn_layout_filter.rs @@ -32,8 +32,8 @@ fn main() { } })) .append( - append::Stdout::default().with_encoder(CustomLayout::new(|record| { - Ok(format!("[system alert] {}", record.args())) + append::Stdout::default().with_layout(CustomLayout::new(|record| { + Ok(format!("[system alert] {}", record.args()).into_bytes()) })), ), ) diff --git a/examples/json_stdio.rs b/examples/json_stdio.rs index c33516c..216f429 100644 --- a/examples/json_stdio.rs +++ b/examples/json_stdio.rs @@ -14,7 +14,7 @@ use log::LevelFilter; use logforth::append; -use logforth::encoder::JsonEncoder; +use logforth::layout::JsonLayout; use logforth::Dispatch; use logforth::Logger; @@ -23,7 +23,7 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .append(append::Stdout::default().with_encoder(JsonEncoder::default())), + .append(append::Stdout::default().with_layout(JsonLayout::default())), ) .apply() .unwrap(); diff --git a/examples/rolling_file.rs b/examples/rolling_file.rs index 5514dc6..a78b35f 100644 --- a/examples/rolling_file.rs +++ b/examples/rolling_file.rs @@ -18,7 +18,7 @@ use logforth::append::rolling_file::RollingFile; use logforth::append::rolling_file::RollingFileWriter; use logforth::append::rolling_file::Rotation; use logforth::append::Stdout; -use logforth::encoder::JsonEncoder; +use logforth::layout::JsonLayout; use logforth::Dispatch; use logforth::Logger; @@ -37,7 +37,7 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .append(RollingFile::new(writer).with_encoder(JsonEncoder::default())) + .append(RollingFile::new(writer).with_layout(JsonLayout::default())) .append(Stdout::default()), ) .apply() diff --git a/src/append/opentelemetry.rs b/src/append/opentelemetry.rs index 98e29ff..85ecb10 100644 --- a/src/append/opentelemetry.rs +++ b/src/append/opentelemetry.rs @@ -176,10 +176,10 @@ impl Append for OpentelemetryLog { log_record_.severity_number = Some(log_level_to_otel_severity(record.level())); log_record_.severity_text = Some(record.level().as_str()); log_record_.target = Some(record.target().to_string().into()); - log_record_.body = Some(AnyValue::from(match self.layout.as_ref() { - None => record.args().to_string(), + log_record_.body = Some(AnyValue::Bytes(Box::new(match self.layout.as_ref() { + None => record.args().to_string().into_bytes(), Some(layout) => layout.format(record)?, - })); + }))); if let Some(module_path) = record.module_path() { log_record_.add_attribute("module_path", module_path.to_string()); diff --git a/src/append/rolling_file/append.rs b/src/append/rolling_file/append.rs index f160e1e..27c763e 100644 --- a/src/append/rolling_file/append.rs +++ b/src/append/rolling_file/append.rs @@ -16,15 +16,14 @@ use log::Record; use crate::append::rolling_file::non_blocking::NonBlocking; use crate::append::Append; -use crate::encoder::IntoEncoder; use crate::layout::TextLayout; -use crate::Encoder; +use crate::Layout; /// An appender that writes log records to a file that rolls over when it reaches a certain date /// time. #[derive(Debug)] pub struct RollingFile { - encoder: Encoder, + layout: Layout, writer: NonBlocking, } @@ -34,21 +33,21 @@ impl RollingFile { /// This appender by default uses [`TextLayout`] to format log records as bytes. pub fn new(writer: NonBlocking) -> Self { Self { - encoder: TextLayout::default().no_color().into(), + layout: TextLayout::default().no_color().into(), writer, } } - /// Sets the encoder used to format log records as bytes. - pub fn with_encoder(mut self, encoder: impl IntoEncoder) -> Self { - self.encoder = encoder.into(); + /// Sets the layout used to format log records as bytes. + pub fn with_layout(mut self, layout: impl Into) -> Self { + self.layout = layout.into(); self } } impl Append for RollingFile { fn append(&self, record: &Record) -> anyhow::Result<()> { - let mut bytes = self.encoder.format(record)?; + let mut bytes = self.layout.format(record)?; bytes.push(b'\n'); self.writer.send(bytes)?; Ok(()) diff --git a/src/append/stdio.rs b/src/append/stdio.rs index bf61a25..79474d4 100644 --- a/src/append/stdio.rs +++ b/src/append/stdio.rs @@ -15,35 +15,34 @@ use std::io::Write; use crate::append::Append; -use crate::encoder::IntoEncoder; use crate::layout::TextLayout; -use crate::Encoder; +use crate::Layout; /// An appender that prints log records to stdout. #[derive(Debug)] pub struct Stdout { - encoder: Encoder, + layout: Layout, } impl Default for Stdout { fn default() -> Self { Self { - encoder: TextLayout::default().into(), + layout: TextLayout::default().into(), } } } impl Stdout { - /// Creates a new `Stdout` appender with the given encoder. - pub fn with_encoder(mut self, encoder: impl IntoEncoder) -> Self { - self.encoder = encoder.into(); + /// Creates a new `Stdout` appender with the given layout. + pub fn with_layout(mut self, layout: impl Into) -> Self { + self.layout = layout.into(); self } } impl Append for Stdout { fn append(&self, record: &log::Record) -> anyhow::Result<()> { - let mut bytes = self.encoder.format(record)?; + let mut bytes = self.layout.format(record)?; bytes.push(b'\n'); std::io::stdout().write_all(&bytes)?; Ok(()) @@ -57,28 +56,28 @@ impl Append for Stdout { /// An appender that prints log records to stderr. #[derive(Debug)] pub struct Stderr { - encoder: Encoder, + layout: Layout, } impl Default for Stderr { fn default() -> Self { Self { - encoder: TextLayout::default().into(), + layout: TextLayout::default().into(), } } } impl Stderr { - /// Creates a new `Stderr` appender with the given encoder. - pub fn with_encoder(mut self, encoder: impl IntoEncoder) -> Self { - self.encoder = encoder.into(); + /// Creates a new `Stderr` appender with the given layout. + pub fn with_layout(mut self, encoder: impl Into) -> Self { + self.layout = encoder.into(); self } } impl Append for Stderr { fn append(&self, record: &log::Record) -> anyhow::Result<()> { - let mut bytes = self.encoder.format(record)?; + let mut bytes = self.layout.format(record)?; bytes.push(b'\n'); std::io::stderr().write_all(&bytes)?; Ok(()) diff --git a/src/encoder/custom.rs b/src/encoder/custom.rs deleted file mode 100644 index 9c77ba6..0000000 --- a/src/encoder/custom.rs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2024 FastLabs Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fmt::Debug; -use std::fmt::Formatter; - -use log::Record; - -use crate::Encoder; - -// TODO(tisonkun): use trait alias when it's stable - https://github.com/rust-lang/rust/issues/41517 -// then we can use the alias for both `dyn` and `impl`. -type FormatFunction = dyn Fn(&Record) -> anyhow::Result> + Send + Sync + 'static; - -/// An encoder that you can pass the custom encoder function. -/// -/// The custom encoder function accepts [`&log::Record`][Record] and formats it into [`Vec`]. -/// For example: -/// -/// ```rust -/// use log::Record; -/// use logforth::encoder::CustomEncoder; -/// -/// let layout = CustomEncoder::new(|record: &Record| { -/// Ok(format!("{} - {}", record.level(), record.args()).into_bytes()) -/// }); -/// ``` -pub struct CustomEncoder { - f: Box, -} - -impl Debug for CustomEncoder { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - write!(f, "CustomEncoder {{ ... }}") - } -} - -impl CustomEncoder { - pub fn new( - layout: impl Fn(&Record) -> anyhow::Result> + Send + Sync + 'static, - ) -> Self { - CustomEncoder { - f: Box::new(layout), - } - } - - pub(crate) fn format(&self, record: &Record) -> anyhow::Result> { - (self.f)(record) - } -} - -impl From for Encoder { - fn from(encoder: CustomEncoder) -> Self { - Encoder::Custom(encoder) - } -} diff --git a/src/encoder/json.rs b/src/encoder/json.rs deleted file mode 100644 index 1d122fe..0000000 --- a/src/encoder/json.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2024 FastLabs Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use jiff::tz::TimeZone; -use log::Record; - -use crate::Encoder; - -#[derive(Default, Debug, Clone)] -pub struct JsonEncoder { - pub tz: Option, -} - -impl JsonEncoder { - pub(crate) fn format(&self, record: &Record) -> anyhow::Result> { - let record_line = crate::format::json::do_format(record, self.tz.clone())?; - Ok(serde_json::to_vec(&record_line)?) - } -} - -impl From for Encoder { - fn from(encoder: JsonEncoder) -> Self { - Encoder::Json(encoder) - } -} diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs deleted file mode 100644 index a6229a8..0000000 --- a/src/encoder/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2024 FastLabs Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -mod custom; -pub use custom::CustomEncoder; - -#[cfg(feature = "json")] -mod json; -#[cfg(feature = "json")] -pub use json::JsonEncoder; - -use crate::Layout; - -#[derive(Debug)] -pub enum Encoder { - Custom(CustomEncoder), - #[cfg(feature = "json")] - Json(JsonEncoder), -} - -impl Encoder { - pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result> { - match self { - Encoder::Custom(encoder) => encoder.format(record), - #[cfg(feature = "json")] - Encoder::Json(encoder) => encoder.format(record), - } - } -} - -pub trait IntoEncoder { - fn into(self) -> Encoder; -} - -impl> IntoEncoder for L { - fn into(self) -> Encoder { - self.into() - } -} - -impl> IntoEncoder for L { - fn into(self) -> Encoder { - let layout = self.into(); - Encoder::Custom(CustomEncoder::new(move |record| { - Ok(layout.format(record)?.into_bytes()) - })) - } -} diff --git a/src/format/json.rs b/src/format/json.rs deleted file mode 100644 index 3b83d77..0000000 --- a/src/format/json.rs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2024 FastLabs Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fmt::Arguments; - -use jiff::tz::TimeZone; -use jiff::Timestamp; -use jiff::Zoned; -use log::Record; -use serde::Serialize; -use serde_json::Map; -use serde_json::Value; - -struct KvCollector<'a> { - kvs: &'a mut Map, -} - -impl<'a, 'kvs> log::kv::Visitor<'kvs> for KvCollector<'a> { - fn visit_pair( - &mut self, - key: log::kv::Key<'kvs>, - value: log::kv::Value<'kvs>, - ) -> Result<(), log::kv::Error> { - let k = key.to_string(); - let v = value.to_string(); - self.kvs.insert(k, v.into()); - Ok(()) - } -} - -#[derive(Debug, Clone, Serialize)] -pub(crate) struct RecordLine<'a> { - #[serde(serialize_with = "serialize_time_zone")] - timestamp: Zoned, - level: &'a str, - module_path: &'a str, - file: &'a str, - line: u32, - #[serde(serialize_with = "serialize_args")] - message: &'a Arguments<'a>, - kvs: Map, -} - -fn serialize_time_zone(timestamp: &Zoned, serializer: S) -> Result -where - S: serde::Serializer, -{ - serializer.collect_str(&format_args!("{timestamp:.6}")) -} - -fn serialize_args(args: &Arguments, serializer: S) -> Result -where - S: serde::Serializer, -{ - serializer.collect_str(args) -} - -pub(crate) fn do_format<'a>( - record: &'a Record<'a>, - tz: Option, -) -> anyhow::Result> { - let mut kvs = Map::new(); - let mut visitor = KvCollector { kvs: &mut kvs }; - record.key_values().visit(&mut visitor)?; - - Ok(RecordLine { - timestamp: match tz { - Some(tz) => Timestamp::now().to_zoned(tz), - None => Zoned::now(), - }, - level: record.level().as_str(), - module_path: record.module_path().unwrap_or_default(), - file: record.file().unwrap_or_default(), - line: record.line().unwrap_or_default(), - message: record.args(), - kvs, - }) -} diff --git a/src/format/mod.rs b/src/format/mod.rs deleted file mode 100644 index 34a0fa9..0000000 --- a/src/format/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2024 FastLabs Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Shared logics between `encoder` and `layout`. - -#[cfg(feature = "json")] -pub(crate) mod json; diff --git a/src/layout/custom.rs b/src/layout/custom.rs index acf9449..7591211 100644 --- a/src/layout/custom.rs +++ b/src/layout/custom.rs @@ -21,19 +21,20 @@ use crate::layout::Layout; // TODO(tisonkun): use trait alias when it's stable - https://github.com/rust-lang/rust/issues/41517 // then we can use the alias for both `dyn` and `impl`. -type FormatFunction = dyn Fn(&Record) -> anyhow::Result + Send + Sync + 'static; +type FormatFunction = dyn Fn(&Record) -> anyhow::Result> + Send + Sync + 'static; /// A layout that you can pass the custom layout function. /// -/// The custom layout function accepts [`&log::Record`][Record] and formats it into [`String`]. +/// The custom layout function accepts [`&log::Record`][Record] and formats it into [`Vec`]. /// For example: /// /// ```rust /// use log::Record; /// use logforth::layout::CustomLayout; /// -/// let layout = -/// CustomLayout::new(|record: &Record| Ok(format!("{} - {}", record.level(), record.args()))); +/// let layout = CustomLayout::new(|record: &Record| { +/// Ok(format!("{} - {}", record.level(), record.args()).into_bytes()) +/// }); /// ``` pub struct CustomLayout { f: Box, @@ -46,13 +47,15 @@ impl Debug for CustomLayout { } impl CustomLayout { - pub fn new(layout: impl Fn(&Record) -> anyhow::Result + Send + Sync + 'static) -> Self { + pub fn new( + layout: impl Fn(&Record) -> anyhow::Result> + Send + Sync + 'static, + ) -> Self { CustomLayout { f: Box::new(layout), } } - pub(crate) fn format(&self, record: &Record) -> anyhow::Result { + pub(crate) fn format(&self, record: &Record) -> anyhow::Result> { (self.f)(record) } } diff --git a/src/layout/json.rs b/src/layout/json.rs index fd3df93..345fe00 100644 --- a/src/layout/json.rs +++ b/src/layout/json.rs @@ -12,8 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Arguments; + use jiff::tz::TimeZone; +use jiff::Timestamp; +use jiff::Zoned; use log::Record; +use serde::Serialize; +use serde_json::Map; +use serde_json::Value; use crate::layout::Layout; @@ -36,10 +43,70 @@ pub struct JsonLayout { pub tz: Option, } +struct KvCollector<'a> { + kvs: &'a mut Map, +} + +impl<'a, 'kvs> log::kv::Visitor<'kvs> for KvCollector<'a> { + fn visit_pair( + &mut self, + key: log::kv::Key<'kvs>, + value: log::kv::Value<'kvs>, + ) -> Result<(), log::kv::Error> { + let k = key.to_string(); + let v = value.to_string(); + self.kvs.insert(k, v.into()); + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct RecordLine<'a> { + #[serde(serialize_with = "serialize_time_zone")] + timestamp: Zoned, + level: &'a str, + module_path: &'a str, + file: &'a str, + line: u32, + #[serde(serialize_with = "serialize_args")] + message: &'a Arguments<'a>, + kvs: Map, +} + +fn serialize_time_zone(timestamp: &Zoned, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.collect_str(&format_args!("{timestamp:.6}")) +} + +fn serialize_args(args: &Arguments, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.collect_str(args) +} + impl JsonLayout { - pub(crate) fn format(&self, record: &Record) -> anyhow::Result { - let record_line = crate::format::json::do_format(record, self.tz.clone())?; - Ok(serde_json::to_string(&record_line)?) + pub(crate) fn format(&self, record: &Record) -> anyhow::Result> { + let mut kvs = Map::new(); + let mut visitor = KvCollector { kvs: &mut kvs }; + record.key_values().visit(&mut visitor)?; + + let record_line = RecordLine { + timestamp: match self.tz.clone() { + Some(tz) => Timestamp::now().to_zoned(tz), + None => Zoned::now(), + }, + level: record.level().as_str(), + module_path: record.module_path().unwrap_or_default(), + file: record.file().unwrap_or_default(), + line: record.line().unwrap_or_default(), + message: record.args(), + kvs, + }; + + Ok(serde_json::to_vec(&record_line)?) } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index b86808c..2ba0697 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -14,7 +14,6 @@ //! Describe how to format a log record. -use crate::Encoder; pub use custom::CustomLayout; #[cfg(feature = "json")] pub use json::JsonLayout; @@ -39,7 +38,7 @@ pub enum Layout { } impl Layout { - pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result { + pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result> { match self { Layout::Custom(layout) => layout.format(record), Layout::Text(layout) => layout.format(record), @@ -48,23 +47,3 @@ impl Layout { } } } - -pub trait IntoLayout { - fn into(self) -> Layout; -} - -impl> IntoLayout for L { - fn into(self) -> Layout { - self.into() - } -} - -impl> IntoLayout for L { - fn into(self) -> Layout { - let encoder = self.into(); - Layout::Custom(CustomLayout::new(move |record| { - let bytes = encoder.format(record)?; - Ok(String::from_utf8_lossy(&bytes).to_string()) - })) - } -} diff --git a/src/layout/text.rs b/src/layout/text.rs index 0a9a229..49e9736 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -131,7 +131,7 @@ impl Default for LevelColor { } impl TextLayout { - pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result { + pub(crate) fn format(&self, record: &log::Record) -> anyhow::Result> { let time = match self.tz.clone() { Some(tz) => Timestamp::now().to_zoned(tz), None => Zoned::now(), @@ -153,10 +153,7 @@ impl TextLayout { let line = record.line().unwrap_or_default(); let message = record.args(); let kvs = KvDisplay::new(record.key_values()); - - Ok(format!( - "{time:.6} {level:>5} {module}: {file}:{line} {message}{kvs}" - )) + Ok(format!("{time:.6} {level:>5} {module}: {file}:{line} {message}{kvs}").into_bytes()) } } diff --git a/src/lib.rs b/src/lib.rs index b86b9f8..6171d69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,16 +55,11 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod append; -pub mod encoder; pub mod filter; pub mod layout; mod logger; -// utilities -pub(crate) mod format; - pub use append::Append; -pub use encoder::Encoder; pub use filter::Filter; pub use layout::Layout; pub use logger::Dispatch; diff --git a/tests/recursive_logging.rs b/tests/recursive_logging.rs index 9627b18..771d9d3 100644 --- a/tests/recursive_logging.rs +++ b/tests/recursive_logging.rs @@ -35,15 +35,15 @@ fn test_meta_logging_in_format_works() { let layout = |src: &'static str| { layout::CustomLayout::new(move |record| { - Ok(format!("{src} [{}] {}", record.level(), record.args())) + Ok(format!("{src} [{}] {}", record.level(), record.args()).into_bytes()) }) }; Logger::new() - .dispatch(Dispatch::new().append(append::Stdout::default().with_encoder(layout("out")))) - .dispatch(Dispatch::new().append(append::Stderr::default().with_encoder(layout("err")))) + .dispatch(Dispatch::new().append(append::Stdout::default().with_layout(layout("out")))) + .dispatch(Dispatch::new().append(append::Stderr::default().with_layout(layout("err")))) .dispatch( - Dispatch::new().append(append::RollingFile::new(writer).with_encoder(layout("file"))), + Dispatch::new().append(append::RollingFile::new(writer).with_layout(layout("file"))), ) .apply() .unwrap();