diff --git a/README.md b/README.md index b8d2a5b..aeb78f5 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::default()), ) .apply() .unwrap(); diff --git a/examples/env_filter.rs b/examples/env_filter.rs index 7aa8458..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; @@ -23,8 +22,7 @@ fn main() { .dispatch( Dispatch::new() .filter(EnvFilter::from_default_env()) - .layout(TextLayout::default()) - .append(append::Stdout), + .append(append::Stdout::default()), ) .apply() .unwrap(); diff --git a/examples/fn_layout_filter.rs b/examples/fn_layout_filter.rs index bfd64f7..06cd51a 100644 --- a/examples/fn_layout_filter.rs +++ b/examples/fn_layout_filter.rs @@ -31,10 +31,11 @@ fn main() { FilterResult::Reject } })) - .layout(CustomLayout::new(|record, f| { - f(format_args!("[system alert] {}", record.args())) - })) - .append(append::Stdout), + .append( + append::Stdout::default().with_layout(CustomLayout::new(|record| { + Ok(format!("[system alert] {}", record.args()).into_bytes()) + })), + ), ) .apply() .unwrap(); diff --git a/examples/json_stdio.rs b/examples/json_stdio.rs index 2420ed5..216f429 100644 --- a/examples/json_stdio.rs +++ b/examples/json_stdio.rs @@ -23,8 +23,7 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .layout(JsonLayout::default()) - .append(append::Stdout), + .append(append::Stdout::default().with_layout(JsonLayout::default())), ) .apply() .unwrap(); diff --git a/examples/rolling_file.rs b/examples/rolling_file.rs index 3571f77..a78b35f 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::layout::JsonLayout; -use logforth::layout::TextLayout; use logforth::Dispatch; use logforth::Logger; @@ -38,10 +37,9 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .layout(JsonLayout::default()) - .append(RollingFile::new(writer)), + .append(RollingFile::new(writer).with_layout(JsonLayout::default())) + .append(Stdout::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..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; @@ -23,14 +22,8 @@ fn main() { .dispatch( Dispatch::new() .filter(LevelFilter::Trace) - .layout(TextLayout::default()) - .append(append::Stdout), - ) - .dispatch( - Dispatch::new() - .filter(LevelFilter::Trace) - .layout(TextLayout::default().no_color()) - .append(append::Stderr), + .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/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..85ecb10 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,30 +161,34 @@ impl OpentelemetryLogBuilder { #[derive(Debug)] pub struct OpentelemetryLog { name: String, + layout: Option, library: Arc, provider: LoggerProvider, } 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())); - - if let Some(module_path) = log_record.module_path() { - record.add_attribute("module_path", module_path.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::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()); } - 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 +208,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..27c763e 100644 --- a/src/append/rolling_file/append.rs +++ b/src/append/rolling_file/append.rs @@ -16,23 +16,39 @@ use log::Record; use crate::append::rolling_file::non_blocking::NonBlocking; use crate::append::Append; +use crate::layout::TextLayout; +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 { + layout: Layout, writer: NonBlocking, } impl RollingFile { + /// 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 { writer } + Self { + layout: TextLayout::default().no_color().into(), + writer, + } + } + + /// 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 bytes = format!("{}\n", record.args()).into_bytes(); + 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 5c780eb..79474d4 100644 --- a/src/append/stdio.rs +++ b/src/append/stdio.rs @@ -15,14 +15,35 @@ use std::io::Write; use crate::append::Append; +use crate::layout::TextLayout; +use crate::Layout; /// An appender that prints log records to stdout. -#[derive(Default, Debug)] -pub struct Stdout; +#[derive(Debug)] +pub struct Stdout { + layout: Layout, +} + +impl Default for Stdout { + fn default() -> Self { + Self { + layout: TextLayout::default().into(), + } + } +} + +impl Stdout { + /// 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 bytes = format!("{}\n", record.args()).into_bytes(); + let mut bytes = self.layout.format(record)?; + bytes.push(b'\n'); std::io::stdout().write_all(&bytes)?; Ok(()) } @@ -33,12 +54,31 @@ impl Append for Stdout { } /// An appender that prints log records to stderr. -#[derive(Default, Debug)] -pub struct Stderr; +#[derive(Debug)] +pub struct Stderr { + layout: Layout, +} + +impl Default for Stderr { + fn default() -> Self { + Self { + layout: TextLayout::default().into(), + } + } +} + +impl Stderr { + /// 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 bytes = format!("{}\n", record.args()).into_bytes(); + let mut bytes = self.layout.format(record)?; + bytes.push(b'\n'); std::io::stderr().write_all(&bytes)?; Ok(()) } diff --git a/src/layout/custom.rs b/src/layout/custom.rs index 0235904..7591211 100644 --- a/src/layout/custom.rs +++ b/src/layout/custom.rs @@ -12,7 +12,6 @@ // 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; @@ -22,27 +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, &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. /// -/// 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 [`Vec`]. +/// For example: /// /// ```rust -/// use std::fmt::Arguments; -/// /// 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()).into_bytes()) +/// }); /// ``` pub struct CustomLayout { f: Box, @@ -56,21 +48,15 @@ impl Debug for CustomLayout { impl CustomLayout { pub fn new( - layout: impl Fn(&Record, &dyn Fn(Arguments) -> anyhow::Result<()>) -> anyhow::Result<()> - + Send - + Sync - + 'static, + 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) } } diff --git a/src/layout/identical.rs b/src/layout/identical.rs deleted file mode 100644 index 18efecc..0000000 --- a/src/layout/identical.rs +++ /dev/null @@ -1,39 +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 crate::layout::Layout; - -/// 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; - -impl IdenticalLayout { - pub(crate) fn format(&self, record: &log::Record, f: &F) -> anyhow::Result<()> - where - F: Fn(Arguments) -> anyhow::Result<()>, - { - f(*record.args()) - } -} - -impl From for Layout { - fn from(layout: IdenticalLayout) -> Self { - Layout::Identical(layout) - } -} diff --git a/src/layout/json.rs b/src/layout/json.rs index ee3ed14..345fe00 100644 --- a/src/layout/json.rs +++ b/src/layout/json.rs @@ -61,7 +61,7 @@ impl<'a, 'kvs> log::kv::Visitor<'kvs> for KvCollector<'a> { } #[derive(Debug, Clone, Serialize)] -struct RecordLine<'a> { +pub(crate) struct RecordLine<'a> { #[serde(serialize_with = "serialize_time_zone")] timestamp: Zoned, level: &'a str, @@ -88,10 +88,7 @@ where } impl JsonLayout { - pub(crate) fn format(&self, record: &Record, f: &F) -> anyhow::Result<()> - where - F: Fn(Arguments) -> anyhow::Result<()>, - { + 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)?; @@ -109,8 +106,7 @@ impl JsonLayout { kvs, }; - let text = serde_json::to_string(&record_line)?; - f(format_args!("{text}")) + Ok(serde_json::to_vec(&record_line)?) } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 5ac9a21..2ba0697 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..49e9736 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; @@ -132,10 +131,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(), @@ -157,10 +153,7 @@ impl TextLayout { let line = record.line().unwrap_or_default(); let message = record.args(); let kvs = KvDisplay::new(record.key_values()); - - f(format_args!( - "{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 51400c9..6171d69 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::default()), //! ) //! .apply() //! .unwrap(); 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..771d9d3 100644 --- a/tests/recursive_logging.rs +++ b/tests/recursive_logging.rs @@ -34,18 +34,16 @@ 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()).into_bytes()) }) }; Logger::new() - .dispatch(Dispatch::new().layout(layout("out")).append(append::Stdout)) - .dispatch(Dispatch::new().layout(layout("err")).append(append::Stderr)) + .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() - .layout(layout("file")) - .append(append::RollingFile::new(writer)), + Dispatch::new().append(append::RollingFile::new(writer).with_layout(layout("file"))), ) .apply() .unwrap();