From 9f4e0dafbc09cf20917b1571cbd0d9ebef3dd278 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 6 Jan 2026 02:04:57 +0800 Subject: [PATCH 1/4] refactor!: rework core api Signed-off-by: tison --- Cargo.toml | 1 - appenders/async/Cargo.toml | 1 - appenders/async/src/append.rs | 14 +++- appenders/async/src/lib.rs | 36 ++++++++- appenders/async/src/state.rs | 6 +- appenders/async/src/worker.rs | 77 ++++++++++++++++++- appenders/fastrace/src/lib.rs | 6 +- appenders/file/tests/global_file_limit.rs | 4 +- appenders/journald/src/field.rs | 8 ++ appenders/opentelemetry/src/lib.rs | 2 +- bridges/log/src/lib.rs | 6 +- core/src/kv.rs | 12 +-- core/src/logger/builder.rs | 4 +- core/src/record.rs | 90 +++++------------------ core/src/str.rs | 74 ------------------- layouts/google-cloud-logging/src/lib.rs | 2 +- layouts/json/src/lib.rs | 2 +- layouts/logfmt/src/lib.rs | 10 ++- 18 files changed, 180 insertions(+), 175 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 085642a..26708b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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/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..47e3cdb 100644 --- a/appenders/async/src/append.rs +++ b/appenders/async/src/append.rs @@ -22,8 +22,10 @@ use logforth_core::record::Record; use logforth_core::trap::BestEffortTrap; use crate::Overflow; +use crate::Sender; use crate::Task; use crate::state::AsyncState; +use crate::worker::RecordOwned; use crate::worker::Worker; /// A composable appender, logging and flushing asynchronously. @@ -64,7 +66,7 @@ impl Append for Async { } let task = Task::Log { - record: Box::new(record.to_owned()), + record: Box::new(RecordOwned::from_record(record)), diags: diagnostics, }; self.state.send_task(task) @@ -146,8 +148,14 @@ impl AsyncBuilder { } = self; let (sender, receiver) = match buffered_lines_limit { - Some(limit) => crossbeam_channel::bounded(limit), - None => crossbeam_channel::unbounded(), + Some(limit) => { + let (tx, rx) = std::sync::mpsc::sync_channel(limit); + (Sender::Bounded(tx), rx) + } + None => { + let (tx, rx) = std::sync::mpsc::channel(); + (Sender::Unbounded(tx), rx) + } }; let worker = Worker::new(appends, receiver, trap); diff --git a/appenders/async/src/lib.rs b/appenders/async/src/lib.rs index 5d40e5d..8ae62d9 100644 --- a/appenders/async/src/lib.rs +++ b/appenders/async/src/lib.rs @@ -19,7 +19,6 @@ use logforth_core::Error; use logforth_core::kv; -use logforth_core::record::RecordOwned; mod append; mod state; @@ -30,7 +29,7 @@ pub use self::append::AsyncBuilder; enum Task { Log { - record: Box, + record: Box, diags: Vec<(kv::KeyOwned, kv::ValueOwned)>, }, Flush { @@ -45,3 +44,36 @@ enum Overflow { /// Drops the incoming operation. DropIncoming, } + +#[derive(Clone)] +enum Sender { + Unbounded(std::sync::mpsc::Sender), + Bounded(std::sync::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 { + fn send(&self, value: T) -> Result<(), std::sync::mpsc::SendError> { + match self { + Sender::Unbounded(s) => s.send(value), + Sender::Bounded(s) => s.send(value), + } + } + + fn try_send(&self, value: T) -> Result<(), std::sync::mpsc::TrySendError> { + match self { + Sender::Unbounded(s) => s + .send(value) + .map_err(|e| std::sync::mpsc::TrySendError::Disconnected(e.0)), + Sender::Bounded(s) => s.try_send(value), + } + } +} diff --git a/appenders/async/src/state.rs b/appenders/async/src/state.rs index 3e86af8..c6b9aea 100644 --- a/appenders/async/src/state.rs +++ b/appenders/async/src/state.rs @@ -16,10 +16,10 @@ use std::sync::Arc; use std::thread::JoinHandle; use arc_swap::ArcSwapOption; -use crossbeam_channel::Sender; use logforth_core::Error; use crate::Overflow; +use crate::Sender; use crate::Task; #[derive(Debug)] @@ -56,8 +56,8 @@ impl AsyncState { }), Overflow::DropIncoming => match sender.try_send(task) { Ok(()) => Ok(()), - Err(crossbeam_channel::TrySendError::Full(_)) => Ok(()), - Err(crossbeam_channel::TrySendError::Disconnected(task)) => { + Err(std::sync::mpsc::TrySendError::Full(_)) => Ok(()), + Err(std::sync::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..6d607c9 100644 --- a/appenders/async/src/worker.rs +++ b/appenders/async/src/worker.rs @@ -12,13 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crossbeam_channel::Receiver; +use std::borrow::Cow; +use std::sync::mpsc::Receiver; +use std::time::SystemTime; + use logforth_core::Append; use logforth_core::Diagnostic; use logforth_core::Error; use logforth_core::Trap; use logforth_core::kv; +use logforth_core::kv::KeyValues; use logforth_core::kv::Visitor; +use logforth_core::record::Level; +use logforth_core::record::Record; use crate::Task; @@ -56,7 +62,18 @@ impl Worker { } else { &[Box::new(OwnedDiagnostic(diags))] }; - let record = record.as_record(); + let payload = format_args!("{}", record.payload); + let record = Record::builder() + .time(record.now) + .level(record.level) + .target(record.target.as_ref()) + .module_path(record.module_path.as_deref()) + .file(record.file.as_deref()) + .line(record.line) + .column(record.column) + .payload(payload) + .key_values(KeyValues::from(record.kvs.as_slice())) + .build(); for append in appends.iter() { if let Err(err) = append.append(&record, diags) { let err = Error::new("failed to append record").with_source(err); @@ -93,3 +110,59 @@ impl Diagnostic for OwnedDiagnostic { Ok(()) } } + +#[derive(Clone, Debug)] +pub(crate) 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 { + pub fn from_record(record: &Record) -> Self { + RecordOwned { + now: record.time(), + level: record.level(), + target: if let Some(target) = record.target_static() { + Cow::Borrowed(target) + } else { + Cow::Owned(record.target().to_string()) + }, + module_path: if let Some(module_path) = record.module_path_static() { + Some(Cow::Borrowed(module_path)) + } else { + record.module_path().map(|s| Cow::Owned(s.to_string())) + }, + file: if let Some(file) = record.file_static() { + Some(Cow::Borrowed(file)) + } else { + record.file().map(|s| Cow::Owned(s.to_string())) + }, + line: record.line(), + column: record.column(), + payload: if let Some(payload) = record.payload_static() { + Cow::Borrowed(payload) + } else { + Cow::Owned(record.payload().to_string()) + }, + kvs: record + .key_values() + .iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect(), + } + } +} 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/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..48d0ad5 100644 --- a/core/src/record.rs +++ b/core/src/record.rs @@ -20,9 +20,7 @@ use std::str::FromStr; 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 +38,7 @@ pub struct Record<'a> { column: Option, // the payload - payload: OwnedStr, + payload: fmt::Arguments<'a>, // structural logging kvs: KeyValues<'a>, @@ -120,13 +118,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 +132,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,7 +143,7 @@ impl<'a> Record<'a> { file: self.file, line: self.line, column: self.column, - payload: self.payload.clone(), + payload: self.payload, kvs: self.kvs.clone(), }, } @@ -193,7 +172,7 @@ impl Default for RecordBuilder<'_> { file: None, line: None, column: None, - payload: OwnedStr::Static(""), + payload: format_args!(""), kvs: Default::default(), }, } @@ -201,12 +180,15 @@ impl Default for RecordBuilder<'_> { } impl<'a> RecordBuilder<'a> { + /// Set [`time`](Record::time). + pub fn time(mut self, now: SystemTime) -> Self { + self.record.now = now; + self + } + /// 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 +240,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(); @@ -340,44 +328,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 { From 46a1c3c7268e84a02d1e2e925162b97723d704c0 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 6 Jan 2026 02:25:13 +0800 Subject: [PATCH 2/4] bump MSRV Signed-off-by: tison --- CHANGELOG.md | 1 + Cargo.toml | 2 +- README.md | 6 +++--- appenders/file/src/rolling.rs | 20 ++++++++++---------- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ffdd9..0ebd964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. * `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. +* Bump minimum supported Rust version (MSRV) to 1.89.0. Mainly for [allowing storing `format_args!()` in a variable](https://github.com/rust-lang/rust/pull/140748). ## [0.29.1] 2025-11-03 diff --git a/Cargo.toml b/Cargo.toml index 26708b6..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 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/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() From f54a483e8505139095adb3b5b2d18817d7bccf8d Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 6 Jan 2026 08:39:53 +0800 Subject: [PATCH 3/4] revert a bit Signed-off-by: tison --- CHANGELOG.md | 2 +- appenders/async/src/append.rs | 17 ++----- appenders/async/src/channel.rs | 63 ++++++++++++++++++++++++ appenders/async/src/lib.rs | 37 ++------------ appenders/async/src/state.rs | 15 +++--- appenders/async/src/worker.rs | 89 ++++------------------------------ core/src/record.rs | 79 +++++++++++++++++++++++++++--- 7 files changed, 159 insertions(+), 143 deletions(-) create mode 100644 appenders/async/src/channel.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ebd964..ae5a85e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file. * `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. -* Bump minimum supported Rust version (MSRV) to 1.89.0. Mainly for [allowing storing `format_args!()` in a variable](https://github.com/rust-lang/rust/pull/140748). +* Bump minimum supported Rust version (MSRV) to 1.89.0. ## [0.29.1] 2025-11-03 diff --git a/appenders/async/src/append.rs b/appenders/async/src/append.rs index 47e3cdb..dad1dd7 100644 --- a/appenders/async/src/append.rs +++ b/appenders/async/src/append.rs @@ -22,10 +22,9 @@ use logforth_core::record::Record; use logforth_core::trap::BestEffortTrap; use crate::Overflow; -use crate::Sender; use crate::Task; +use crate::channel::channel; use crate::state::AsyncState; -use crate::worker::RecordOwned; use crate::worker::Worker; /// A composable appender, logging and flushing asynchronously. @@ -66,7 +65,7 @@ impl Append for Async { } let task = Task::Log { - record: Box::new(RecordOwned::from_record(record)), + record: Box::new(record.to_owned()), diags: diagnostics, }; self.state.send_task(task) @@ -147,17 +146,7 @@ impl AsyncBuilder { overflow, } = self; - let (sender, receiver) = match buffered_lines_limit { - Some(limit) => { - let (tx, rx) = std::sync::mpsc::sync_channel(limit); - (Sender::Bounded(tx), rx) - } - None => { - let (tx, rx) = std::sync::mpsc::channel(); - (Sender::Unbounded(tx), rx) - } - }; - + 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 8ae62d9..f237cb7 100644 --- a/appenders/async/src/lib.rs +++ b/appenders/async/src/lib.rs @@ -19,8 +19,10 @@ use logforth_core::Error; use logforth_core::kv; +use logforth_core::record::RecordOwned; mod append; +mod channel; mod state; mod worker; @@ -29,7 +31,7 @@ pub use self::append::AsyncBuilder; enum Task { Log { - record: Box, + record: Box, diags: Vec<(kv::KeyOwned, kv::ValueOwned)>, }, Flush { @@ -44,36 +46,3 @@ enum Overflow { /// Drops the incoming operation. DropIncoming, } - -#[derive(Clone)] -enum Sender { - Unbounded(std::sync::mpsc::Sender), - Bounded(std::sync::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 { - fn send(&self, value: T) -> Result<(), std::sync::mpsc::SendError> { - match self { - Sender::Unbounded(s) => s.send(value), - Sender::Bounded(s) => s.send(value), - } - } - - fn try_send(&self, value: T) -> Result<(), std::sync::mpsc::TrySendError> { - match self { - Sender::Unbounded(s) => s - .send(value) - .map_err(|e| std::sync::mpsc::TrySendError::Disconnected(e.0)), - Sender::Bounded(s) => s.try_send(value), - } - } -} diff --git a/appenders/async/src/state.rs b/appenders/async/src/state.rs index c6b9aea..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 logforth_core::Error; use crate::Overflow; -use crate::Sender; 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(std::sync::mpsc::TrySendError::Full(_)) => Ok(()), - Err(std::sync::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", - })) - } + 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 6d607c9..96be16e 100644 --- a/appenders/async/src/worker.rs +++ b/appenders/async/src/worker.rs @@ -12,21 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::borrow::Cow; -use std::sync::mpsc::Receiver; -use std::time::SystemTime; - use logforth_core::Append; use logforth_core::Diagnostic; use logforth_core::Error; use logforth_core::Trap; use logforth_core::kv; -use logforth_core::kv::KeyValues; use logforth_core::kv::Visitor; -use logforth_core::record::Level; -use logforth_core::record::Record; use crate::Task; +use crate::channel::Receiver; pub(crate) struct Worker { appends: Vec>, @@ -62,24 +56,15 @@ impl Worker { } else { &[Box::new(OwnedDiagnostic(diags))] }; - let payload = format_args!("{}", record.payload); - let record = Record::builder() - .time(record.now) - .level(record.level) - .target(record.target.as_ref()) - .module_path(record.module_path.as_deref()) - .file(record.file.as_deref()) - .line(record.line) - .column(record.column) - .payload(payload) - .key_values(KeyValues::from(record.kvs.as_slice())) - .build(); - 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; @@ -110,59 +95,3 @@ impl Diagnostic for OwnedDiagnostic { Ok(()) } } - -#[derive(Clone, Debug)] -pub(crate) 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 { - pub fn from_record(record: &Record) -> Self { - RecordOwned { - now: record.time(), - level: record.level(), - target: if let Some(target) = record.target_static() { - Cow::Borrowed(target) - } else { - Cow::Owned(record.target().to_string()) - }, - module_path: if let Some(module_path) = record.module_path_static() { - Some(Cow::Borrowed(module_path)) - } else { - record.module_path().map(|s| Cow::Owned(s.to_string())) - }, - file: if let Some(file) = record.file_static() { - Some(Cow::Borrowed(file)) - } else { - record.file().map(|s| Cow::Owned(s.to_string())) - }, - line: record.line(), - column: record.column(), - payload: if let Some(payload) = record.payload_static() { - Cow::Borrowed(payload) - } else { - Cow::Owned(record.payload().to_string()) - }, - kvs: record - .key_values() - .iter() - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect(), - } - } -} diff --git a/core/src/record.rs b/core/src/record.rs index 48d0ad5..6aabce5 100644 --- a/core/src/record.rs +++ b/core/src/record.rs @@ -20,6 +20,7 @@ use std::str::FromStr; use std::time::SystemTime; use crate::Error; +use crate::kv; use crate::kv::KeyValues; use crate::str::RefStr; @@ -149,6 +150,29 @@ impl<'a> Record<'a> { } } + /// 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() @@ -180,12 +204,6 @@ impl Default for RecordBuilder<'_> { } impl<'a> RecordBuilder<'a> { - /// Set [`time`](Record::time). - pub fn time(mut self, now: SystemTime) -> Self { - self.record.now = now; - self - } - /// Set [`payload`](Record::payload). pub fn payload(mut self, payload: fmt::Arguments<'a>) -> Self { self.record.payload = payload; @@ -258,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> { From b42d0eea04efc4ed43820c05e3ff04d3b542e2bb Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 6 Jan 2026 08:55:32 +0800 Subject: [PATCH 4/4] changelog Signed-off-by: tison --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae5a85e..6080e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +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. -* Bump minimum supported Rust version (MSRV) to 1.89.0. +* `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