diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ffdd9..6080e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,11 @@ All notable changes to this project will be documented in this file. ### Breaking changes +* Bump minimum supported Rust version (MSRV) to 1.89.0. * `Append` has no more `exit` method. Users should compose `logforth::core::default_logger().flush()` with their own graceful shutdown logic. * `Async` appender's `flush` method is now blocking until all buffered logs are flushed by worker threads. Any errors during flushing will be propagated back to the `flush` caller. +* `Record::payload` is now `std::fmt::Arguments` instead of `Cow<'static, str>`. +* `RecordOwned::as_record` has been removed; use `RecordOwned::with` instead. (This is a limitation of Rust as described [here](https://github.com/rust-lang/rust/issues/92698#issuecomment-3311144848).) ## [0.29.1] 2025-11-03 diff --git a/Cargo.toml b/Cargo.toml index 085642a..bfbb9df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ homepage = "https://github.com/fast/logforth" license = "Apache-2.0" readme = "README.md" repository = "https://github.com/fast/logforth" -rust-version = "1.85.0" +rust-version = "1.89.0" [workspace.dependencies] # Workspace dependencies @@ -57,7 +57,6 @@ anyhow = { version = "1.0" } arc-swap = { version = "1.7.1" } clap = { version = "4.5.49", features = ["derive"] } colored = { version = "3.0" } -crossbeam-channel = { version = "0.5.15" } fastrace = { version = "0.7" } fasyslog = { version = "1.0.0" } insta = { version = "1.43.2" } diff --git a/README.md b/README.md index 1fb2978..2bc3596 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ [![Crates.io][crates-badge]][crates-url] [![Documentation][docs-badge]][docs-url] -[![MSRV 1.80][msrv-badge]](https://www.whatrustisit.com) +[![MSRV 1.89][msrv-badge]](https://www.whatrustisit.com) [![Apache 2.0 licensed][license-badge]][license-url] [![Build Status][actions-badge]][actions-url] [crates-badge]: https://img.shields.io/crates/v/logforth.svg [crates-url]: https://crates.io/crates/logforth [docs-badge]: https://docs.rs/logforth/badge.svg -[msrv-badge]: https://img.shields.io/badge/MSRV-1.80-green?logo=rust +[msrv-badge]: https://img.shields.io/badge/MSRV-1.89-green?logo=rust [docs-url]: https://docs.rs/logforth [license-badge]: https://img.shields.io/crates/l/logforth [license-url]: LICENSE @@ -220,7 +220,7 @@ Components are organized into several crates: ## Minimum Rust version policy -This crate is built against the latest stable release, and its minimum supported rustc version is 1.85.0. +This crate is built against the latest stable release, and its minimum supported rustc version is 1.89.0. The policy is that the minimum Rust version required to use this crate can be increased in minor version updates. For example, if Logforth 1.0 requires Rust 1.60.0, then Logforth 1.0.z for all values of z will also require Rust 1.60.0 or newer. However, Logforth 1.y for y > 0 may require a newer minimum version of Rust. diff --git a/appenders/async/Cargo.toml b/appenders/async/Cargo.toml index cc298f3..a5b85fc 100644 --- a/appenders/async/Cargo.toml +++ b/appenders/async/Cargo.toml @@ -33,7 +33,6 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] arc-swap = { workspace = true } -crossbeam-channel = { workspace = true } logforth-core = { workspace = true } oneshot = { workspace = true } diff --git a/appenders/async/src/append.rs b/appenders/async/src/append.rs index f8c8d39..dad1dd7 100644 --- a/appenders/async/src/append.rs +++ b/appenders/async/src/append.rs @@ -23,6 +23,7 @@ use logforth_core::trap::BestEffortTrap; use crate::Overflow; use crate::Task; +use crate::channel::channel; use crate::state::AsyncState; use crate::worker::Worker; @@ -145,11 +146,7 @@ impl AsyncBuilder { overflow, } = self; - let (sender, receiver) = match buffered_lines_limit { - Some(limit) => crossbeam_channel::bounded(limit), - None => crossbeam_channel::unbounded(), - }; - + let (sender, receiver) = channel(buffered_lines_limit); let worker = Worker::new(appends, receiver, trap); let thread_handle = std::thread::Builder::new() .name(thread_name) diff --git a/appenders/async/src/channel.rs b/appenders/async/src/channel.rs new file mode 100644 index 0000000..b6e12de --- /dev/null +++ b/appenders/async/src/channel.rs @@ -0,0 +1,63 @@ +// 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::sync::mpsc; + +pub(crate) fn channel(bound: Option) -> (Sender, Receiver) { + match bound { + Some(bound) => { + let (tx, rx) = mpsc::sync_channel(bound); + (Sender::Bounded(tx), rx) + } + None => { + let (tx, rx) = mpsc::channel(); + (Sender::Unbounded(tx), rx) + } + } +} + +pub(crate) type Receiver = mpsc::Receiver; + +#[derive(Clone)] +pub(crate) enum Sender { + Unbounded(mpsc::Sender), + Bounded(mpsc::SyncSender), +} + +impl std::fmt::Debug for Sender { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Sender::Unbounded(tx) => tx.fmt(f), + Sender::Bounded(tx) => tx.fmt(f), + } + } +} + +impl Sender { + pub(crate) fn send(&self, value: T) -> Result<(), mpsc::SendError> { + match self { + Sender::Unbounded(s) => s.send(value), + Sender::Bounded(s) => s.send(value), + } + } + + pub(crate) fn try_send(&self, value: T) -> Result<(), mpsc::TrySendError> { + match self { + Sender::Unbounded(s) => s + .send(value) + .map_err(|e| mpsc::TrySendError::Disconnected(e.0)), + Sender::Bounded(s) => s.try_send(value), + } + } +} diff --git a/appenders/async/src/lib.rs b/appenders/async/src/lib.rs index 5d40e5d..f237cb7 100644 --- a/appenders/async/src/lib.rs +++ b/appenders/async/src/lib.rs @@ -22,6 +22,7 @@ use logforth_core::kv; use logforth_core::record::RecordOwned; mod append; +mod channel; mod state; mod worker; diff --git a/appenders/async/src/state.rs b/appenders/async/src/state.rs index 3e86af8..a609212 100644 --- a/appenders/async/src/state.rs +++ b/appenders/async/src/state.rs @@ -13,14 +13,15 @@ // limitations under the License. use std::sync::Arc; +use std::sync::mpsc; use std::thread::JoinHandle; use arc_swap::ArcSwapOption; -use crossbeam_channel::Sender; use logforth_core::Error; use crate::Overflow; use crate::Task; +use crate::channel::Sender; #[derive(Debug)] pub(crate) struct AsyncState(ArcSwapOption); @@ -56,13 +57,11 @@ impl AsyncState { }), Overflow::DropIncoming => match sender.try_send(task) { Ok(()) => Ok(()), - Err(crossbeam_channel::TrySendError::Full(_)) => Ok(()), - Err(crossbeam_channel::TrySendError::Disconnected(task)) => { - Err(Error::new(match task { - Task::Log { .. } => "failed to send log task to async appender", - Task::Flush { .. } => "failed to send flush task to async appender", - })) - } + Err(mpsc::TrySendError::Full(_)) => Ok(()), + Err(mpsc::TrySendError::Disconnected(task)) => Err(Error::new(match task { + Task::Log { .. } => "failed to send log task to async appender", + Task::Flush { .. } => "failed to send flush task to async appender", + })), }, } } diff --git a/appenders/async/src/worker.rs b/appenders/async/src/worker.rs index 294695e..96be16e 100644 --- a/appenders/async/src/worker.rs +++ b/appenders/async/src/worker.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crossbeam_channel::Receiver; use logforth_core::Append; use logforth_core::Diagnostic; use logforth_core::Error; @@ -21,6 +20,7 @@ use logforth_core::kv; use logforth_core::kv::Visitor; use crate::Task; +use crate::channel::Receiver; pub(crate) struct Worker { appends: Vec>, @@ -56,13 +56,15 @@ impl Worker { } else { &[Box::new(OwnedDiagnostic(diags))] }; - let record = record.as_record(); - for append in appends.iter() { - if let Err(err) = append.append(&record, diags) { - let err = Error::new("failed to append record").with_source(err); - trap.trap(&err); + + record.with(|record| { + for append in appends.iter() { + if let Err(err) = append.append(&record, diags) { + let err = Error::new("failed to append record").with_source(err); + trap.trap(&err); + } } - } + }); } Task::Flush { done } => { let mut error = None; diff --git a/appenders/fastrace/src/lib.rs b/appenders/fastrace/src/lib.rs index a5a7dcc..b02bf27 100644 --- a/appenders/fastrace/src/lib.rs +++ b/appenders/fastrace/src/lib.rs @@ -49,7 +49,11 @@ pub struct FastraceEvent {} impl Append for FastraceEvent { fn append(&self, record: &Record, diags: &[Box]) -> Result<(), Error> { - let message = record.payload().to_owned(); + let message = if let Some(msg) = record.payload_static() { + Cow::Borrowed(msg) + } else { + Cow::Owned(record.payload().to_string()) + }; let mut collector = KvCollector { kv: Vec::new() }; record.key_values().visit(&mut collector)?; diff --git a/appenders/file/src/rolling.rs b/appenders/file/src/rolling.rs index 82035da..6607def 100644 --- a/appenders/file/src/rolling.rs +++ b/appenders/file/src/rolling.rs @@ -273,11 +273,11 @@ impl State { } else { state.current_filesize = last.metadata.len() as usize; - if let Ok(mtime) = last.metadata.modified() { - if let Ok(mtime) = Zoned::try_from(mtime) { - state.next_date_timestamp = state.rotation.next_date_timestamp(&mtime); - state.this_date_timestamp = mtime; - } + if let Ok(mtime) = last.metadata.modified() + && let Ok(mtime) = Zoned::try_from(mtime) + { + state.next_date_timestamp = state.rotation.next_date_timestamp(&mtime); + state.this_date_timestamp = mtime; } // continue to use the existing current log file @@ -448,11 +448,11 @@ impl State { .with_source(err) })?; - if let Some(max_files) = self.max_files { - if let Err(err) = self.delete_oldest_logs(max_files.get()) { - let err = Error::new("failed to delete oldest logs").with_source(err); - self.trap.trap(&err); - } + if let Some(max_files) = self.max_files + && let Err(err) = self.delete_oldest_logs(max_files.get()) + { + let err = Error::new("failed to delete oldest logs").with_source(err); + self.trap.trap(&err); } self.create_log_writer() diff --git a/appenders/file/tests/global_file_limit.rs b/appenders/file/tests/global_file_limit.rs index ee0e193..dbafda7 100644 --- a/appenders/file/tests/global_file_limit.rs +++ b/appenders/file/tests/global_file_limit.rs @@ -45,7 +45,7 @@ fn test_global_file_count_limit() { writer .append( &Record::builder() - .payload(format!("Log entry {}: {}\n", i, "A".repeat(50))) + .payload(format_args!("Log entry {}: {}\n", i, "A".repeat(50))) .build(), &[], ) @@ -156,7 +156,7 @@ fn create_logs(dir: &Path, max_files: usize, max_size: usize, filename: &str, co writer .append( &Record::builder() - .payload(format!( + .payload(format_args!( "Prefix {}, Log {}: {}\n", filename, i, diff --git a/appenders/journald/src/field.rs b/appenders/journald/src/field.rs index 9a3dfa0..b786fd9 100644 --- a/appenders/journald/src/field.rs +++ b/appenders/journald/src/field.rs @@ -82,6 +82,14 @@ impl PutAsFieldValue for &str { } } +impl PutAsFieldValue for std::fmt::Arguments<'_> { + fn put_field_value(self, buffer: &mut Vec) { + // SAFETY: no more than an allocate-less version + // buffer.extend_from_slice(format!("{}", self)) + write!(buffer, "{self}").unwrap(); + } +} + impl PutAsFieldValue for Value<'_> { fn put_field_value(self, buffer: &mut Vec) { // SAFETY: no more than an allocate-less version diff --git a/appenders/opentelemetry/src/lib.rs b/appenders/opentelemetry/src/lib.rs index d9c0f19..25d2c45 100644 --- a/appenders/opentelemetry/src/lib.rs +++ b/appenders/opentelemetry/src/lib.rs @@ -245,7 +245,7 @@ impl Append for OpentelemetryLog { } else if let Some(payload) = record.payload_static() { log_record.set_body(AnyValue::from(payload)); } else { - log_record.set_body(AnyValue::from(record.payload().to_owned())); + log_record.set_body(AnyValue::from(record.payload().to_string())); } if let Some(module_path) = record.module_path_static() { diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index 75bd7a0..1ae4992 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -132,11 +132,7 @@ fn forward_log(logger: &Logger, record: &Record) { }; // payload - builder = if let Some(payload) = record.args().as_str() { - builder.payload(payload) - } else { - builder.payload(record.args().to_string()) - }; + builder = builder.payload(*record.args()); // key-values let mut kvs = Vec::new(); diff --git a/core/src/kv.rs b/core/src/kv.rs index 98f5fee..83879bb 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -24,7 +24,6 @@ use value_bag::OwnedValueBag; use value_bag::ValueBag; use crate::Error; -use crate::str::OwnedStr; use crate::str::RefStr; /// A visitor to walk through key-value pairs. @@ -63,7 +62,7 @@ impl<'a> Key<'a> { /// Convert to an owned key. pub fn to_owned(&self) -> KeyOwned { - KeyOwned(self.0.into_owned()) + KeyOwned(self.0.into_cow_static()) } /// Convert to a `Cow` str. @@ -82,12 +81,15 @@ pub type ValueOwned = OwnedValueBag; /// An owned key in a key-value pair. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct KeyOwned(OwnedStr); +pub struct KeyOwned(Cow<'static, str>); impl KeyOwned { /// Create a `Key` ref. pub fn by_ref(&self) -> Key<'_> { - Key(self.0.by_ref()) + Key(match &self.0 { + Cow::Borrowed(s) => RefStr::Static(s), + Cow::Owned(s) => RefStr::Borrowed(s), + }) } } @@ -143,7 +145,7 @@ impl<'a> KeyValues<'a> { } }), KeyValuesState::Owned(p) => p.iter().find_map(|(k, v)| { - if k.0.get() != key { + if k.0.as_ref() != key { None } else { Some(v.by_ref()) diff --git a/core/src/logger/builder.rs b/core/src/logger/builder.rs index e1635e2..99b1ca4 100644 --- a/core/src/logger/builder.rs +++ b/core/src/logger/builder.rs @@ -80,7 +80,9 @@ impl LoggerBuilder { /// use logforth_core::record::Record; /// /// let l = logforth_core::builder().build(); - /// let r = Record::builder().payload("hello world!").build(); + /// let r = Record::builder() + /// .payload(format_args!("hello world!")) + /// .build(); /// l.log(&r); /// ``` pub fn build(self) -> Logger { diff --git a/core/src/record.rs b/core/src/record.rs index ba5cd74..6aabce5 100644 --- a/core/src/record.rs +++ b/core/src/record.rs @@ -22,7 +22,6 @@ use std::time::SystemTime; use crate::Error; use crate::kv; use crate::kv::KeyValues; -use crate::str::OwnedStr; use crate::str::RefStr; /// The payload of a log message. @@ -40,7 +39,7 @@ pub struct Record<'a> { column: Option, // the payload - payload: OwnedStr, + payload: fmt::Arguments<'a>, // structural logging kvs: KeyValues<'a>, @@ -120,13 +119,13 @@ impl<'a> Record<'a> { } /// The message body. - pub fn payload(&self) -> &str { - self.payload.get() + pub fn payload(&self) -> fmt::Arguments<'a> { + self.payload } /// The message body, if it is a `'static` str. pub fn payload_static(&self) -> Option<&'static str> { - self.payload.get_static() + self.payload.as_str() } /// The key-values. @@ -134,25 +133,6 @@ impl<'a> Record<'a> { &self.kvs } - /// Convert to an owned record. - pub fn to_owned(&self) -> RecordOwned { - RecordOwned { - now: self.now, - level: self.level, - target: self.target.into_owned(), - module_path: self.module_path.map(RefStr::into_owned), - file: self.file.map(RefStr::into_owned), - line: self.line, - column: self.column, - payload: self.payload.clone(), - kvs: self - .kvs - .iter() - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect(), - } - } - /// Create a builder initialized with the current record's values. pub fn to_builder(&self) -> RecordBuilder<'a> { RecordBuilder { @@ -164,12 +144,35 @@ impl<'a> Record<'a> { file: self.file, line: self.line, column: self.column, - payload: self.payload.clone(), + payload: self.payload, kvs: self.kvs.clone(), }, } } + /// Convert to an owned record. + pub fn to_owned(&self) -> RecordOwned { + RecordOwned { + now: self.now, + level: self.level, + target: self.target.into_cow_static(), + module_path: self.module_path.map(|m| m.into_cow_static()), + file: self.file.map(|f| f.into_cow_static()), + line: self.line, + column: self.column, + payload: if let Some(s) = self.payload.as_str() { + Cow::Borrowed(s) + } else { + Cow::Owned(self.payload.to_string()) + }, + kvs: self + .kvs + .iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect(), + } + } + /// Returns a new builder. pub fn builder() -> RecordBuilder<'a> { RecordBuilder::default() @@ -193,7 +196,7 @@ impl Default for RecordBuilder<'_> { file: None, line: None, column: None, - payload: OwnedStr::Static(""), + payload: format_args!(""), kvs: Default::default(), }, } @@ -202,11 +205,8 @@ impl Default for RecordBuilder<'_> { impl<'a> RecordBuilder<'a> { /// Set [`payload`](Record::payload). - pub fn payload(mut self, payload: impl Into>) -> Self { - self.record.payload = match payload.into() { - Cow::Borrowed(s) => OwnedStr::Static(s), - Cow::Owned(s) => OwnedStr::Owned(s.into_boxed_str()), - }; + pub fn payload(mut self, payload: fmt::Arguments<'a>) -> Self { + self.record.payload = payload; self } @@ -258,6 +258,12 @@ impl<'a> RecordBuilder<'a> { self } + /// Set [`column`](Record::column). + pub fn column(mut self, column: Option) -> Self { + self.record.column = column; + self + } + /// Set [`key_values`](struct.Record.html#method.key_values) pub fn key_values(mut self, kvs: impl Into>) -> Self { self.record.kvs = kvs.into(); @@ -270,6 +276,55 @@ impl<'a> RecordBuilder<'a> { } } +/// Owned version of a log record. +#[derive(Clone, Debug)] +pub struct RecordOwned { + // the observed time + now: SystemTime, + + // the metadata + level: Level, + target: Cow<'static, str>, + module_path: Option>, + file: Option>, + line: Option, + column: Option, + + // the payload + payload: Cow<'static, str>, + + // structural logging + kvs: Vec<(kv::KeyOwned, kv::ValueOwned)>, +} + +impl RecordOwned { + /// Execute the given function with the `Record`. + pub fn with(&self, f: impl FnOnce(Record<'_>)) { + f(Record { + now: self.now, + level: self.level, + target: match &self.target { + Cow::Borrowed(s) => RefStr::Static(s), + Cow::Owned(s) => RefStr::Borrowed(s.as_ref()), + }, + module_path: match &self.module_path { + Some(Cow::Borrowed(s)) => Some(RefStr::Static(s)), + Some(Cow::Owned(s)) => Some(RefStr::Borrowed(s)), + None => None, + }, + file: match &self.file { + Some(Cow::Borrowed(s)) => Some(RefStr::Static(s)), + Some(Cow::Owned(s)) => Some(RefStr::Borrowed(s)), + None => None, + }, + line: self.line, + column: self.column, + payload: format_args!("{}", self.payload), + kvs: KeyValues::from(self.kvs.as_slice()), + }); + } +} + /// A minimal set of criteria for pre-filtering purposes. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct FilterCriteria<'a> { @@ -340,44 +395,6 @@ impl<'a> FilterCriteriaBuilder<'a> { } } -/// Owned version of a log record. -#[derive(Clone, Debug)] -pub struct RecordOwned { - // the observed time - now: SystemTime, - - // the metadata - level: Level, - target: OwnedStr, - module_path: Option, - file: Option, - line: Option, - column: Option, - - // the payload - payload: OwnedStr, - - // structural logging - kvs: Vec<(kv::KeyOwned, kv::ValueOwned)>, -} - -impl RecordOwned { - /// Create a `Record` referencing the data in this `RecordOwned`. - pub fn as_record(&self) -> Record<'_> { - Record { - now: self.now, - level: self.level, - target: self.target.by_ref(), - module_path: self.module_path.as_ref().map(OwnedStr::by_ref), - file: self.file.as_ref().map(OwnedStr::by_ref), - line: self.line, - column: self.column, - payload: self.payload.clone(), - kvs: KeyValues::from(self.kvs.as_slice()), - } - } -} - /// A Level is the importance or severity of a log event. /// /// The higher the level, the more important or severe the event. diff --git a/core/src/str.rs b/core/src/str.rs index 6ada0e8..7174fda 100644 --- a/core/src/str.rs +++ b/core/src/str.rs @@ -56,13 +56,6 @@ impl<'a> RefStr<'a> { RefStr::Static(s) => Cow::Borrowed(s), } } - - pub fn into_owned(self) -> OwnedStr { - match self { - RefStr::Borrowed(s) => OwnedStr::Owned(Box::from(s)), - RefStr::Static(s) => OwnedStr::Static(s), - } - } } impl PartialEq for RefStr<'_> { @@ -90,70 +83,3 @@ impl Hash for RefStr<'_> { Hash::hash(self.get(), state) } } - -#[derive(Clone)] -pub enum OwnedStr { - Owned(Box), - Static(&'static str), -} - -impl fmt::Debug for OwnedStr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(self.get(), f) - } -} - -impl fmt::Display for OwnedStr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(self.get(), f) - } -} - -impl OwnedStr { - pub fn get(&self) -> &str { - match self { - OwnedStr::Owned(s) => s, - OwnedStr::Static(s) => s, - } - } - - pub fn get_static(&self) -> Option<&'static str> { - match self { - OwnedStr::Owned(_) => None, - OwnedStr::Static(s) => Some(s), - } - } - - pub fn by_ref(&self) -> RefStr<'_> { - match self { - OwnedStr::Owned(s) => RefStr::Borrowed(s), - OwnedStr::Static(s) => RefStr::Static(s), - } - } -} - -impl PartialEq for OwnedStr { - fn eq(&self, other: &Self) -> bool { - PartialEq::eq(self.get(), other.get()) - } -} - -impl Eq for OwnedStr {} - -impl PartialOrd for OwnedStr { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for OwnedStr { - fn cmp(&self, other: &Self) -> Ordering { - Ord::cmp(self.get(), other.get()) - } -} - -impl Hash for OwnedStr { - fn hash(&self, state: &mut H) { - Hash::hash(self.get(), state) - } -} diff --git a/layouts/google-cloud-logging/src/lib.rs b/layouts/google-cloud-logging/src/lib.rs index f43f670..65983ed 100644 --- a/layouts/google-cloud-logging/src/lib.rs +++ b/layouts/google-cloud-logging/src/lib.rs @@ -183,7 +183,7 @@ struct RecordLine<'a> { extra_fields: BTreeMap, severity: &'a str, timestamp: jiff::Timestamp, - message: &'a str, + message: std::fmt::Arguments<'a>, #[serde(skip_serializing_if = "BTreeMap::is_empty")] #[serde(rename = "logging.googleapis.com/labels")] labels: BTreeMap, diff --git a/layouts/json/src/lib.rs b/layouts/json/src/lib.rs index 5fed307..2346aac 100644 --- a/layouts/json/src/lib.rs +++ b/layouts/json/src/lib.rs @@ -133,7 +133,7 @@ struct RecordLine<'a> { target: &'a str, file: &'a str, line: u32, - message: &'a str, + message: std::fmt::Arguments<'a>, #[serde(skip_serializing_if = "Map::is_empty")] kvs: Map, #[serde(skip_serializing_if = "Map::is_empty")] diff --git a/layouts/logfmt/src/lib.rs b/layouts/logfmt/src/lib.rs index e2c61df..eda4239 100644 --- a/layouts/logfmt/src/lib.rs +++ b/layouts/logfmt/src/lib.rs @@ -19,6 +19,8 @@ pub extern crate jiff; +use std::borrow::Cow; + use jiff::Timestamp; use jiff::tz::TimeZone; use logforth_core::Diagnostic; @@ -112,7 +114,11 @@ impl Layout for LogfmtLayout { let target = record.target(); let file = record.filename(); let line = record.line().unwrap_or_default(); - let message = record.payload(); + let message = if let Some(msg) = record.payload_static() { + Cow::Borrowed(msg) + } else { + Cow::Owned(record.payload().to_string()) + }; let mut visitor = KvFormatter { text: format!("timestamp={time:.6}"), @@ -124,7 +130,7 @@ impl Layout for LogfmtLayout { Key::new("position"), Value::from_display(&format_args!("{file}:{line}")), )?; - visitor.visit(Key::new("message"), Value::from_str(message))?; + visitor.visit(Key::new("message"), Value::from_str(message.as_ref()))?; record.key_values().visit(&mut visitor)?; for d in diags {