From a81648cadb09795a6c95d7f72860bbde22e80c73 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 21 Sep 2026 20:05:32 +0800 Subject: [PATCH 1/2] feat: add native Rust logging facade --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 5 + README.md | 159 +++++++----- bridges/log/src/lib.rs | 419 +------------------------------ core/src/filter/mod.rs | 7 +- core/src/kv.rs | 15 ++ core/src/kv/convert.rs | 81 ++++++ core/src/kv/serialize.rs | 476 ++++++++++++++++++++++++++++++++++++ core/src/lib.rs | 2 + core/src/logger/log_impl.rs | 64 ++++- core/src/macros.rs | 140 +++++++++++ core/tests/serde.rs | 128 ++++++++++ examples/Cargo.toml | 6 + examples/src/native.rs | 71 ++++++ logforth/Cargo.toml | 4 + logforth/src/lib.rs | 79 +++--- logforth/tests/native.rs | 382 +++++++++++++++++++++++++++++ 17 files changed, 1511 insertions(+), 528 deletions(-) create mode 100644 core/src/kv/convert.rs create mode 100644 core/src/kv/serialize.rs create mode 100644 core/src/macros.rs create mode 100644 core/tests/serde.rs create mode 100644 examples/src/native.rs create mode 100644 logforth/tests/native.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7627a8..ffded80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,7 @@ jobs: run: | set -x + cargo run --features="serde,bridge-log,filter-rustlog,layout-json" --example native cargo run --features="bridge-log" --example log_with_logger cargo run --features="starter-log" --example simple_stdout diff --git a/CHANGELOG.md b/CHANGELOG.md index 596655b..59c32ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. ## Unreleased +### Improvements + +* Add instance-based native logging macros with lazy typed fields, shared named logger handles, and optional direct Serde capture. +* Make native logging the primary documented API while retaining the optional `log` compatibility bridge. + ### Breaking changes * Bump minimum supported Rust version (MSRV) to 1.91.0. diff --git a/README.md b/README.md index 418bc8c..657f823 100644 --- a/README.md +++ b/README.md @@ -20,88 +20,127 @@ Logforth is a versatile, extensible, and easy-to-use logging framework for Rust ## Getting Started -Add `log` and `logforth` to your `Cargo.toml`: +Add Logforth to your application: ```shell -cargo add log -cargo add logforth -F starter-log +cargo add logforth ``` -## Simple Usage - -Set up a basic logger that outputs to stdout: +Build a logger and pass it to the native logging macros: ```rust +use logforth::{Level, LevelFilter, append}; + fn main() { - logforth::starter_log::stdout().apply(); + let logger = logforth::builder() + .dispatch(|d| d + .filter(LevelFilter::MoreSevereEqual(Level::Info)) + .append(append::Stderr::default())) + .build(); - log::error!("This is an error message."); - log::info!("This is an info message."); - log::debug!("This debug message will not be printed by default."); + logforth::info!(logger, "service started"); + logforth::debug!(logger, "this message is filtered out"); + logger.flush(); } ``` -By default, all logging except the `error` level is disabled. You can enable logging at other levels by setting the [`RUST_LOG`](https://docs.rs/logforth-filter-rustlog/*/logforth_filter_rustlog/index.html) environment variable. For example, `RUST_LOG=all cargo run` will print all logs. +The native API is always available and does not depend on the `log` crate. There is no implicit global logger or global maximum level: each logger's dispatches decide what to emit. An unfiltered dispatch accepts every level; a logger without dispatches emits nothing. -## Advanced Usage +## Events and structured fields -Configure multiple dispatches with different filters and appenders: +Use `trace!`, `debug!`, `info!`, `warn!`, `error!`, or `fatal!` for common severities, or `log!(logger, level, ...)` for any Logforth `Level`. `fatal!` records severity; it does not terminate the process or flush. ```rust -fn main() { - logforth::starter_log::builder() - .dispatch(|d| d - .filter(LevelFilter::MoreSevereEqual(Level::Error)) - .append(append::Stderr::default())) - .dispatch(|d| d - .filter(LevelFilter::MoreSevereEqual(Level::Info)) - .append(append::Stdout::default())) - .apply(); +use logforth::kv::Value; + +let logger = logforth::builder() + .dispatch(|d| d.append(logforth::append::Stderr::default())) + .build(); +let queue = String::from("background"); +let completed = 3u64; +logforth::info!(logger, { + "queue" => queue, + "completed" => completed, + "healthy" => true, + "details" => Value::debug(&[1, 2, 3]), + "description" => Value::display(&format_args!("{queue}: {completed}")), +}, "batch completed"); + +// Fields-only events have an empty message. +logforth::info!(logger, { "queue.depth" => 0 }); +``` - log::error!("This error will be logged to stderr."); - log::info!("This info will be logged to stdout."); - log::debug!("This debug message will not be logged."); -} +Keys are string expressions; values are borrowed through `kv::ToValue`, retaining scalar types. Use `Value::debug` and `Value::display` when a field should be text. The message uses standard Rust formatting, including named arguments and captures. The optional field map is one syntax, without capture modifiers or a separate enabled probe. + +Macros check level and target before evaluating messages, keys, fields, or conversions. Full-record filters may still reject an event afterward; filters requiring payload or source information must return `Neutral` during prefiltering. Formatting can happen once per consuming appender, so formatting implementations should avoid side effects. + +Enable `serde` for nested structures, arrays, and snapshots: + +```shell +cargo add logforth -F serde ``` -Configure OpenTelemetry appender to export logs to an OpenTelemetry backend ([full example](https://github.com/scopedb/percas/blob/d01db13b/crates/server/src/telemetry.rs#L131-L227)): +```rust +let logger = logforth::builder() + .dispatch(|d| d.append(logforth::append::Stderr::default())) + .build(); +logforth::info!(logger, { + "samples" => logforth::kv::serde(&vec![Some(1), None, Some(3)]), +}, "resource sample"); +``` + +`kv::serde` converts directly into Logforth values after prefiltering. If serialization returns an error, that field becomes an explicit `` string and the event continues. Use `ValueOwned::from_serde` when you need to handle the error yourself. This path does not use `log`, value-bag, or sval. + +## Categories, dispatches, and diagnostics + +A logger without a name uses the calling Rust module as its target. `logger.named("worker")` creates a cheap handle sharing the same dispatches and diagnostics, with `"worker"` as its target. Source module, file, line, and column always identify the actual call site. Names replace the current category; there is no implicit logger hierarchy or appender inheritance. ```rust -fn main() { - let static_diagnostic = { - let mut static_diagnostic = StaticDiagnostic::default(); - static_diagnostic.insert("node_id", node_id); - static_diagnostic.insert("nodegroup", nodegroup); - static_diagnostic - }; - - let runtime = async_runtime(); - let filter = make_rust_log_filter(&opentelemetry.filter); - let appender = runtime.block_on(async { - let exporter = opentelemetry_otlp::LogExporter::builder() - .with_tonic() - .with_endpoint(&opentelemetry.otlp_endpoint) - .with_protocol(opentelemetry_otlp::Protocol::Grpc) - .build() - .expect("failed to initialize opentelemetry logger"); - - append::opentelemetry::OpentelemetryLogBuilder::new(service_name, exporter) - .label("service.name", service_name) - .build() - }); - - logforth::starter_log::builder() - .dispatch(|b| { - b.filter(filter) - .diagnostic(FastraceDiagnostic::default()) - .diagnostic(static_diagnostic) - .append(appender) - }) - .apply(); +use logforth::append; + +let logger = logforth::builder() + .dispatch(|d| d.append(append::Stderr::default())) + .build(); +let worker = logger.named("worker"); +logforth::info!(worker, "started"); +``` + +Existing `RustLogFilter` directives such as `worker=debug` match named handles. Migrate a stable `log` target by creating a handle with the same name; ordinary module directives continue to match unnamed loggers. Bridge records retain their original targets even if the bridge was given a named logger. + +For independent policy, build a separate logger with its own dispatches. For example, a usage stream can use an unfiltered dedicated logger so that changing ordinary diagnostic logging to `off` does not discard usage events. Add multiple dispatches or appenders explicitly when an event should reach several destinations. Names alone do not isolate output or filtering. + +Existing `StaticDiagnostic`, `FastraceDiagnostic`, thread/task diagnostics, text/JSON layouts, rolling files, async appenders, and OTLP all work with native events. Diagnostics remain dispatch context; per-event data belongs in fields. The [native example](examples/src/native.rs) combines categories, JSON, static context, a dedicated stream, and dependency logs. [Appender documentation](https://docs.rs/logforth-append-opentelemetry) covers OTLP configuration. + +## Lifecycle and dependency logs + +Keep a logger handle until shutdown. Stop producers, call `logger.flush()`, and only then tear down runtimes or exporters needed by the appenders. Flushing any clone or named handle flushes the whole shared dispatch graph; it does not disable further logging. Independent loggers must each be flushed. Async appenders wait for pending work during flush; configured overflow policies still apply. This is logging, not a transactional delivery guarantee. + +The optional `log` bridge is the compatibility entry point for dependencies. Enable `bridge-log` and install it once at startup: + +```shell +cargo add log +cargo add logforth -F bridge-log +``` + +```rust +use logforth::append; +use logforth::bridge::log::LogBridge; + +fn main() -> Result<(), log::SetLoggerError> { + let logger = logforth::builder() + .dispatch(|d| d.append(append::Stderr::default())) + .build(); + log::set_boxed_logger(Box::new(LogBridge::new(logger.clone())))?; + log::set_max_level(log::LevelFilter::Trace); + + logforth::info!(logger, "native event"); + log::info!("dependency event"); + logger.flush(); + Ok(()) } ``` -Read more demos under the [examples](examples) directory. +Use `bridge-log-serde` only when incoming `log` fields require Serde support. Native Serde capture needs only `serde`. The `starter-log` helpers remain available for applications using the `log` facade. More examples are in the [examples](examples) directory. ## Features @@ -191,7 +230,7 @@ Users can also provide their own MDC by implementing the [`Diagnostic`] trait. ### Bridges -So far, Logforth provides out-of-the-box integration with the `log` crate. You can use Logforth as the backend for any crate that uses the `log` facade. +The optional `log` bridge forwards dependency logs into the same dispatch graph as native events. It preserves the original level, target, source location, and supported structured fields. ## Documentation diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index 8d704a1..05d5005 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -265,13 +265,10 @@ mod kv { #[cfg(feature = "serde")] mod kv { - use std::collections::HashMap; - use std::fmt; use std::marker::PhantomData; use logforth_core::kv::KeyOwned; use logforth_core::kv::ValueOwned; - use logforth_core::kv::ValueView; pub(super) struct KeyValues<'a> { kvs: Vec<(KeyOwned, ValueOwned)>, @@ -314,421 +311,7 @@ mod kv { } } - // this is derived from `opentelemetry-appender-log`'s serde impl: - // https://github.com/open-telemetry/opentelemetry-rust/blob/f7b0dd99/opentelemetry-appender-log/src/lib.rs#L304-L763 fn value_to_value(value: impl serde::Serialize) -> Option { - value.serialize(ValueSerializer).ok() - } - - struct ValueSerializer; - - struct ValueSerializeSeq { - value: Vec, - } - - struct ValueSerializeTuple { - value: Vec, - } - - struct ValueSerializeTupleStruct { - value: Vec, - } - - struct ValueSerializeMap { - key: Option, - value: HashMap, - } - - struct ValueSerializeStruct { - value: HashMap, - } - - struct ValueSerializeTupleVariant { - variant: &'static str, - value: Vec, - } - - struct ValueSerializeStructVariant { - variant: &'static str, - value: HashMap, - } - - #[derive(Debug)] - struct ValueError(String); - - impl fmt::Display for ValueError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } - } - - impl serde::ser::Error for ValueError { - fn custom(msg: T) -> Self - where - T: fmt::Display, - { - ValueError(msg.to_string()) - } - } - - impl std::error::Error for ValueError {} - - impl serde::Serializer for ValueSerializer { - type Ok = ValueOwned; - - type Error = ValueError; - - type SerializeSeq = ValueSerializeSeq; - - type SerializeTuple = ValueSerializeTuple; - - type SerializeTupleStruct = ValueSerializeTupleStruct; - - type SerializeTupleVariant = ValueSerializeTupleVariant; - - type SerializeMap = ValueSerializeMap; - - type SerializeStruct = ValueSerializeStruct; - - type SerializeStructVariant = ValueSerializeStructVariant; - - fn serialize_bool(self, v: bool) -> Result { - Ok(ValueOwned::bool(v)) - } - - fn serialize_i8(self, v: i8) -> Result { - self.serialize_i64(v as i64) - } - - fn serialize_i16(self, v: i16) -> Result { - self.serialize_i64(v as i64) - } - - fn serialize_i32(self, v: i32) -> Result { - self.serialize_i64(v as i64) - } - - fn serialize_i64(self, v: i64) -> Result { - Ok(ValueOwned::i64(v)) - } - - fn serialize_i128(self, v: i128) -> Result { - if let Ok(v) = v.try_into() { - self.serialize_i64(v) - } else { - self.collect_str(&v) - } - } - - fn serialize_u8(self, v: u8) -> Result { - self.serialize_u64(v as u64) - } - - fn serialize_u16(self, v: u16) -> Result { - self.serialize_u64(v as u64) - } - - fn serialize_u32(self, v: u32) -> Result { - self.serialize_u64(v as u64) - } - - fn serialize_u64(self, v: u64) -> Result { - Ok(ValueOwned::u64(v)) - } - - fn serialize_u128(self, v: u128) -> Result { - if let Ok(v) = v.try_into() { - self.serialize_u64(v) - } else { - self.collect_str(&v) - } - } - - fn serialize_f32(self, v: f32) -> Result { - self.serialize_f64(v as f64) - } - - fn serialize_f64(self, v: f64) -> Result { - Ok(ValueOwned::f64(v)) - } - - fn serialize_char(self, v: char) -> Result { - Ok(ValueOwned::char(v)) - } - - fn serialize_str(self, v: &str) -> Result { - Ok(ValueOwned::str(v.to_string())) - } - - fn serialize_bytes(self, v: &[u8]) -> Result { - Ok(ValueOwned::bytes(v.to_vec())) - } - - fn serialize_none(self) -> Result { - Ok(ValueOwned::none()) - } - - fn serialize_some( - self, - value: &T, - ) -> Result { - value.serialize(self) - } - - fn serialize_unit(self) -> Result { - Ok(ValueOwned::none()) - } - - fn serialize_unit_struct(self, name: &'static str) -> Result { - Ok(ValueOwned::str(name)) - } - - fn serialize_unit_variant( - self, - _: &'static str, - _: u32, - variant: &'static str, - ) -> Result { - Ok(ValueOwned::str(variant)) - } - - fn serialize_newtype_struct( - self, - _: &'static str, - value: &T, - ) -> Result { - value.serialize(self) - } - - fn serialize_newtype_variant( - self, - _: &'static str, - _: u32, - variant: &'static str, - value: &T, - ) -> Result { - let mut map = self.serialize_map(Some(1))?; - serde::ser::SerializeMap::serialize_entry(&mut map, variant, value)?; - serde::ser::SerializeMap::end(map) - } - - fn serialize_seq(self, _: Option) -> Result { - Ok(ValueSerializeSeq { value: vec![] }) - } - - fn serialize_tuple(self, _: usize) -> Result { - Ok(ValueSerializeTuple { value: vec![] }) - } - - fn serialize_tuple_struct( - self, - _: &'static str, - _: usize, - ) -> Result { - Ok(ValueSerializeTupleStruct { value: vec![] }) - } - - fn serialize_tuple_variant( - self, - _: &'static str, - _: u32, - variant: &'static str, - _: usize, - ) -> Result { - Ok(ValueSerializeTupleVariant { - variant, - value: vec![], - }) - } - - fn serialize_map(self, _: Option) -> Result { - Ok(ValueSerializeMap { - key: None, - value: HashMap::new(), - }) - } - - fn serialize_struct( - self, - _: &'static str, - _: usize, - ) -> Result { - Ok(ValueSerializeStruct { - value: HashMap::new(), - }) - } - - fn serialize_struct_variant( - self, - _: &'static str, - _: u32, - variant: &'static str, - _: usize, - ) -> Result { - Ok(ValueSerializeStructVariant { - variant, - value: HashMap::new(), - }) - } - } - - impl serde::ser::SerializeSeq for ValueSerializeSeq { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_element( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - self.value.push(value.serialize(ValueSerializer)?); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_vec(self.value)) - } - } - - impl serde::ser::SerializeTuple for ValueSerializeTuple { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_element( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - self.value.push(value.serialize(ValueSerializer)?); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_vec(self.value)) - } - } - - impl serde::ser::SerializeTupleStruct for ValueSerializeTupleStruct { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_field( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - self.value.push(value.serialize(ValueSerializer)?); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_vec(self.value)) - } - } - - impl serde::ser::SerializeTupleVariant for ValueSerializeTupleVariant { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_field( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - self.value.push(value.serialize(ValueSerializer)?); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_hash_map({ - let mut variant = HashMap::::new(); - variant.insert(KeyOwned::new(self.variant), ValueOwned::list(self.value)); - variant - })) - } - } - - impl serde::ser::SerializeMap for ValueSerializeMap { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_key( - &mut self, - key: &T, - ) -> Result<(), Self::Error> { - let key = match key.serialize(ValueSerializer)?.view() { - ValueView::StaticStr(s) => KeyOwned::new(s), - value => KeyOwned::new(value.to_string()), - }; - self.key = Some(key); - Ok(()) - } - - fn serialize_value( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - let key = self - .key - .take() - .ok_or_else(|| serde::ser::Error::custom("missing key"))?; - let value = value.serialize(ValueSerializer)?; - self.value.insert(key, value); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_hash_map(self.value)) - } - } - - impl serde::ser::SerializeStruct for ValueSerializeStruct { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_field( - &mut self, - key: &'static str, - value: &T, - ) -> Result<(), Self::Error> { - let key = KeyOwned::new(key); - let value = value.serialize(ValueSerializer)?; - self.value.insert(key, value); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_hash_map(self.value)) - } - } - - impl serde::ser::SerializeStructVariant for ValueSerializeStructVariant { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_field( - &mut self, - key: &'static str, - value: &T, - ) -> Result<(), Self::Error> { - let key = KeyOwned::new(key); - let value = value.serialize(ValueSerializer)?; - self.value.insert(key, value); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_hash_map({ - let mut variant = HashMap::::new(); - variant.insert( - KeyOwned::new(self.variant), - ValueOwned::from_hash_map(self.value), - ); - variant - })) - } + ValueOwned::from_serde(&value).ok() } } diff --git a/core/src/filter/mod.rs b/core/src/filter/mod.rs index 3fc4786..1ef1207 100644 --- a/core/src/filter/mod.rs +++ b/core/src/filter/mod.rs @@ -34,7 +34,12 @@ pub enum FilterResult { /// A filter that can be applied to log records. pub trait Filter: fmt::Debug + Send + Sync + 'static { - /// Whether the record is filtered by its given metadata. + /// Prefilter an event by level and target before its payload is evaluated. + /// + /// Return `Neutral` if the decision needs message, fields, or source location, + /// and inspect those in [`Self::matches`]. `Reject` must mean no matching + /// record could be accepted with these criteria. An accepted prefilter does + /// not bypass the full-record filter pass. fn enabled(&self, criteria: &FilterCriteria, diags: &[Box]) -> FilterResult; /// Whether the record is filtered. diff --git a/core/src/kv.rs b/core/src/kv.rs index 4447f2b..b679608 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -14,6 +14,11 @@ //! Key-value pairs in a log record or a diagnostic context. +mod convert; +pub use self::convert::ToValue; + +#[cfg(feature = "serde")] +mod serialize; use std::borrow::Borrow; use std::borrow::Cow; use std::collections::HashMap; @@ -21,6 +26,8 @@ use std::collections::hash_map; use std::fmt; use std::slice; +#[cfg(feature = "serde")] +pub use self::serialize::serde; use crate::Error; use crate::str::RefStr; @@ -382,6 +389,7 @@ enum ValueState<'a> { Map(&'a [(Key<'a>, Value<'a>)]), Debug(&'a dyn fmt::Debug), Display(&'a dyn fmt::Display), + Owned(&'a ValueOwned), } impl fmt::Debug for ValueState<'_> { @@ -401,6 +409,7 @@ impl fmt::Debug for ValueState<'_> { ValueState::Map(v) => v.fmt(f), ValueState::Debug(v) => fmt::Debug::fmt(v, f), ValueState::Display(v) => fmt::Display::fmt(v, f), + ValueState::Owned(v) => fmt::Debug::fmt(v, f), } } } @@ -431,11 +440,17 @@ impl Value<'_> { ValueState::Map(m) => ValueView::Map(MapValue(MapValueState::Borrowed(m))), ValueState::Debug(d) => ValueView::Debug(DebugValue(d)), ValueState::Display(d) => ValueView::Display(DisplayValue(d)), + ValueState::Owned(v) => v.view(), } } } impl<'a> Value<'a> { + /// Borrow an owned value without copying its strings or nested values. + pub fn borrowed(value: &'a ValueOwned) -> Self { + Self(ValueState::Owned(value)) + } + /// Create a value representing the absence of data. pub fn none() -> Value<'a> { Value(ValueState::None) diff --git a/core/src/kv/convert.rs b/core/src/kv/convert.rs new file mode 100644 index 0000000..770dc6b --- /dev/null +++ b/core/src/kv/convert.rs @@ -0,0 +1,81 @@ +// 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 super::Value; +use super::ValueOwned; + +/// Borrow a typed value for a structured log field. +/// +/// Native macros borrow their arguments through this trait, so strings and owned +/// values are not consumed. Implement it for domain types with a natural scalar +/// representation. Use [`Value::debug`] or [`Value::display`] for text, and +/// `ValueOwned::from_serde` (with the `serde` feature) for nested structures. +pub trait ToValue { + /// Borrow this value, preserving its supported scalar or structured type. + fn to_value(&self) -> Value<'_>; +} + +impl ToValue for &T { + fn to_value(&self) -> Value<'_> { + T::to_value(self) + } +} + +impl ToValue for Value<'_> { + fn to_value(&self) -> Value<'_> { + *self + } +} + +impl ToValue for ValueOwned { + fn to_value(&self) -> Value<'_> { + Value::borrowed(self) + } +} + +impl ToValue for str { + fn to_value(&self) -> Value<'_> { + Value::str(self) + } +} + +impl ToValue for String { + fn to_value(&self) -> Value<'_> { + Value::str(self) + } +} + +impl ToValue for Option { + fn to_value(&self) -> Value<'_> { + self.as_ref().map_or_else(Value::none, ToValue::to_value) + } +} + +macro_rules! scalar { + ($constructor:ident, $repr:ty, $($ty:ty),+ $(,)?) => { + $(impl ToValue for $ty { + fn to_value(&self) -> Value<'_> { + Value::$constructor(*self as $repr) + } + })+ + }; +} + +scalar!(bool, bool, bool); +scalar!(char, char, char); +scalar!(i64, i64, i8, i16, i32, i64, isize); +scalar!(u64, u64, u8, u16, u32, u64, usize); +scalar!(i128, i128, i128); +scalar!(u128, u128, u128); +scalar!(f64, f64, f32, f64); diff --git a/core/src/kv/serialize.rs b/core/src/kv/serialize.rs new file mode 100644 index 0000000..ea45dff --- /dev/null +++ b/core/src/kv/serialize.rs @@ -0,0 +1,476 @@ +// 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::collections::HashMap; +use std::fmt; + +use super::KeyOwned; +use super::ValueOwned; +use super::ValueView; +use crate::Error; + +/// Capture a serializable field in Logforth's native value model. +/// +/// Nested structures and scalar types are preserved. If serialization fails, +/// the field becomes a string of the form ``; the +/// rest of the event is still emitted. Serialization errors do not cause a panic +/// or recursive logging. Panics in a custom serializer are not caught. +/// For explicit error handling, use [`ValueOwned::from_serde`] instead. +/// +/// Call this inside a native macro to serialize only after metadata filtering: +/// +/// ``` +/// let logger = logforth_core::builder().build(); +/// logforth_core::info!(logger, { +/// "samples" => logforth_core::kv::serde(&[1, 2, 3]), +/// }, "resource sample"); +/// ``` +pub fn serde(value: &(impl serde::Serialize + ?Sized)) -> ValueOwned { + ValueOwned::from_serde(value) + .unwrap_or_else(|err| ValueOwned::str(format!(""))) +} + +impl ValueOwned { + /// Serialize into Logforth's native value model. + /// + /// Numbers (including 128-bit integers), booleans, strings, bytes, lists, and + /// nested maps retain their types. Map keys are converted to strings; keys + /// that produce the same string overwrite earlier entries. Enums use Serde's + /// externally tagged representation. Unit structs are represented by name. + /// Returns an error if the source's serializer fails. + /// + /// Place the conversion inside a native macro's field expression to skip it + /// when metadata filtering rejects the event. Handle failures explicitly; + /// logging does not silently discard a failed field or substitute null. + /// + /// ``` + /// # fn main() -> Result<(), logforth_core::Error> { + /// use logforth_core::kv::ValueOwned; + /// let logger = logforth_core::builder().build(); + /// logforth_core::info!(logger, { + /// "samples" => ValueOwned::from_serde(&[1, 2, 3])?, + /// }, "resource sample"); + /// # Ok(()) + /// # } + /// ``` + pub fn from_serde(value: &(impl serde::Serialize + ?Sized)) -> Result { + value + .serialize(ValueSerializer) + .map_err(|err| Error::new("failed to serialize log value").with_source(err)) + } +} + +// Derived from the serializer previously implemented in logforth-bridge-log, +// based on opentelemetry-appender-log: +// https://github.com/open-telemetry/opentelemetry-rust/blob/f7b0dd99/opentelemetry-appender-log/src/lib.rs#L304-L763 +struct ValueSerializer; + +struct ValueSerializeSeq { + value: Vec, +} + +struct ValueSerializeTuple { + value: Vec, +} + +struct ValueSerializeTupleStruct { + value: Vec, +} + +struct ValueSerializeMap { + key: Option, + value: HashMap, +} + +struct ValueSerializeStruct { + value: HashMap, +} + +struct ValueSerializeTupleVariant { + variant: &'static str, + value: Vec, +} + +struct ValueSerializeStructVariant { + variant: &'static str, + value: HashMap, +} + +#[derive(Debug)] +struct ValueError(String); + +impl fmt::Display for ValueError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl serde::ser::Error for ValueError { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + ValueError(msg.to_string()) + } +} + +impl std::error::Error for ValueError {} + +impl serde::Serializer for ValueSerializer { + type Ok = ValueOwned; + + type Error = ValueError; + + type SerializeSeq = ValueSerializeSeq; + + type SerializeTuple = ValueSerializeTuple; + + type SerializeTupleStruct = ValueSerializeTupleStruct; + + type SerializeTupleVariant = ValueSerializeTupleVariant; + + type SerializeMap = ValueSerializeMap; + + type SerializeStruct = ValueSerializeStruct; + + type SerializeStructVariant = ValueSerializeStructVariant; + + fn serialize_bool(self, v: bool) -> Result { + Ok(ValueOwned::bool(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + self.serialize_i64(v as i64) + } + + fn serialize_i16(self, v: i16) -> Result { + self.serialize_i64(v as i64) + } + + fn serialize_i32(self, v: i32) -> Result { + self.serialize_i64(v as i64) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(ValueOwned::i64(v)) + } + + fn serialize_i128(self, v: i128) -> Result { + Ok(ValueOwned::i128(v)) + } + + fn serialize_u8(self, v: u8) -> Result { + self.serialize_u64(v as u64) + } + + fn serialize_u16(self, v: u16) -> Result { + self.serialize_u64(v as u64) + } + + fn serialize_u32(self, v: u32) -> Result { + self.serialize_u64(v as u64) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(ValueOwned::u64(v)) + } + + fn serialize_u128(self, v: u128) -> Result { + Ok(ValueOwned::u128(v)) + } + + fn serialize_f32(self, v: f32) -> Result { + self.serialize_f64(v as f64) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(ValueOwned::f64(v)) + } + + fn serialize_char(self, v: char) -> Result { + Ok(ValueOwned::char(v)) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(ValueOwned::str(v.to_string())) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(ValueOwned::bytes(v.to_vec())) + } + + fn serialize_none(self) -> Result { + Ok(ValueOwned::none()) + } + + fn serialize_some( + self, + value: &T, + ) -> Result { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(ValueOwned::none()) + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + Ok(ValueOwned::str(name)) + } + + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + ) -> Result { + Ok(ValueOwned::str(variant)) + } + + fn serialize_newtype_struct( + self, + _: &'static str, + value: &T, + ) -> Result { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + value: &T, + ) -> Result { + let mut map = self.serialize_map(Some(1))?; + serde::ser::SerializeMap::serialize_entry(&mut map, variant, value)?; + serde::ser::SerializeMap::end(map) + } + + fn serialize_seq(self, _: Option) -> Result { + Ok(ValueSerializeSeq { value: vec![] }) + } + + fn serialize_tuple(self, _: usize) -> Result { + Ok(ValueSerializeTuple { value: vec![] }) + } + + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeTupleStruct { value: vec![] }) + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeTupleVariant { + variant, + value: vec![], + }) + } + + fn serialize_map(self, _: Option) -> Result { + Ok(ValueSerializeMap { + key: None, + value: HashMap::new(), + }) + } + + fn serialize_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeStruct { + value: HashMap::new(), + }) + } + + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeStructVariant { + variant, + value: HashMap::new(), + }) + } +} + +impl serde::ser::SerializeSeq for ValueSerializeSeq { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.value.push(value.serialize(ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_vec(self.value)) + } +} + +impl serde::ser::SerializeTuple for ValueSerializeTuple { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.value.push(value.serialize(ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_vec(self.value)) + } +} + +impl serde::ser::SerializeTupleStruct for ValueSerializeTupleStruct { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.value.push(value.serialize(ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_vec(self.value)) + } +} + +impl serde::ser::SerializeTupleVariant for ValueSerializeTupleVariant { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.value.push(value.serialize(ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_hash_map({ + let mut variant = HashMap::::new(); + variant.insert(KeyOwned::new(self.variant), ValueOwned::list(self.value)); + variant + })) + } +} + +impl serde::ser::SerializeMap for ValueSerializeMap { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> { + let key = match key.serialize(ValueSerializer)?.view() { + ValueView::StaticStr(s) => KeyOwned::new(s), + value => KeyOwned::new(value.to_string()), + }; + self.key = Some(key); + Ok(()) + } + + fn serialize_value( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + let key = self + .key + .take() + .ok_or_else(|| serde::ser::Error::custom("missing key"))?; + let value = value.serialize(ValueSerializer)?; + self.value.insert(key, value); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_hash_map(self.value)) + } +} + +impl serde::ser::SerializeStruct for ValueSerializeStruct { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + let key = KeyOwned::new(key); + let value = value.serialize(ValueSerializer)?; + self.value.insert(key, value); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_hash_map(self.value)) + } +} + +impl serde::ser::SerializeStructVariant for ValueSerializeStructVariant { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + let key = KeyOwned::new(key); + let value = value.serialize(ValueSerializer)?; + self.value.insert(key, value); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_hash_map({ + let mut variant = HashMap::::new(); + variant.insert( + KeyOwned::new(self.variant), + ValueOwned::from_hash_map(self.value), + ); + variant + })) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index dc5f75f..351c63a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -34,6 +34,8 @@ pub use self::trap::Trap; mod error; pub use self::error::*; +mod macros; + mod logger; pub use self::logger::*; diff --git a/core/src/logger/log_impl.rs b/core/src/logger/log_impl.rs index 9795c4d..7ed4c87 100644 --- a/core/src/logger/log_impl.rs +++ b/core/src/logger/log_impl.rs @@ -14,6 +14,7 @@ use std::io::Write; use std::panic; +use std::sync::Arc; use crate::Append; use crate::Diagnostic; @@ -23,20 +24,62 @@ use crate::filter::FilterResult; use crate::record::FilterCriteria; use crate::record::Record; -/// A logger that dispatches log records to one or more dispatcher. -#[derive(Debug)] +/// A handle to shared dispatches, filters, diagnostics, and appenders. +/// +/// Cloning or naming a logger shares the configured pipeline. The final handle +/// owns appender teardown; flushing any handle flushes the shared pipeline. +#[derive(Debug, Clone)] pub struct Logger { - dispatches: Vec, + dispatches: Arc<[Dispatch]>, + name: Option<&'static str>, } impl Logger { pub(super) fn new(dispatches: Vec) -> Self { - Self { dispatches } + Self { + dispatches: dispatches.into(), + name: None, + } } } impl Logger { - /// Determine if a log message with the specified metadata would be logged. + /// Create a named handle sharing this logger's dispatches and diagnostics. + /// + /// Native macros use this name as the record target, while source metadata + /// still identifies the actual call site. Names replace rather than append + /// to the current name. Cloning or naming a handle never rebuilds appenders. + /// + /// Use stable application categories such as `"worker"`. Event-specific data + /// belongs in fields. For an independent filtering or output policy, build + /// a separate logger instead of naming a shared handle. + /// + /// ``` + /// let logger = logforth_core::builder().build(); + /// let worker = logger.named("worker"); + /// logforth_core::info!(worker, "started"); + /// ``` + #[must_use = "use the returned logger handle to emit events with this name"] + pub fn named(&self, name: &'static str) -> Self { + Self { + dispatches: self.dispatches.clone(), + name: Some(name), + } + } + + /// The native logging category, or `None` for call-site module targets. + /// + /// Names do not rewrite records passed directly to [`Self::log`], including + /// records forwarded by a bridge. + pub fn name(&self) -> Option<&'static str> { + self.name + } + + /// Test whether any dispatch might accept an event with this level and target. + /// + /// This is a conservative prefilter. A full-record filter can still reject + /// the event, and dynamic filters can change before emission. Native macros + /// call this before evaluating the message and fields. pub fn enabled(&self, criteria: &FilterCriteria) -> bool { self.dispatches .iter() @@ -45,16 +88,21 @@ impl Logger { /// Log the [`Record`]. pub fn log(&self, record: &Record) { - for dispatch in &self.dispatches { + for dispatch in self.dispatches.iter() { for err in dispatch.log(record) { handle_log_error(record, &err); } } } - /// Flush any buffered records. + /// Flush buffered records through every configured appender. + /// + /// Stop logging producers before calling this during graceful shutdown, and + /// keep any runtimes required by appenders alive until it returns. This does + /// not disable the logger or prevent later events. Appender errors use the + /// same stderr fallback as logging errors. There is no implicit exit hook. pub fn flush(&self) { - for dispatch in &self.dispatches { + for dispatch in self.dispatches.iter() { for err in dispatch.flush() { handle_flush_error(&err); } diff --git a/core/src/macros.rs b/core/src/macros.rs new file mode 100644 index 0000000..2d8a8ae --- /dev/null +++ b/core/src/macros.rs @@ -0,0 +1,140 @@ +// 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. + +/// Emit a native log event through an explicit [`Logger`](crate::Logger). +/// +/// The logger is borrowed and evaluated once. An optional field map follows the +/// logger (and level for `log!`). Keys are string expressions; +/// values implement [`ToValue`](crate::kv::ToValue). A fields-only event has an +/// empty message. The message uses the standard Rust formatting syntax. +/// +/// Logger and level are evaluated once before metadata filtering. Message, key, +/// and value expressions are only evaluated if at least one dispatch may accept +/// the event. Full-record filters can still reject it. Formatting can happen more +/// than once when several appenders consume the same event. +/// +/// A named logger uses its name as the target; otherwise the target is the calling +/// module. Module, file, line, and column always describe the actual call site. All native levels +/// are supported, including `Level::Fatal`, which neither terminates the process nor implicitly +/// flushes the logger. +/// +/// ``` +/// use logforth_core::{builder, info, log}; +/// use logforth_core::kv::Value; +/// use logforth_core::record::Level; +/// +/// let logger = builder().build(); +/// let count = 3; +/// info!(logger, "processed {count} jobs"); +/// log!(logger.named("worker"), Level::Info2, { +/// "jobs.completed" => count, +/// "healthy" => true, +/// "details" => Value::debug(&[1, 2, 3]), +/// }, "batch complete"); +/// info!(logger, { "queue.depth" => 0 }); +/// logger.flush(); +/// ``` +#[macro_export] +macro_rules! log { + ($logger:expr, $level:expr, { $($key:expr => $value:expr),* $(,)? } $(, $($message:tt)*)?) => { + $crate::__log!($logger, $level, { $($key => $value),* }, $($($message)*)?) + }; + ($logger:expr, $level:expr, $($message:tt)+) => { + $crate::__log!($logger, $level, {}, $($message)+) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __log { + ($logger:expr, $level:expr, { $($key:expr => $value:expr),* }, $($message:tt)+) => {{ + // The match keeps temporary logger expressions alive through dispatch. + match (&$logger, $level) { + (logger, level) => { + let logger: &$crate::Logger = logger; + let target = logger.name().unwrap_or(::core::module_path!()); + let criteria = $crate::record::FilterCriteria::builder() + .level(level) + .target(target) + .build(); + if logger.enabled(&criteria) { + logger.log(&$crate::record::Record::builder() + .level(level) + .target_static(target) + .module_path_static(::core::module_path!()) + .file_static(::core::file!()) + .line(::core::option::Option::Some(::core::line!())) + .column(::core::option::Option::Some(::core::column!())) + .payload(::core::format_args!($($message)+)) + .key_values(&[ + $(($crate::kv::Key::borrowed(&$key), $crate::kv::ToValue::to_value(&$value))),* + ][..] as &[($crate::kv::Key<'_>, $crate::kv::Value<'_>)]) + .build()); + } + } + } + }}; + ($logger:expr, $level:expr, { $($key:expr => $value:expr),* },) => { + $crate::__log!($logger, $level, { $($key => $value),* }, "") + }; +} + +/// Emit a [`Trace`](crate::record::Level::Trace) event. See [`log!`](crate::log!) for syntax. +#[macro_export] +macro_rules! trace { + ($logger:expr, $($event:tt)+) => { + $crate::log!($logger, $crate::record::Level::Trace, $($event)+) + }; +} + +/// Emit a [`Debug`](crate::record::Level::Debug) event. See [`log!`](crate::log!) for syntax. +#[macro_export] +macro_rules! debug { + ($logger:expr, $($event:tt)+) => { + $crate::log!($logger, $crate::record::Level::Debug, $($event)+) + }; +} + +/// Emit a [`Info`](crate::record::Level::Info) event. See [`log!`](crate::log!) for syntax. +#[macro_export] +macro_rules! info { + ($logger:expr, $($event:tt)+) => { + $crate::log!($logger, $crate::record::Level::Info, $($event)+) + }; +} + +/// Emit a [`Warn`](crate::record::Level::Warn) event. See [`log!`](crate::log!) for syntax. +#[macro_export] +macro_rules! warn { + ($logger:expr, $($event:tt)+) => { + $crate::log!($logger, $crate::record::Level::Warn, $($event)+) + }; +} + +/// Emit a [`Error`](crate::record::Level::Error) event. See [`log!`](crate::log!) for syntax. +#[macro_export] +macro_rules! error { + ($logger:expr, $($event:tt)+) => { + $crate::log!($logger, $crate::record::Level::Error, $($event)+) + }; +} + +/// Emit a [`Fatal`](crate::record::Level::Fatal) event. See [`log!`](crate::log!) for syntax. +/// This records severity only; it does not terminate the process or flush. +#[macro_export] +macro_rules! fatal { + ($logger:expr, $($event:tt)+) => { + $crate::log!($logger, $crate::record::Level::Fatal, $($event)+) + }; +} diff --git a/core/tests/serde.rs b/core/tests/serde.rs new file mode 100644 index 0000000..eeafed9 --- /dev/null +++ b/core/tests/serde.rs @@ -0,0 +1,128 @@ +// 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. + +#![cfg(feature = "serde")] + +use logforth_core::kv::ValueOwned; + +#[test] +fn nested_variants_bytes_and_wide_numbers_retain_their_structure() { + #[derive(serde::Serialize)] + enum State { + Idle, + Count(u128), + Pair(i128, bool), + Active { name: String }, + } + #[derive(serde::Serialize)] + struct Snapshot { + states: Vec, + missing: Option, + } + let input = Snapshot { + states: vec![ + State::Idle, + State::Count(u128::MAX), + State::Pair(i128::MIN, true), + State::Active { + name: "worker".into(), + }, + ], + missing: None, + }; + let owned = ValueOwned::from_serde(&input).unwrap(); + let map = owned.view().to_map().unwrap(); + let states: Vec<_> = map + .get("states") + .unwrap() + .to_list() + .unwrap() + .iter() + .collect(); + assert_eq!(states[0].to_str(), Some("Idle")); + assert_eq!( + states[1].to_map().unwrap().get("Count").unwrap().to_u128(), + Some(u128::MAX) + ); + let pair: Vec<_> = states[2] + .to_map() + .unwrap() + .get("Pair") + .unwrap() + .to_list() + .unwrap() + .iter() + .collect(); + assert_eq!(pair[0].to_i128(), Some(i128::MIN)); + assert_eq!(pair[1].to_bool(), Some(true)); + assert_eq!( + states[3] + .to_map() + .unwrap() + .get("Active") + .unwrap() + .to_map() + .unwrap() + .get("name") + .unwrap() + .to_str(), + Some("worker") + ); + assert!(matches!( + map.get("missing"), + Some(logforth_core::kv::ValueView::None) + )); + + struct Bytes; + impl serde::Serialize for Bytes { + fn serialize(&self, s: S) -> Result { + s.serialize_bytes(&[0, 255]) + } + } + assert!(matches!( + ValueOwned::from_serde(&Bytes).unwrap().view(), + logforth_core::kv::ValueView::Bytes(&[0, 255]) + )); +} + +#[test] +fn serialization_errors_are_reported_and_disabled_events_do_not_serialize() { + struct Broken; + impl serde::Serialize for Broken { + fn serialize(&self, _: S) -> Result { + Err(serde::ser::Error::custom("snapshot unavailable")) + } + } + let err = ValueOwned::from_serde(&Broken).unwrap_err(); + assert!(err.to_string().contains("snapshot unavailable")); + let captured = logforth_core::kv::serde(&Broken); + assert!( + captured + .view() + .to_str() + .unwrap() + .starts_with(" ValueOwned::from_serde(&Broken).expect("must remain unevaluated"), + }); +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 7b72c8d..33ebb31 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -23,6 +23,7 @@ rust-version.workspace = true release = false [features] +serde = ["logforth/serde"] # Starters starter-log = ["logforth/starter-log"] @@ -148,3 +149,8 @@ required-features = [ "diagnostic-fastrace", "layout-google-cloud-logging", ] + +[[example]] +name = "native" +path = "src/native.rs" +required-features = ["serde", "bridge-log", "filter-rustlog", "layout-json"] diff --git a/examples/src/native.rs b/examples/src/native.rs new file mode 100644 index 0000000..8cc9e63 --- /dev/null +++ b/examples/src/native.rs @@ -0,0 +1,71 @@ +// 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 logforth::append; +use logforth::bridge::log::LogBridge; +use logforth::diagnostic::StaticDiagnostic; +use logforth::filter::RustLogFilter; +use logforth::filter::rustlog::RustLogFilterBuilder; +use logforth::layout::JsonLayout; + +#[derive(serde::Serialize)] +struct Snapshot { + queue_depths: Vec, + healthy: bool, +} + +fn main() -> Result<(), Box> { + let mut context = StaticDiagnostic::default(); + context.insert("service", "worker"); + let worker_filter = "off,worker=info".parse::()?; + let logger = logforth::builder() + .dispatch(|d| { + d.filter(RustLogFilterBuilder::from_default_env_or("info").build()) + .diagnostic(context.clone()) + .append(append::Stderr::default()) + }) + .dispatch(|d| { + d.filter(worker_filter) + .diagnostic(context.clone()) + .append(append::Stdout::default().with_layout(JsonLayout::default())) + }) + .build(); + log::set_boxed_logger(Box::new(LogBridge::new(logger.clone())))?; + log::set_max_level(log::LevelFilter::Trace); + + // A dedicated stream has independent filtering and output policy. + let usage = logforth::builder() + .dispatch(|d| { + d.diagnostic(context) + .append(append::Stdout::default().with_layout(JsonLayout::default())) + }) + .build() + .named("usage"); + let worker = logger.named("worker"); + let snapshot = Snapshot { + queue_depths: vec![3, 5], + healthy: true, + }; + logforth::info!(worker, { + "snapshot" => logforth::kv::serde(&snapshot), + "completed" => 8u64, + }, "batch complete"); + logforth::info!(usage, { "resource" => "worker", "units" => 8i64 }); + log::warn!("dependency connection interrupted"); + + // Stop producers first; keep exporter runtimes alive through both flushes. + logger.flush(); + usage.flush(); + Ok(()) +} diff --git a/logforth/Cargo.toml b/logforth/Cargo.toml index 3fc26a0..b3124fe 100644 --- a/logforth/Cargo.toml +++ b/logforth/Cargo.toml @@ -72,6 +72,7 @@ filter-rustlog = ["dep:logforth-filter-rustlog"] # Standalone features native-tls = ["logforth-append-syslog?/native-tls"] rustls = ["logforth-append-syslog?/rustls"] +serde = ["logforth-core/serde"] [dependencies] logforth-core = { workspace = true } @@ -94,8 +95,11 @@ logforth-layout-logfmt = { workspace = true, optional = true } logforth-layout-text = { workspace = true, optional = true } [dev-dependencies] +fastrace = { workspace = true, features = ["enable"] } log = { workspace = true } logforth-append-file = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } [lints] workspace = true diff --git a/logforth/src/lib.rs b/logforth/src/lib.rs index 668dcd5..39d4115 100644 --- a/logforth/src/lib.rs +++ b/logforth/src/lib.rs @@ -12,68 +12,65 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Logforth is a flexible logging framework for Rust applications, providing easy log dispatching -//! and configuration. +//! Native structured logging with configurable dispatches, filters, diagnostics, and appenders. //! -//! # Overview +//! Build an explicit [`Logger`] and use the native macros. They skip message and +//! field evaluation when metadata filtering rejects the event. //! -//! Logforth allows you to set up multiple log dispatches with different filters and appenders. You -//! can configure global log levels, use built-in appenders for stdout, stderr, files, or create -//! custom appenders. -//! -//! It provides out-of-the-box integrations with the `log` crate: -//! -//! ```shell -//! cargo add log -//! cargo add logforth -F starter-log //! ``` +//! use logforth::{Level, LevelFilter, append}; //! -//! # Examples -//! -//! Simple setup with a stderr appender: -//! +//! let logger = logforth::builder() +//! .dispatch(|d| d +//! .filter(LevelFilter::MoreSevereEqual(Level::Info)) +//! .append(append::Stderr::default())) +//! .build(); +//! let worker = logger.named("worker"); +//! logforth::info!(worker, { "jobs" => 3u64, "healthy" => true }, "batch complete"); +//! logger.flush(); //! ``` -//! logforth::starter_log::stderr().apply(); //! -//! log::info!("This is an info message."); -//! ``` +//! [`log!`] accepts every native severity; [`info!`] and the other convenience +//! macros use the same syntax. Fields borrow values through [`kv::ToValue`]. +//! Use [`kv::Value::display`] / [`kv::Value::debug`] for text, or `kv::serde` +//! (feature `serde`) for nested structures. A fields-only event needs no message. //! -//! Advanced setup with custom filters and multiple appenders: +//! [`Logger::named`] shares dispatches and selects a stable target, while source +//! metadata always describes the call site. Build a separate logger for an +//! independent output or filtering policy. Stop producers and flush each shared +//! dispatch graph before shutting down resources used by appenders. //! -//! ``` -//! use logforth::append; -//! use logforth::record::Level; -//! use logforth::record::LevelFilter; -//! -//! logforth::starter_log::builder() -//! .dispatch(|d| { -//! d.filter(LevelFilter::MoreSevereEqual(Level::Error)) -//! .append(append::Stderr::default()) -//! }) -//! .dispatch(|d| { -//! d.filter(LevelFilter::MoreSevereEqual(Level::Info)) -//! .append(append::Stdout::default()) -//! }) -//! .apply(); -//! -//! log::error!("Error message."); -//! log::info!("Info message."); -//! ``` +//! The optional `bridge-log` feature forwards dependency logs to a native logger. +//! Existing `starter-log` helpers configure the `log` facade as a compatibility +//! entry point. Native logging does not require either feature. //! -//! See the [README] file for more details and examples. +//! See the [README] for setup, migration, and lifecycle details. //! -//! [README]: https://github.com/fast/logforth?tab=readme-ov-file +//! [README]: https://github.com/fast/logforth#readme #![cfg_attr(docsrs, feature(doc_cfg))] #![deny(missing_docs)] +pub use logforth_core::DispatchBuilder; pub use logforth_core::Error; +pub use logforth_core::Logger; +pub use logforth_core::LoggerBuilder; pub use logforth_core::append::Append; +pub use logforth_core::builder; +pub use logforth_core::debug; pub use logforth_core::diagnostic::Diagnostic; +pub use logforth_core::error; +pub use logforth_core::fatal; pub use logforth_core::filter::Filter; +pub use logforth_core::info; pub use logforth_core::kv; pub use logforth_core::layout::Layout; +pub use logforth_core::log; pub use logforth_core::record; +pub use logforth_core::record::Level; +pub use logforth_core::record::LevelFilter; +pub use logforth_core::trace; +pub use logforth_core::warn; /// Dispatch log records to various targets. pub mod append { diff --git a/logforth/tests/native.rs b/logforth/tests/native.rs new file mode 100644 index 0000000..fa933cf --- /dev/null +++ b/logforth/tests/native.rs @@ -0,0 +1,382 @@ +// 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::cell::Cell; +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +use logforth::Append; +use logforth::Diagnostic; +use logforth::Error; +use logforth::Filter; +use logforth::Level; +use logforth::LevelFilter; +use logforth::filter::FilterResult; +use logforth::kv::KeyOwned; +use logforth::kv::Value; +use logforth::kv::ValueOwned; +use logforth::record::FilterCriteria; +use logforth::record::Record; +use logforth::record::RecordOwned; + +type Captured = (RecordOwned, Vec<(KeyOwned, ValueOwned)>); + +#[derive(Clone, Debug, Default)] +struct Capture { + events: Arc>>, + flushes: Arc, +} + +impl Append for Capture { + fn append(&self, record: &Record, diags: &[Box]) -> Result<(), Error> { + let mut fields = Vec::new(); + for diag in diags { + diag.visit(&mut |key: logforth::kv::KeyView<'_>, + value: logforth::kv::ValueView<'_>| { + fields.push((key.to_owned(), value.to_owned())); + Ok(()) + })?; + } + self.events + .lock() + .unwrap() + .push((record.to_owned(), fields)); + Ok(()) + } + + fn flush(&self) -> Result<(), Error> { + self.flushes.fetch_add(1, Ordering::SeqCst); + Ok(()) + } +} + +#[test] +fn disabled_events_skip_payload_keys_and_conversions() { + struct Expensive; + impl logforth::kv::ToValue for Expensive { + fn to_value(&self) -> Value<'_> { + panic!("disabled field conversion"); + } + } + + let output = Capture::default(); + let logger = logforth::builder() + .dispatch(|d| d.filter(LevelFilter::Off).append(output.clone())) + .build(); + let probes = Cell::new(0); + let payloads = Cell::new(0); + let key = || { + payloads.set(payloads.get() + 1); + "field" + }; + let value = || { + payloads.set(payloads.get() + 1); + Expensive + }; + logforth::log!( + { probes.set(probes.get() + 1); &logger }, + { probes.set(probes.get() + 1); Level::Info }, + { key() => value() }, + "{}", { payloads.set(payloads.get() + 1); 42 }, + ); + assert_eq!(probes.get(), 2); + assert_eq!(payloads.get(), 0); + assert!(output.events.lock().unwrap().is_empty()); +} + +#[test] +fn fields_are_borrowed_typed_and_evaluated_once_across_dispatches() { + let output = Capture::default(); + let logger = Arc::new( + logforth::builder() + .dispatch(|d| d.append(output.clone())) + .dispatch(|d| d.append(output.clone())) + .build(), + ); + let name = String::from("worker"); + let calls = Cell::new(0); + let expected_line = line!() + 1; + logforth::info!(&logger, { + String::from("name") => name, + "count" => { calls.set(calls.get() + 1); 3u32 }, + "ready" => true, + "missing" => None::, + "wide" => u128::MAX, + "debug" => Value::debug(&vec![1, 2]), + "display" => Value::display(&format_args!("{name}:{}", 3)), + }, "{name} finished {count}", count = { calls.set(calls.get() + 1); 3 }); + assert_eq!(calls.get(), 2); + assert_eq!(name, "worker"); + let events = output.events.lock().unwrap(); + assert_eq!(events.len(), 2); + for (event, _) in events.iter() { + event.with(|r| { + assert_eq!(r.target_static(), Some(module_path!())); + assert_eq!(r.module_path_static(), Some(module_path!())); + assert_eq!(r.file_static(), Some(file!())); + assert_eq!(r.line(), Some(expected_line)); + assert_eq!(r.column(), Some(5)); + assert_eq!(r.payload().to_string(), "worker finished 3"); + let fields = r.key_values(); + assert_eq!(fields.get("name").unwrap().to_str(), Some("worker")); + assert_eq!(fields.get("count").unwrap().to_u64(), Some(3)); + assert_eq!(fields.get("ready").unwrap().to_bool(), Some(true)); + assert!(matches!( + fields.get("missing").unwrap(), + logforth::kv::ValueView::None + )); + assert_eq!(fields.get("wide").unwrap().to_u128(), Some(u128::MAX)); + assert_eq!(fields.get("debug").unwrap().to_str(), Some("[1, 2]")); + assert_eq!(fields.get("display").unwrap().to_str(), Some("worker:3")); + }); + } +} + +#[derive(Debug)] +struct MessageFilter; + +impl Filter for MessageFilter { + fn enabled(&self, _: &FilterCriteria, _: &[Box]) -> FilterResult { + FilterResult::Neutral + } + + fn matches(&self, record: &Record, _: &[Box]) -> FilterResult { + if record.payload().to_string().contains("keep") { + FilterResult::Accept + } else { + FilterResult::Reject + } + } +} + +#[test] +fn full_record_filtering_and_explicit_flush_keep_the_logger_usable() { + let output = Capture::default(); + let logger = logforth::builder() + .dispatch(|d| d.filter(MessageFilter).append(output.clone())) + .build(); + logforth::info!(logger, "discard"); + logforth::fatal!(logger, "keep going"); + logger.flush(); + logforth::log!(logger.named("runtime"), Level::Warn4, "keep this too"); + let events = output.events.lock().unwrap(); + assert_eq!(events.len(), 2); + events[0].0.with(|r| assert_eq!(r.level(), Level::Fatal)); + events[1].0.with(|r| { + assert_eq!(r.level(), Level::Warn4); + assert_eq!(r.target(), "runtime"); + assert_eq!(r.module_path(), Some(module_path!())); + }); + assert_eq!(output.flushes.load(Ordering::SeqCst), 1); +} + +#[cfg(feature = "filter-rustlog")] +#[test] +fn targets_route_fields_only_events_and_prefilter_expensive_values() { + use logforth::filter::RustLogFilter; + + let ordinary = Capture::default(); + let usage = Capture::default(); + let logger = logforth::builder() + .dispatch(|d| { + d.filter("info,usage=off".parse::().unwrap()) + .append(ordinary.clone()) + }) + .dispatch(|d| { + d.filter("off,usage=info".parse::().unwrap()) + .append(usage.clone()) + }) + .build(); + logforth::debug!(logger.named("usage"), "{}", panic_if_called()); + logforth::info!(logger.named("usage"), { "units" => 7u64 }); + logforth::warn!(logger, "queue is full"); + logforth::error!(logger, "worker failed"); + logforth::trace!(logger, "{}", panic_if_called()); + assert_eq!(ordinary.events.lock().unwrap().len(), 2); + let usage = usage.events.lock().unwrap(); + assert_eq!(usage.len(), 1); + usage[0].0.with(|r| { + assert_eq!(r.target(), "usage"); + assert_eq!(r.payload().to_string(), ""); + assert_eq!(r.key_values().get("units").unwrap().to_u64(), Some(7)); + }); +} + +#[cfg(feature = "filter-rustlog")] +fn panic_if_called() -> usize { + panic!("disabled message expression"); +} + +#[cfg(feature = "bridge-log")] +#[test] +fn dependency_logs_and_native_events_share_dispatch_and_flush() { + let output = Capture::default(); + let logger = Arc::new( + logforth::builder() + .dispatch(|d| d.append(output.clone())) + .build(), + ); + log::set_boxed_logger(Box::new(logforth::bridge::log::LogBridge::new( + logger.named("bridge"), + ))) + .unwrap(); + log::set_max_level(log::LevelFilter::Trace); + logforth::info!(logger.named("service"), { "ready" => true }, "started"); + log::warn!(target: "dependency", attempts = 2u64; "retrying"); + logger.flush(); + let events = output.events.lock().unwrap(); + assert_eq!(events.len(), 2); + events[0].0.with(|r| assert_eq!(r.target(), "service")); + events[1].0.with(|r| { + assert_eq!(r.target(), "dependency"); + assert_eq!(r.key_values().get("attempts").unwrap().to_u64(), Some(2)); + }); + assert_eq!(output.flushes.load(Ordering::SeqCst), 1); +} + +#[cfg(all( + feature = "serde", + feature = "append-async", + feature = "layout-json", + feature = "diagnostic-fastrace" +))] +#[test] +fn async_dispatch_owns_nested_values_and_captures_diagnostics_before_flush() -> Result<(), Error> { + use logforth::Layout; + use logforth::append::asynchronous::AsyncBuilder; + use logforth::diagnostic::FastraceDiagnostic; + use logforth::diagnostic::StaticDiagnostic; + + #[derive(serde::Serialize)] + struct Snapshot { + queues: Vec>, + healthy: bool, + } + + let output = Capture::default(); + let mut context = StaticDiagnostic::default(); + context.insert("service", "worker"); + let logger = logforth::builder() + .dispatch(|d| { + d.diagnostic(context) + .diagnostic(FastraceDiagnostic::default()) + .append( + AsyncBuilder::new("native-test") + .append(output.clone()) + .build(), + ) + }) + .build(); + { + let span = fastrace::Span::root("batch", fastrace::collector::SpanContext::random()); + let _guard = span.set_local_parent(); + let snapshot = Snapshot { + queues: vec![vec![1, 2], vec![3]], + healthy: true, + }; + logforth::info!(logger, { + "snapshot" => logforth::kv::serde(&snapshot), + "details" => Value::debug(&snapshot.queues), + }, "resource sample"); + } + logger.flush(); + let events = output.events.lock().unwrap(); + assert_eq!(events.len(), 1); + let (record, diags) = &events[0]; + record.with(|r| { + let data = logforth::layout::JsonLayout::default() + .format(&r, &[]) + .unwrap(); + let json: serde_json::Value = serde_json::from_slice(&data).unwrap(); + assert_eq!( + json["kvs"]["snapshot"], + serde_json::json!({"queues": [[1, 2], [3]], "healthy": true}) + ); + assert_eq!(json["kvs"]["details"], "[[1, 2], [3]]"); + }); + let get = |key: &str| { + diags + .iter() + .find(|(k, _)| k.view().as_str() == key) + .unwrap() + .1 + .view() + }; + assert_eq!(get("service").to_str(), Some("worker")); + assert_eq!(get("trace_id").to_str().unwrap().len(), 32); + assert_eq!(get("span_id").to_str().unwrap().len(), 16); + assert_eq!(output.flushes.load(Ordering::SeqCst), 1); + Ok(()) +} + +#[test] +fn named_handles_share_the_pipeline_and_independent_loggers_keep_their_policy() { + let ordinary = Capture::default(); + let logger = logforth::builder() + .dispatch(|d| d.filter(LevelFilter::Off).append(ordinary.clone())) + .build(); + let usage_output = Capture::default(); + let usage = logforth::builder() + .dispatch(|d| d.append(usage_output.clone())) + .build() + .named("usage"); + let cloned = usage.clone(); + let other = usage.named("resource"); + drop(usage); + logforth::info!(logger.named("worker"), "disabled"); + logforth::info!(cloned, { "units" => 5i64 },); + logforth::info!(other, "enabled"); + other.flush(); + assert!(ordinary.events.lock().unwrap().is_empty()); + let events = usage_output.events.lock().unwrap(); + assert_eq!(events.len(), 2); + events[0] + .0 + .with(|r| assert_eq!(r.target_static(), Some("usage"))); + events[1] + .0 + .with(|r| assert_eq!(r.target_static(), Some("resource"))); + assert_eq!(usage_output.flushes.load(Ordering::SeqCst), 1); +} + +#[cfg(feature = "serde")] +#[test] +fn failed_serialization_preserves_the_event_and_other_fields() { + struct Broken; + impl serde::Serialize for Broken { + fn serialize(&self, _: S) -> Result { + Err(serde::ser::Error::custom("snapshot unavailable")) + } + } + let output = Capture::default(); + let logger = logforth::builder() + .dispatch(|d| d.append(output.clone())) + .build(); + logforth::warn!(logger, { + "snapshot" => logforth::kv::serde(&Broken), + "retry" => true, + }, "resource monitor failed"); + let events = output.events.lock().unwrap(); + assert_eq!(events.len(), 1); + events[0].0.with(|r| { + assert_eq!(r.payload().to_string(), "resource monitor failed"); + assert_eq!(r.key_values().get("retry").unwrap().to_bool(), Some(true)); + let value = r.key_values().get("snapshot").unwrap(); + let error = value.to_str().unwrap(); + assert!(error.starts_with(" Date: Mon, 21 Sep 2026 21:13:30 +0800 Subject: [PATCH 2/2] refactor: narrow native facade to event construction --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 2 +- README.md | 68 +----- bridges/log/src/lib.rs | 419 ++++++++++++++++++++++++++++++- core/src/kv.rs | 9 +- core/src/kv/convert.rs | 73 +++--- core/src/kv/serialize.rs | 476 ------------------------------------ core/src/logger/log_impl.rs | 63 +---- core/src/macros.rs | 19 +- core/tests/serde.rs | 128 ---------- examples/Cargo.toml | 2 - examples/src/native.rs | 57 ++--- logforth/Cargo.toml | 4 - logforth/src/lib.rs | 21 +- logforth/tests/native.rs | 229 +++++------------ 15 files changed, 591 insertions(+), 981 deletions(-) delete mode 100644 core/src/kv/serialize.rs delete mode 100644 core/tests/serde.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffded80..0f7cda5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: run: | set -x - cargo run --features="serde,bridge-log,filter-rustlog,layout-json" --example native + cargo run --example native cargo run --features="bridge-log" --example log_with_logger cargo run --features="starter-log" --example simple_stdout diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c32ca..0b3c5a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ### Improvements -* Add instance-based native logging macros with lazy typed fields, shared named logger handles, and optional direct Serde capture. +* Add instance-based native logging macros with lazy typed fields using the existing record and value model. * Make native logging the primary documented API while retaining the optional `log` compatibility bridge. ### Breaking changes diff --git a/README.md b/README.md index 657f823..2878f7a 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The native API is always available and does not depend on the `log` crate. There ## Events and structured fields -Use `trace!`, `debug!`, `info!`, `warn!`, `error!`, or `fatal!` for common severities, or `log!(logger, level, ...)` for any Logforth `Level`. `fatal!` records severity; it does not terminate the process or flush. +Use `trace!`, `debug!`, `info!`, `warn!`, `error!`, or `fatal!`, or `log!(logger, level, ...)` for any Logforth `Level`. `fatal!` records severity; it does not terminate the process or flush. ```rust use logforth::kv::Value; @@ -57,82 +57,40 @@ let logger = logforth::builder() .dispatch(|d| d.append(logforth::append::Stderr::default())) .build(); let queue = String::from("background"); -let completed = 3u64; logforth::info!(logger, { "queue" => queue, - "completed" => completed, + "completed" => 3u64, "healthy" => true, "details" => Value::debug(&[1, 2, 3]), - "description" => Value::display(&format_args!("{queue}: {completed}")), -}, "batch completed"); - -// Fields-only events have an empty message. +}, "batch complete"); logforth::info!(logger, { "queue.depth" => 0 }); ``` -Keys are string expressions; values are borrowed through `kv::ToValue`, retaining scalar types. Use `Value::debug` and `Value::display` when a field should be text. The message uses standard Rust formatting, including named arguments and captures. The optional field map is one syntax, without capture modifiers or a separate enabled probe. +Fields use standard `From` conversions into the existing `kv::Value`: scalars retain their types, and strings and existing values are borrowed. Use `Value::none()` for an absent value. Use `Value::display` or `Value::debug` for text and `Value::list` or `Value::map` for nested values. No logging-specific conversion trait is required. -Macros check level and target before evaluating messages, keys, fields, or conversions. Full-record filters may still reject an event afterward; filters requiring payload or source information must return `Neutral` during prefiltering. Formatting can happen once per consuming appender, so formatting implementations should avoid side effects. +Macros prefilter before evaluating messages, keys, fields, or conversions. Full-record filters may still reject an event afterward. The target defaults to the calling module, and source module/file/line/column identify the actual call site. Existing `RustLogFilter` module directives apply directly. Custom targets remain available through `RecordBuilder` and the `log` bridge. -Enable `serde` for nested structures, arrays, and snapshots: +Event fields describe one event. Existing `Diagnostic` implementations supply dispatch, thread, task, or trace context. These macros do not introduce logger-bound fields, merging precedence, or a context propagation API. Appenders keep their existing field/context handling. For independent filtering or output policy, build independent loggers; use `Arc` to share a pipeline. -```shell -cargo add logforth -F serde -``` - -```rust -let logger = logforth::builder() - .dispatch(|d| d.append(logforth::append::Stderr::default())) - .build(); -logforth::info!(logger, { - "samples" => logforth::kv::serde(&vec![Some(1), None, Some(3)]), -}, "resource sample"); -``` - -`kv::serde` converts directly into Logforth values after prefiltering. If serialization returns an error, that field becomes an explicit `` string and the event continues. Use `ValueOwned::from_serde` when you need to handle the error yourself. This path does not use `log`, value-bag, or sval. - -## Categories, dispatches, and diagnostics - -A logger without a name uses the calling Rust module as its target. `logger.named("worker")` creates a cheap handle sharing the same dispatches and diagnostics, with `"worker"` as its target. Source module, file, line, and column always identify the actual call site. Names replace the current category; there is no implicit logger hierarchy or appender inheritance. - -```rust -use logforth::append; - -let logger = logforth::builder() - .dispatch(|d| d.append(append::Stderr::default())) - .build(); -let worker = logger.named("worker"); -logforth::info!(worker, "started"); -``` - -Existing `RustLogFilter` directives such as `worker=debug` match named handles. Migrate a stable `log` target by creating a handle with the same name; ordinary module directives continue to match unnamed loggers. Bridge records retain their original targets even if the bridge was given a named logger. - -For independent policy, build a separate logger with its own dispatches. For example, a usage stream can use an unfiltered dedicated logger so that changing ordinary diagnostic logging to `off` does not discard usage events. Add multiple dispatches or appenders explicitly when an event should reach several destinations. Names alone do not isolate output or filtering. - -Existing `StaticDiagnostic`, `FastraceDiagnostic`, thread/task diagnostics, text/JSON layouts, rolling files, async appenders, and OTLP all work with native events. Diagnostics remain dispatch context; per-event data belongs in fields. The [native example](examples/src/native.rs) combines categories, JSON, static context, a dedicated stream, and dependency logs. [Appender documentation](https://docs.rs/logforth-append-opentelemetry) covers OTLP configuration. +See the [native example](examples/src/native.rs) for static context and typed fields. Existing text/JSON layouts, rolling files, async appenders, and OTLP consume the same records. [Appender documentation](https://docs.rs/logforth-append-opentelemetry) covers OTLP configuration. ## Lifecycle and dependency logs -Keep a logger handle until shutdown. Stop producers, call `logger.flush()`, and only then tear down runtimes or exporters needed by the appenders. Flushing any clone or named handle flushes the whole shared dispatch graph; it does not disable further logging. Independent loggers must each be flushed. Async appenders wait for pending work during flush; configured overflow policies still apply. This is logging, not a transactional delivery guarantee. - -The optional `log` bridge is the compatibility entry point for dependencies. Enable `bridge-log` and install it once at startup: +Stop logging producers, call `logger.flush()`, and then tear down resources required by appenders. Flush does not disable the logger or provide a transactional delivery guarantee. Independent loggers must each be flushed. -```shell -cargo add log -cargo add logforth -F bridge-log -``` +The optional `bridge-log` feature forwards dependency logs into a native logger. Install the bridge once at startup, retaining the `Arc` for native calls and shutdown: ```rust +use std::sync::Arc; use logforth::append; use logforth::bridge::log::LogBridge; fn main() -> Result<(), log::SetLoggerError> { - let logger = logforth::builder() + let logger = Arc::new(logforth::builder() .dispatch(|d| d.append(append::Stderr::default())) - .build(); + .build()); log::set_boxed_logger(Box::new(LogBridge::new(logger.clone())))?; log::set_max_level(log::LevelFilter::Trace); - logforth::info!(logger, "native event"); log::info!("dependency event"); logger.flush(); @@ -140,7 +98,7 @@ fn main() -> Result<(), log::SetLoggerError> { } ``` -Use `bridge-log-serde` only when incoming `log` fields require Serde support. Native Serde capture needs only `serde`. The `starter-log` helpers remain available for applications using the `log` facade. More examples are in the [examples](examples) directory. +Add `log` and enable `logforth/bridge-log` for this example. `bridge-log-serde` continues to support Serde fields from the `log` facade. Native Serde capture is outside this API; the existing `Value` model supports explicit nested fields. The `starter-log` helpers remain available for applications using the compatibility facade. ## Features diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index 05d5005..8d704a1 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -265,10 +265,13 @@ mod kv { #[cfg(feature = "serde")] mod kv { + use std::collections::HashMap; + use std::fmt; use std::marker::PhantomData; use logforth_core::kv::KeyOwned; use logforth_core::kv::ValueOwned; + use logforth_core::kv::ValueView; pub(super) struct KeyValues<'a> { kvs: Vec<(KeyOwned, ValueOwned)>, @@ -311,7 +314,421 @@ mod kv { } } + // this is derived from `opentelemetry-appender-log`'s serde impl: + // https://github.com/open-telemetry/opentelemetry-rust/blob/f7b0dd99/opentelemetry-appender-log/src/lib.rs#L304-L763 fn value_to_value(value: impl serde::Serialize) -> Option { - ValueOwned::from_serde(&value).ok() + value.serialize(ValueSerializer).ok() + } + + struct ValueSerializer; + + struct ValueSerializeSeq { + value: Vec, + } + + struct ValueSerializeTuple { + value: Vec, + } + + struct ValueSerializeTupleStruct { + value: Vec, + } + + struct ValueSerializeMap { + key: Option, + value: HashMap, + } + + struct ValueSerializeStruct { + value: HashMap, + } + + struct ValueSerializeTupleVariant { + variant: &'static str, + value: Vec, + } + + struct ValueSerializeStructVariant { + variant: &'static str, + value: HashMap, + } + + #[derive(Debug)] + struct ValueError(String); + + impl fmt::Display for ValueError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } + } + + impl serde::ser::Error for ValueError { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + ValueError(msg.to_string()) + } + } + + impl std::error::Error for ValueError {} + + impl serde::Serializer for ValueSerializer { + type Ok = ValueOwned; + + type Error = ValueError; + + type SerializeSeq = ValueSerializeSeq; + + type SerializeTuple = ValueSerializeTuple; + + type SerializeTupleStruct = ValueSerializeTupleStruct; + + type SerializeTupleVariant = ValueSerializeTupleVariant; + + type SerializeMap = ValueSerializeMap; + + type SerializeStruct = ValueSerializeStruct; + + type SerializeStructVariant = ValueSerializeStructVariant; + + fn serialize_bool(self, v: bool) -> Result { + Ok(ValueOwned::bool(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + self.serialize_i64(v as i64) + } + + fn serialize_i16(self, v: i16) -> Result { + self.serialize_i64(v as i64) + } + + fn serialize_i32(self, v: i32) -> Result { + self.serialize_i64(v as i64) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(ValueOwned::i64(v)) + } + + fn serialize_i128(self, v: i128) -> Result { + if let Ok(v) = v.try_into() { + self.serialize_i64(v) + } else { + self.collect_str(&v) + } + } + + fn serialize_u8(self, v: u8) -> Result { + self.serialize_u64(v as u64) + } + + fn serialize_u16(self, v: u16) -> Result { + self.serialize_u64(v as u64) + } + + fn serialize_u32(self, v: u32) -> Result { + self.serialize_u64(v as u64) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(ValueOwned::u64(v)) + } + + fn serialize_u128(self, v: u128) -> Result { + if let Ok(v) = v.try_into() { + self.serialize_u64(v) + } else { + self.collect_str(&v) + } + } + + fn serialize_f32(self, v: f32) -> Result { + self.serialize_f64(v as f64) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(ValueOwned::f64(v)) + } + + fn serialize_char(self, v: char) -> Result { + Ok(ValueOwned::char(v)) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(ValueOwned::str(v.to_string())) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(ValueOwned::bytes(v.to_vec())) + } + + fn serialize_none(self) -> Result { + Ok(ValueOwned::none()) + } + + fn serialize_some( + self, + value: &T, + ) -> Result { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(ValueOwned::none()) + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + Ok(ValueOwned::str(name)) + } + + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + ) -> Result { + Ok(ValueOwned::str(variant)) + } + + fn serialize_newtype_struct( + self, + _: &'static str, + value: &T, + ) -> Result { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + value: &T, + ) -> Result { + let mut map = self.serialize_map(Some(1))?; + serde::ser::SerializeMap::serialize_entry(&mut map, variant, value)?; + serde::ser::SerializeMap::end(map) + } + + fn serialize_seq(self, _: Option) -> Result { + Ok(ValueSerializeSeq { value: vec![] }) + } + + fn serialize_tuple(self, _: usize) -> Result { + Ok(ValueSerializeTuple { value: vec![] }) + } + + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeTupleStruct { value: vec![] }) + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeTupleVariant { + variant, + value: vec![], + }) + } + + fn serialize_map(self, _: Option) -> Result { + Ok(ValueSerializeMap { + key: None, + value: HashMap::new(), + }) + } + + fn serialize_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeStruct { + value: HashMap::new(), + }) + } + + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeStructVariant { + variant, + value: HashMap::new(), + }) + } + } + + impl serde::ser::SerializeSeq for ValueSerializeSeq { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.value.push(value.serialize(ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_vec(self.value)) + } + } + + impl serde::ser::SerializeTuple for ValueSerializeTuple { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.value.push(value.serialize(ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_vec(self.value)) + } + } + + impl serde::ser::SerializeTupleStruct for ValueSerializeTupleStruct { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.value.push(value.serialize(ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_vec(self.value)) + } + } + + impl serde::ser::SerializeTupleVariant for ValueSerializeTupleVariant { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + self.value.push(value.serialize(ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_hash_map({ + let mut variant = HashMap::::new(); + variant.insert(KeyOwned::new(self.variant), ValueOwned::list(self.value)); + variant + })) + } + } + + impl serde::ser::SerializeMap for ValueSerializeMap { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_key( + &mut self, + key: &T, + ) -> Result<(), Self::Error> { + let key = match key.serialize(ValueSerializer)?.view() { + ValueView::StaticStr(s) => KeyOwned::new(s), + value => KeyOwned::new(value.to_string()), + }; + self.key = Some(key); + Ok(()) + } + + fn serialize_value( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + let key = self + .key + .take() + .ok_or_else(|| serde::ser::Error::custom("missing key"))?; + let value = value.serialize(ValueSerializer)?; + self.value.insert(key, value); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_hash_map(self.value)) + } + } + + impl serde::ser::SerializeStruct for ValueSerializeStruct { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + let key = KeyOwned::new(key); + let value = value.serialize(ValueSerializer)?; + self.value.insert(key, value); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_hash_map(self.value)) + } + } + + impl serde::ser::SerializeStructVariant for ValueSerializeStructVariant { + type Ok = ValueOwned; + + type Error = ValueError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + let key = KeyOwned::new(key); + let value = value.serialize(ValueSerializer)?; + self.value.insert(key, value); + Ok(()) + } + + fn end(self) -> Result { + Ok(ValueOwned::from_hash_map({ + let mut variant = HashMap::::new(); + variant.insert( + KeyOwned::new(self.variant), + ValueOwned::from_hash_map(self.value), + ); + variant + })) + } } } diff --git a/core/src/kv.rs b/core/src/kv.rs index b679608..ce7cc6b 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -15,10 +15,7 @@ //! Key-value pairs in a log record or a diagnostic context. mod convert; -pub use self::convert::ToValue; -#[cfg(feature = "serde")] -mod serialize; use std::borrow::Borrow; use std::borrow::Cow; use std::collections::HashMap; @@ -26,8 +23,6 @@ use std::collections::hash_map; use std::fmt; use std::slice; -#[cfg(feature = "serde")] -pub use self::serialize::serde; use crate::Error; use crate::str::RefStr; @@ -370,6 +365,10 @@ impl<'a> Iterator for MapValueIter<'a> { } /// A borrowed value in a key-value pair. +/// +/// Standard [`From`] conversions preserve scalar types and borrow strings, +/// and existing values. Use [`Self::display`] or [`Self::debug`] +/// to explicitly capture a value as text. #[derive(Debug, Clone, Copy)] pub struct Value<'a>(ValueState<'a>); diff --git a/core/src/kv/convert.rs b/core/src/kv/convert.rs index 770dc6b..3e6d11e 100644 --- a/core/src/kv/convert.rs +++ b/core/src/kv/convert.rs @@ -15,58 +15,71 @@ use super::Value; use super::ValueOwned; -/// Borrow a typed value for a structured log field. -/// -/// Native macros borrow their arguments through this trait, so strings and owned -/// values are not consumed. Implement it for domain types with a natural scalar -/// representation. Use [`Value::debug`] or [`Value::display`] for text, and -/// `ValueOwned::from_serde` (with the `serde` feature) for nested structures. -pub trait ToValue { - /// Borrow this value, preserving its supported scalar or structured type. - fn to_value(&self) -> Value<'_>; +impl<'a> From<&&'a str> for Value<'a> { + fn from(value: &&'a str) -> Self { + Self::str(value) + } +} + +impl<'a> From<&'a Value<'_>> for Value<'a> { + fn from(value: &'a Value<'_>) -> Self { + *value + } } -impl ToValue for &T { - fn to_value(&self) -> Value<'_> { - T::to_value(self) +impl<'a> From<&'a ValueOwned> for Value<'a> { + fn from(value: &'a ValueOwned) -> Self { + Self::borrowed(value) } } -impl ToValue for Value<'_> { - fn to_value(&self) -> Value<'_> { - *self +impl<'a> From<&'a str> for Value<'a> { + fn from(value: &'a str) -> Self { + Self::str(value) } } -impl ToValue for ValueOwned { - fn to_value(&self) -> Value<'_> { - Value::borrowed(self) +impl<'a> From<&'a String> for Value<'a> { + fn from(value: &'a String) -> Self { + Self::str(value) } } -impl ToValue for str { - fn to_value(&self) -> Value<'_> { - Value::str(self) +impl<'a> From<&&'a String> for Value<'a> { + fn from(value: &&'a String) -> Self { + Self::str(value) } } -impl ToValue for String { - fn to_value(&self) -> Value<'_> { - Value::str(self) +impl<'a> From<&&'a Value<'_>> for Value<'a> { + fn from(value: &&'a Value<'_>) -> Self { + **value } } -impl ToValue for Option { - fn to_value(&self) -> Value<'_> { - self.as_ref().map_or_else(Value::none, ToValue::to_value) +impl<'a> From<&&'a ValueOwned> for Value<'a> { + fn from(value: &&'a ValueOwned) -> Self { + Self::borrowed(value) } } macro_rules! scalar { ($constructor:ident, $repr:ty, $($ty:ty),+ $(,)?) => { - $(impl ToValue for $ty { - fn to_value(&self) -> Value<'_> { - Value::$constructor(*self as $repr) + $(impl From<$ty> for Value<'_> { + fn from(value: $ty) -> Self { + Self::$constructor(value as $repr) + } + } + + impl From<&$ty> for Value<'_> { + fn from(value: &$ty) -> Self { + >::from(*value) + } + } + + impl From<&&$ty> for Value<'_> { + fn from(value: &&$ty) -> Self { + >::from(**value) } })+ }; diff --git a/core/src/kv/serialize.rs b/core/src/kv/serialize.rs deleted file mode 100644 index ea45dff..0000000 --- a/core/src/kv/serialize.rs +++ /dev/null @@ -1,476 +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::collections::HashMap; -use std::fmt; - -use super::KeyOwned; -use super::ValueOwned; -use super::ValueView; -use crate::Error; - -/// Capture a serializable field in Logforth's native value model. -/// -/// Nested structures and scalar types are preserved. If serialization fails, -/// the field becomes a string of the form ``; the -/// rest of the event is still emitted. Serialization errors do not cause a panic -/// or recursive logging. Panics in a custom serializer are not caught. -/// For explicit error handling, use [`ValueOwned::from_serde`] instead. -/// -/// Call this inside a native macro to serialize only after metadata filtering: -/// -/// ``` -/// let logger = logforth_core::builder().build(); -/// logforth_core::info!(logger, { -/// "samples" => logforth_core::kv::serde(&[1, 2, 3]), -/// }, "resource sample"); -/// ``` -pub fn serde(value: &(impl serde::Serialize + ?Sized)) -> ValueOwned { - ValueOwned::from_serde(value) - .unwrap_or_else(|err| ValueOwned::str(format!(""))) -} - -impl ValueOwned { - /// Serialize into Logforth's native value model. - /// - /// Numbers (including 128-bit integers), booleans, strings, bytes, lists, and - /// nested maps retain their types. Map keys are converted to strings; keys - /// that produce the same string overwrite earlier entries. Enums use Serde's - /// externally tagged representation. Unit structs are represented by name. - /// Returns an error if the source's serializer fails. - /// - /// Place the conversion inside a native macro's field expression to skip it - /// when metadata filtering rejects the event. Handle failures explicitly; - /// logging does not silently discard a failed field or substitute null. - /// - /// ``` - /// # fn main() -> Result<(), logforth_core::Error> { - /// use logforth_core::kv::ValueOwned; - /// let logger = logforth_core::builder().build(); - /// logforth_core::info!(logger, { - /// "samples" => ValueOwned::from_serde(&[1, 2, 3])?, - /// }, "resource sample"); - /// # Ok(()) - /// # } - /// ``` - pub fn from_serde(value: &(impl serde::Serialize + ?Sized)) -> Result { - value - .serialize(ValueSerializer) - .map_err(|err| Error::new("failed to serialize log value").with_source(err)) - } -} - -// Derived from the serializer previously implemented in logforth-bridge-log, -// based on opentelemetry-appender-log: -// https://github.com/open-telemetry/opentelemetry-rust/blob/f7b0dd99/opentelemetry-appender-log/src/lib.rs#L304-L763 -struct ValueSerializer; - -struct ValueSerializeSeq { - value: Vec, -} - -struct ValueSerializeTuple { - value: Vec, -} - -struct ValueSerializeTupleStruct { - value: Vec, -} - -struct ValueSerializeMap { - key: Option, - value: HashMap, -} - -struct ValueSerializeStruct { - value: HashMap, -} - -struct ValueSerializeTupleVariant { - variant: &'static str, - value: Vec, -} - -struct ValueSerializeStructVariant { - variant: &'static str, - value: HashMap, -} - -#[derive(Debug)] -struct ValueError(String); - -impl fmt::Display for ValueError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -impl serde::ser::Error for ValueError { - fn custom(msg: T) -> Self - where - T: fmt::Display, - { - ValueError(msg.to_string()) - } -} - -impl std::error::Error for ValueError {} - -impl serde::Serializer for ValueSerializer { - type Ok = ValueOwned; - - type Error = ValueError; - - type SerializeSeq = ValueSerializeSeq; - - type SerializeTuple = ValueSerializeTuple; - - type SerializeTupleStruct = ValueSerializeTupleStruct; - - type SerializeTupleVariant = ValueSerializeTupleVariant; - - type SerializeMap = ValueSerializeMap; - - type SerializeStruct = ValueSerializeStruct; - - type SerializeStructVariant = ValueSerializeStructVariant; - - fn serialize_bool(self, v: bool) -> Result { - Ok(ValueOwned::bool(v)) - } - - fn serialize_i8(self, v: i8) -> Result { - self.serialize_i64(v as i64) - } - - fn serialize_i16(self, v: i16) -> Result { - self.serialize_i64(v as i64) - } - - fn serialize_i32(self, v: i32) -> Result { - self.serialize_i64(v as i64) - } - - fn serialize_i64(self, v: i64) -> Result { - Ok(ValueOwned::i64(v)) - } - - fn serialize_i128(self, v: i128) -> Result { - Ok(ValueOwned::i128(v)) - } - - fn serialize_u8(self, v: u8) -> Result { - self.serialize_u64(v as u64) - } - - fn serialize_u16(self, v: u16) -> Result { - self.serialize_u64(v as u64) - } - - fn serialize_u32(self, v: u32) -> Result { - self.serialize_u64(v as u64) - } - - fn serialize_u64(self, v: u64) -> Result { - Ok(ValueOwned::u64(v)) - } - - fn serialize_u128(self, v: u128) -> Result { - Ok(ValueOwned::u128(v)) - } - - fn serialize_f32(self, v: f32) -> Result { - self.serialize_f64(v as f64) - } - - fn serialize_f64(self, v: f64) -> Result { - Ok(ValueOwned::f64(v)) - } - - fn serialize_char(self, v: char) -> Result { - Ok(ValueOwned::char(v)) - } - - fn serialize_str(self, v: &str) -> Result { - Ok(ValueOwned::str(v.to_string())) - } - - fn serialize_bytes(self, v: &[u8]) -> Result { - Ok(ValueOwned::bytes(v.to_vec())) - } - - fn serialize_none(self) -> Result { - Ok(ValueOwned::none()) - } - - fn serialize_some( - self, - value: &T, - ) -> Result { - value.serialize(self) - } - - fn serialize_unit(self) -> Result { - Ok(ValueOwned::none()) - } - - fn serialize_unit_struct(self, name: &'static str) -> Result { - Ok(ValueOwned::str(name)) - } - - fn serialize_unit_variant( - self, - _: &'static str, - _: u32, - variant: &'static str, - ) -> Result { - Ok(ValueOwned::str(variant)) - } - - fn serialize_newtype_struct( - self, - _: &'static str, - value: &T, - ) -> Result { - value.serialize(self) - } - - fn serialize_newtype_variant( - self, - _: &'static str, - _: u32, - variant: &'static str, - value: &T, - ) -> Result { - let mut map = self.serialize_map(Some(1))?; - serde::ser::SerializeMap::serialize_entry(&mut map, variant, value)?; - serde::ser::SerializeMap::end(map) - } - - fn serialize_seq(self, _: Option) -> Result { - Ok(ValueSerializeSeq { value: vec![] }) - } - - fn serialize_tuple(self, _: usize) -> Result { - Ok(ValueSerializeTuple { value: vec![] }) - } - - fn serialize_tuple_struct( - self, - _: &'static str, - _: usize, - ) -> Result { - Ok(ValueSerializeTupleStruct { value: vec![] }) - } - - fn serialize_tuple_variant( - self, - _: &'static str, - _: u32, - variant: &'static str, - _: usize, - ) -> Result { - Ok(ValueSerializeTupleVariant { - variant, - value: vec![], - }) - } - - fn serialize_map(self, _: Option) -> Result { - Ok(ValueSerializeMap { - key: None, - value: HashMap::new(), - }) - } - - fn serialize_struct( - self, - _: &'static str, - _: usize, - ) -> Result { - Ok(ValueSerializeStruct { - value: HashMap::new(), - }) - } - - fn serialize_struct_variant( - self, - _: &'static str, - _: u32, - variant: &'static str, - _: usize, - ) -> Result { - Ok(ValueSerializeStructVariant { - variant, - value: HashMap::new(), - }) - } -} - -impl serde::ser::SerializeSeq for ValueSerializeSeq { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_element( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - self.value.push(value.serialize(ValueSerializer)?); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_vec(self.value)) - } -} - -impl serde::ser::SerializeTuple for ValueSerializeTuple { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_element( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - self.value.push(value.serialize(ValueSerializer)?); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_vec(self.value)) - } -} - -impl serde::ser::SerializeTupleStruct for ValueSerializeTupleStruct { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_field( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - self.value.push(value.serialize(ValueSerializer)?); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_vec(self.value)) - } -} - -impl serde::ser::SerializeTupleVariant for ValueSerializeTupleVariant { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_field( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - self.value.push(value.serialize(ValueSerializer)?); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_hash_map({ - let mut variant = HashMap::::new(); - variant.insert(KeyOwned::new(self.variant), ValueOwned::list(self.value)); - variant - })) - } -} - -impl serde::ser::SerializeMap for ValueSerializeMap { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> { - let key = match key.serialize(ValueSerializer)?.view() { - ValueView::StaticStr(s) => KeyOwned::new(s), - value => KeyOwned::new(value.to_string()), - }; - self.key = Some(key); - Ok(()) - } - - fn serialize_value( - &mut self, - value: &T, - ) -> Result<(), Self::Error> { - let key = self - .key - .take() - .ok_or_else(|| serde::ser::Error::custom("missing key"))?; - let value = value.serialize(ValueSerializer)?; - self.value.insert(key, value); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_hash_map(self.value)) - } -} - -impl serde::ser::SerializeStruct for ValueSerializeStruct { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_field( - &mut self, - key: &'static str, - value: &T, - ) -> Result<(), Self::Error> { - let key = KeyOwned::new(key); - let value = value.serialize(ValueSerializer)?; - self.value.insert(key, value); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_hash_map(self.value)) - } -} - -impl serde::ser::SerializeStructVariant for ValueSerializeStructVariant { - type Ok = ValueOwned; - - type Error = ValueError; - - fn serialize_field( - &mut self, - key: &'static str, - value: &T, - ) -> Result<(), Self::Error> { - let key = KeyOwned::new(key); - let value = value.serialize(ValueSerializer)?; - self.value.insert(key, value); - Ok(()) - } - - fn end(self) -> Result { - Ok(ValueOwned::from_hash_map({ - let mut variant = HashMap::::new(); - variant.insert( - KeyOwned::new(self.variant), - ValueOwned::from_hash_map(self.value), - ); - variant - })) - } -} diff --git a/core/src/logger/log_impl.rs b/core/src/logger/log_impl.rs index 7ed4c87..f61e876 100644 --- a/core/src/logger/log_impl.rs +++ b/core/src/logger/log_impl.rs @@ -14,7 +14,6 @@ use std::io::Write; use std::panic; -use std::sync::Arc; use crate::Append; use crate::Diagnostic; @@ -24,62 +23,23 @@ use crate::filter::FilterResult; use crate::record::FilterCriteria; use crate::record::Record; -/// A handle to shared dispatches, filters, diagnostics, and appenders. -/// -/// Cloning or naming a logger shares the configured pipeline. The final handle -/// owns appender teardown; flushing any handle flushes the shared pipeline. -#[derive(Debug, Clone)] +/// A logger that dispatches log records to one or more dispatcher. +#[derive(Debug)] pub struct Logger { - dispatches: Arc<[Dispatch]>, - name: Option<&'static str>, + dispatches: Vec, } impl Logger { pub(super) fn new(dispatches: Vec) -> Self { - Self { - dispatches: dispatches.into(), - name: None, - } + Self { dispatches } } } impl Logger { - /// Create a named handle sharing this logger's dispatches and diagnostics. - /// - /// Native macros use this name as the record target, while source metadata - /// still identifies the actual call site. Names replace rather than append - /// to the current name. Cloning or naming a handle never rebuilds appenders. - /// - /// Use stable application categories such as `"worker"`. Event-specific data - /// belongs in fields. For an independent filtering or output policy, build - /// a separate logger instead of naming a shared handle. - /// - /// ``` - /// let logger = logforth_core::builder().build(); - /// let worker = logger.named("worker"); - /// logforth_core::info!(worker, "started"); - /// ``` - #[must_use = "use the returned logger handle to emit events with this name"] - pub fn named(&self, name: &'static str) -> Self { - Self { - dispatches: self.dispatches.clone(), - name: Some(name), - } - } - - /// The native logging category, or `None` for call-site module targets. - /// - /// Names do not rewrite records passed directly to [`Self::log`], including - /// records forwarded by a bridge. - pub fn name(&self) -> Option<&'static str> { - self.name - } - /// Test whether any dispatch might accept an event with this level and target. /// - /// This is a conservative prefilter. A full-record filter can still reject - /// the event, and dynamic filters can change before emission. Native macros - /// call this before evaluating the message and fields. + /// This is a conservative prefilter. Full-record filters can still reject + /// the event. Native macros call this before evaluating messages and fields. pub fn enabled(&self, criteria: &FilterCriteria) -> bool { self.dispatches .iter() @@ -88,7 +48,7 @@ impl Logger { /// Log the [`Record`]. pub fn log(&self, record: &Record) { - for dispatch in self.dispatches.iter() { + for dispatch in &self.dispatches { for err in dispatch.log(record) { handle_log_error(record, &err); } @@ -97,12 +57,11 @@ impl Logger { /// Flush buffered records through every configured appender. /// - /// Stop logging producers before calling this during graceful shutdown, and - /// keep any runtimes required by appenders alive until it returns. This does - /// not disable the logger or prevent later events. Appender errors use the - /// same stderr fallback as logging errors. There is no implicit exit hook. + /// Stop producers before flushing during shutdown, and keep resources needed + /// by appenders alive until this returns. This does not disable the logger. + /// Errors use the existing stderr fallback. There is no implicit exit hook. pub fn flush(&self) { - for dispatch in self.dispatches.iter() { + for dispatch in &self.dispatches { for err in dispatch.flush() { handle_flush_error(&err); } diff --git a/core/src/macros.rs b/core/src/macros.rs index 2d8a8ae..47c8e86 100644 --- a/core/src/macros.rs +++ b/core/src/macros.rs @@ -16,18 +16,19 @@ /// /// The logger is borrowed and evaluated once. An optional field map follows the /// logger (and level for `log!`). Keys are string expressions; -/// values implement [`ToValue`](crate::kv::ToValue). A fields-only event has an -/// empty message. The message uses the standard Rust formatting syntax. +/// values use the standard [`From`] conversions into [`Value`](crate::kv::Value). +/// A fields-only event has an empty message. The message uses the standard Rust +/// formatting syntax. /// /// Logger and level are evaluated once before metadata filtering. Message, key, /// and value expressions are only evaluated if at least one dispatch may accept /// the event. Full-record filters can still reject it. Formatting can happen more /// than once when several appenders consume the same event. /// -/// A named logger uses its name as the target; otherwise the target is the calling -/// module. Module, file, line, and column always describe the actual call site. All native levels -/// are supported, including `Level::Fatal`, which neither terminates the process nor implicitly -/// flushes the logger. +/// The target and source metadata identify the calling module and location. +/// Use [`Record::builder`](crate::record::Record::builder) for a custom target. +/// All native levels are supported, including `Level::Fatal`, which neither +/// terminates the process nor implicitly flushes the logger. /// /// ``` /// use logforth_core::{builder, info, log}; @@ -37,7 +38,7 @@ /// let logger = builder().build(); /// let count = 3; /// info!(logger, "processed {count} jobs"); -/// log!(logger.named("worker"), Level::Info2, { +/// log!(logger, Level::Info2, { /// "jobs.completed" => count, /// "healthy" => true, /// "details" => Value::debug(&[1, 2, 3]), @@ -63,7 +64,7 @@ macro_rules! __log { match (&$logger, $level) { (logger, level) => { let logger: &$crate::Logger = logger; - let target = logger.name().unwrap_or(::core::module_path!()); + let target = ::core::module_path!(); let criteria = $crate::record::FilterCriteria::builder() .level(level) .target(target) @@ -78,7 +79,7 @@ macro_rules! __log { .column(::core::option::Option::Some(::core::column!())) .payload(::core::format_args!($($message)+)) .key_values(&[ - $(($crate::kv::Key::borrowed(&$key), $crate::kv::ToValue::to_value(&$value))),* + $(($crate::kv::Key::borrowed(&$key), $crate::kv::Value::from(&$value))),* ][..] as &[($crate::kv::Key<'_>, $crate::kv::Value<'_>)]) .build()); } diff --git a/core/tests/serde.rs b/core/tests/serde.rs deleted file mode 100644 index eeafed9..0000000 --- a/core/tests/serde.rs +++ /dev/null @@ -1,128 +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. - -#![cfg(feature = "serde")] - -use logforth_core::kv::ValueOwned; - -#[test] -fn nested_variants_bytes_and_wide_numbers_retain_their_structure() { - #[derive(serde::Serialize)] - enum State { - Idle, - Count(u128), - Pair(i128, bool), - Active { name: String }, - } - #[derive(serde::Serialize)] - struct Snapshot { - states: Vec, - missing: Option, - } - let input = Snapshot { - states: vec![ - State::Idle, - State::Count(u128::MAX), - State::Pair(i128::MIN, true), - State::Active { - name: "worker".into(), - }, - ], - missing: None, - }; - let owned = ValueOwned::from_serde(&input).unwrap(); - let map = owned.view().to_map().unwrap(); - let states: Vec<_> = map - .get("states") - .unwrap() - .to_list() - .unwrap() - .iter() - .collect(); - assert_eq!(states[0].to_str(), Some("Idle")); - assert_eq!( - states[1].to_map().unwrap().get("Count").unwrap().to_u128(), - Some(u128::MAX) - ); - let pair: Vec<_> = states[2] - .to_map() - .unwrap() - .get("Pair") - .unwrap() - .to_list() - .unwrap() - .iter() - .collect(); - assert_eq!(pair[0].to_i128(), Some(i128::MIN)); - assert_eq!(pair[1].to_bool(), Some(true)); - assert_eq!( - states[3] - .to_map() - .unwrap() - .get("Active") - .unwrap() - .to_map() - .unwrap() - .get("name") - .unwrap() - .to_str(), - Some("worker") - ); - assert!(matches!( - map.get("missing"), - Some(logforth_core::kv::ValueView::None) - )); - - struct Bytes; - impl serde::Serialize for Bytes { - fn serialize(&self, s: S) -> Result { - s.serialize_bytes(&[0, 255]) - } - } - assert!(matches!( - ValueOwned::from_serde(&Bytes).unwrap().view(), - logforth_core::kv::ValueView::Bytes(&[0, 255]) - )); -} - -#[test] -fn serialization_errors_are_reported_and_disabled_events_do_not_serialize() { - struct Broken; - impl serde::Serialize for Broken { - fn serialize(&self, _: S) -> Result { - Err(serde::ser::Error::custom("snapshot unavailable")) - } - } - let err = ValueOwned::from_serde(&Broken).unwrap_err(); - assert!(err.to_string().contains("snapshot unavailable")); - let captured = logforth_core::kv::serde(&Broken); - assert!( - captured - .view() - .to_str() - .unwrap() - .starts_with(" ValueOwned::from_serde(&Broken).expect("must remain unevaluated"), - }); -} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 33ebb31..317e2ae 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -23,7 +23,6 @@ rust-version.workspace = true release = false [features] -serde = ["logforth/serde"] # Starters starter-log = ["logforth/starter-log"] @@ -153,4 +152,3 @@ required-features = [ [[example]] name = "native" path = "src/native.rs" -required-features = ["serde", "bridge-log", "filter-rustlog", "layout-json"] diff --git a/examples/src/native.rs b/examples/src/native.rs index 8cc9e63..e36c6a4 100644 --- a/examples/src/native.rs +++ b/examples/src/native.rs @@ -12,60 +12,33 @@ // See the License for the specific language governing permissions and // limitations under the License. +use logforth::Level; +use logforth::LevelFilter; use logforth::append; -use logforth::bridge::log::LogBridge; use logforth::diagnostic::StaticDiagnostic; -use logforth::filter::RustLogFilter; -use logforth::filter::rustlog::RustLogFilterBuilder; -use logforth::layout::JsonLayout; +use logforth::kv::Value; -#[derive(serde::Serialize)] -struct Snapshot { - queue_depths: Vec, - healthy: bool, -} - -fn main() -> Result<(), Box> { +fn main() { let mut context = StaticDiagnostic::default(); context.insert("service", "worker"); - let worker_filter = "off,worker=info".parse::()?; let logger = logforth::builder() .dispatch(|d| { - d.filter(RustLogFilterBuilder::from_default_env_or("info").build()) - .diagnostic(context.clone()) + d.filter(LevelFilter::MoreSevereEqual(Level::Info)) + .diagnostic(context) .append(append::Stderr::default()) }) - .dispatch(|d| { - d.filter(worker_filter) - .diagnostic(context.clone()) - .append(append::Stdout::default().with_layout(JsonLayout::default())) - }) .build(); - log::set_boxed_logger(Box::new(LogBridge::new(logger.clone())))?; - log::set_max_level(log::LevelFilter::Trace); - // A dedicated stream has independent filtering and output policy. - let usage = logforth::builder() - .dispatch(|d| { - d.diagnostic(context) - .append(append::Stdout::default().with_layout(JsonLayout::default())) - }) - .build() - .named("usage"); - let worker = logger.named("worker"); - let snapshot = Snapshot { - queue_depths: vec![3, 5], - healthy: true, - }; - logforth::info!(worker, { - "snapshot" => logforth::kv::serde(&snapshot), - "completed" => 8u64, + let queue = String::from("background"); + logforth::info!(logger, { + "queue" => queue, + "completed" => 3u64, + "healthy" => true, + "details" => Value::debug(&[1, 2, 3]), }, "batch complete"); - logforth::info!(usage, { "resource" => "worker", "units" => 8i64 }); - log::warn!("dependency connection interrupted"); + logforth::info!(logger, { "queue.depth" => 0 }); + logforth::debug!(logger, "this event is filtered out"); - // Stop producers first; keep exporter runtimes alive through both flushes. + // Stop producers before flushing, while appender resources are still alive. logger.flush(); - usage.flush(); - Ok(()) } diff --git a/logforth/Cargo.toml b/logforth/Cargo.toml index b3124fe..3fc26a0 100644 --- a/logforth/Cargo.toml +++ b/logforth/Cargo.toml @@ -72,7 +72,6 @@ filter-rustlog = ["dep:logforth-filter-rustlog"] # Standalone features native-tls = ["logforth-append-syslog?/native-tls"] rustls = ["logforth-append-syslog?/rustls"] -serde = ["logforth-core/serde"] [dependencies] logforth-core = { workspace = true } @@ -95,11 +94,8 @@ logforth-layout-logfmt = { workspace = true, optional = true } logforth-layout-text = { workspace = true, optional = true } [dev-dependencies] -fastrace = { workspace = true, features = ["enable"] } log = { workspace = true } logforth-append-file = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } [lints] workspace = true diff --git a/logforth/src/lib.rs b/logforth/src/lib.rs index 39d4115..1bab2c2 100644 --- a/logforth/src/lib.rs +++ b/logforth/src/lib.rs @@ -25,20 +25,21 @@ //! .filter(LevelFilter::MoreSevereEqual(Level::Info)) //! .append(append::Stderr::default())) //! .build(); -//! let worker = logger.named("worker"); -//! logforth::info!(worker, { "jobs" => 3u64, "healthy" => true }, "batch complete"); +//! logforth::info!(logger, { "jobs" => 3u64, "healthy" => true }, "batch complete"); //! logger.flush(); //! ``` //! -//! [`log!`] accepts every native severity; [`info!`] and the other convenience -//! macros use the same syntax. Fields borrow values through [`kv::ToValue`]. -//! Use [`kv::Value::display`] / [`kv::Value::debug`] for text, or `kv::serde` -//! (feature `serde`) for nested structures. A fields-only event needs no message. +//! [`log!`] accepts every native severity; the convenience macros use the same +//! syntax. Fields use standard [`From`] conversions into [`kv::Value`], borrowing +//! strings and existing values. Use [`kv::Value::display`] / [`kv::Value::debug`] +//! for text, and [`kv::Value::list`] / [`kv::Value::map`] for structured values. +//! A fields-only event needs no message. //! -//! [`Logger::named`] shares dispatches and selects a stable target, while source -//! metadata always describes the call site. Build a separate logger for an -//! independent output or filtering policy. Stop producers and flush each shared -//! dispatch graph before shutting down resources used by appenders. +//! The target and source metadata come from the call site. Custom targets remain +//! available through [`record::RecordBuilder`] and compatibility bridges. +//! Diagnostics supply context through the existing dispatch API; these macros +//! do not bind context to loggers or change how appenders combine it with fields. +//! Stop producers and flush before shutting down resources used by appenders. //! //! The optional `bridge-log` feature forwards dependency logs to a native logger. //! Existing `starter-log` helpers configure the `log` facade as a compatibility diff --git a/logforth/tests/native.rs b/logforth/tests/native.rs index fa933cf..af544ab 100644 --- a/logforth/tests/native.rs +++ b/logforth/tests/native.rs @@ -66,8 +66,8 @@ impl Append for Capture { #[test] fn disabled_events_skip_payload_keys_and_conversions() { struct Expensive; - impl logforth::kv::ToValue for Expensive { - fn to_value(&self) -> Value<'_> { + impl From<&Expensive> for Value<'_> { + fn from(_: &Expensive) -> Self { panic!("disabled field conversion"); } } @@ -100,20 +100,31 @@ fn disabled_events_skip_payload_keys_and_conversions() { #[test] fn fields_are_borrowed_typed_and_evaluated_once_across_dispatches() { let output = Capture::default(); + let mut context = logforth::diagnostic::StaticDiagnostic::default(); + context.insert("name", "service"); + let nested = ValueOwned::map([("depth".into(), 2u64.into())]); let logger = Arc::new( logforth::builder() - .dispatch(|d| d.append(output.clone())) - .dispatch(|d| d.append(output.clone())) + .dispatch(|d| d.diagnostic(context.clone()).append(output.clone())) + .dispatch(|d| d.diagnostic(context.clone()).append(output.clone())) .build(), ); let name = String::from("worker"); + let name_ref = &name; + let count = 3u32; + let count_ref = &count; + let nested_ref = &nested; + let ready = Value::from(true); + let ready_ref = &ready; let calls = Cell::new(0); let expected_line = line!() + 1; logforth::info!(&logger, { String::from("name") => name, - "count" => { calls.set(calls.get() + 1); 3u32 }, - "ready" => true, - "missing" => None::, + "count" => { calls.set(calls.get() + 1); count_ref }, + "borrowed_name" => name_ref, + "ready" => ready_ref, + "missing" => Value::none(), + "nested" => nested_ref, "wide" => u128::MAX, "debug" => Value::debug(&vec![1, 2]), "display" => Value::display(&format_args!("{name}:{}", 3)), @@ -122,7 +133,10 @@ fn fields_are_borrowed_typed_and_evaluated_once_across_dispatches() { assert_eq!(name, "worker"); let events = output.events.lock().unwrap(); assert_eq!(events.len(), 2); - for (event, _) in events.iter() { + for (event, diagnostics) in events.iter() { + assert_eq!(diagnostics.len(), 1); + assert_eq!(diagnostics[0].0.view().as_str(), "name"); + assert_eq!(diagnostics[0].1.view().to_str(), Some("service")); event.with(|r| { assert_eq!(r.target_static(), Some(module_path!())); assert_eq!(r.module_path_static(), Some(module_path!())); @@ -133,6 +147,21 @@ fn fields_are_borrowed_typed_and_evaluated_once_across_dispatches() { let fields = r.key_values(); assert_eq!(fields.get("name").unwrap().to_str(), Some("worker")); assert_eq!(fields.get("count").unwrap().to_u64(), Some(3)); + assert_eq!( + fields.get("borrowed_name").unwrap().to_str(), + Some("worker") + ); + assert_eq!( + fields + .get("nested") + .unwrap() + .to_map() + .unwrap() + .get("depth") + .unwrap() + .to_u64(), + Some(2) + ); assert_eq!(fields.get("ready").unwrap().to_bool(), Some(true)); assert!(matches!( fields.get("missing").unwrap(), @@ -171,13 +200,13 @@ fn full_record_filtering_and_explicit_flush_keep_the_logger_usable() { logforth::info!(logger, "discard"); logforth::fatal!(logger, "keep going"); logger.flush(); - logforth::log!(logger.named("runtime"), Level::Warn4, "keep this too"); + logforth::log!(logger, Level::Warn4, "keep this too"); let events = output.events.lock().unwrap(); assert_eq!(events.len(), 2); events[0].0.with(|r| assert_eq!(r.level(), Level::Fatal)); events[1].0.with(|r| { assert_eq!(r.level(), Level::Warn4); - assert_eq!(r.target(), "runtime"); + assert_eq!(r.target(), module_path!()); assert_eq!(r.module_path(), Some(module_path!())); }); assert_eq!(output.flushes.load(Ordering::SeqCst), 1); @@ -185,41 +214,29 @@ fn full_record_filtering_and_explicit_flush_keep_the_logger_usable() { #[cfg(feature = "filter-rustlog")] #[test] -fn targets_route_fields_only_events_and_prefilter_expensive_values() { +fn module_filters_apply_before_fields_only_events_are_built() { use logforth::filter::RustLogFilter; - let ordinary = Capture::default(); - let usage = Capture::default(); + let output = Capture::default(); + let filter = format!("off,{}=info", module_path!()) + .parse::() + .unwrap(); let logger = logforth::builder() - .dispatch(|d| { - d.filter("info,usage=off".parse::().unwrap()) - .append(ordinary.clone()) - }) - .dispatch(|d| { - d.filter("off,usage=info".parse::().unwrap()) - .append(usage.clone()) - }) + .dispatch(|d| d.filter(filter).append(output.clone())) .build(); - logforth::debug!(logger.named("usage"), "{}", panic_if_called()); - logforth::info!(logger.named("usage"), { "units" => 7u64 }); - logforth::warn!(logger, "queue is full"); - logforth::error!(logger, "worker failed"); - logforth::trace!(logger, "{}", panic_if_called()); - assert_eq!(ordinary.events.lock().unwrap().len(), 2); - let usage = usage.events.lock().unwrap(); - assert_eq!(usage.len(), 1); - usage[0].0.with(|r| { - assert_eq!(r.target(), "usage"); + let calls = Cell::new(0); + logforth::debug!(logger, { "units" => { calls.set(calls.get() + 1); 7u64 } }); + logforth::info!(logger, { "units" => 7u64 },); + assert_eq!(calls.get(), 0); + let events = output.events.lock().unwrap(); + assert_eq!(events.len(), 1); + events[0].0.with(|r| { + assert_eq!(r.target(), module_path!()); assert_eq!(r.payload().to_string(), ""); assert_eq!(r.key_values().get("units").unwrap().to_u64(), Some(7)); }); } -#[cfg(feature = "filter-rustlog")] -fn panic_if_called() -> usize { - panic!("disabled message expression"); -} - #[cfg(feature = "bridge-log")] #[test] fn dependency_logs_and_native_events_share_dispatch_and_flush() { @@ -230,16 +247,16 @@ fn dependency_logs_and_native_events_share_dispatch_and_flush() { .build(), ); log::set_boxed_logger(Box::new(logforth::bridge::log::LogBridge::new( - logger.named("bridge"), + logger.clone(), ))) .unwrap(); log::set_max_level(log::LevelFilter::Trace); - logforth::info!(logger.named("service"), { "ready" => true }, "started"); + logforth::info!(logger, { "ready" => true }, "started"); log::warn!(target: "dependency", attempts = 2u64; "retrying"); logger.flush(); let events = output.events.lock().unwrap(); assert_eq!(events.len(), 2); - events[0].0.with(|r| assert_eq!(r.target(), "service")); + events[0].0.with(|r| assert_eq!(r.target(), module_path!())); events[1].0.with(|r| { assert_eq!(r.target(), "dependency"); assert_eq!(r.key_values().get("attempts").unwrap().to_u64(), Some(2)); @@ -247,136 +264,18 @@ fn dependency_logs_and_native_events_share_dispatch_and_flush() { assert_eq!(output.flushes.load(Ordering::SeqCst), 1); } -#[cfg(all( - feature = "serde", - feature = "append-async", - feature = "layout-json", - feature = "diagnostic-fastrace" -))] #[test] -fn async_dispatch_owns_nested_values_and_captures_diagnostics_before_flush() -> Result<(), Error> { - use logforth::Layout; - use logforth::append::asynchronous::AsyncBuilder; - use logforth::diagnostic::FastraceDiagnostic; - use logforth::diagnostic::StaticDiagnostic; - - #[derive(serde::Serialize)] - struct Snapshot { - queues: Vec>, - healthy: bool, - } - +fn independent_loggers_keep_their_filtering_and_flush_policy() { let output = Capture::default(); - let mut context = StaticDiagnostic::default(); - context.insert("service", "worker"); - let logger = logforth::builder() - .dispatch(|d| { - d.diagnostic(context) - .diagnostic(FastraceDiagnostic::default()) - .append( - AsyncBuilder::new("native-test") - .append(output.clone()) - .build(), - ) - }) - .build(); - { - let span = fastrace::Span::root("batch", fastrace::collector::SpanContext::random()); - let _guard = span.set_local_parent(); - let snapshot = Snapshot { - queues: vec![vec![1, 2], vec![3]], - healthy: true, - }; - logforth::info!(logger, { - "snapshot" => logforth::kv::serde(&snapshot), - "details" => Value::debug(&snapshot.queues), - }, "resource sample"); - } - logger.flush(); - let events = output.events.lock().unwrap(); - assert_eq!(events.len(), 1); - let (record, diags) = &events[0]; - record.with(|r| { - let data = logforth::layout::JsonLayout::default() - .format(&r, &[]) - .unwrap(); - let json: serde_json::Value = serde_json::from_slice(&data).unwrap(); - assert_eq!( - json["kvs"]["snapshot"], - serde_json::json!({"queues": [[1, 2], [3]], "healthy": true}) - ); - assert_eq!(json["kvs"]["details"], "[[1, 2], [3]]"); - }); - let get = |key: &str| { - diags - .iter() - .find(|(k, _)| k.view().as_str() == key) - .unwrap() - .1 - .view() - }; - assert_eq!(get("service").to_str(), Some("worker")); - assert_eq!(get("trace_id").to_str().unwrap().len(), 32); - assert_eq!(get("span_id").to_str().unwrap().len(), 16); - assert_eq!(output.flushes.load(Ordering::SeqCst), 1); - Ok(()) -} - -#[test] -fn named_handles_share_the_pipeline_and_independent_loggers_keep_their_policy() { - let ordinary = Capture::default(); - let logger = logforth::builder() - .dispatch(|d| d.filter(LevelFilter::Off).append(ordinary.clone())) + let ordinary = logforth::builder() + .dispatch(|d| d.filter(LevelFilter::Off).append(output.clone())) .build(); - let usage_output = Capture::default(); let usage = logforth::builder() - .dispatch(|d| d.append(usage_output.clone())) - .build() - .named("usage"); - let cloned = usage.clone(); - let other = usage.named("resource"); - drop(usage); - logforth::info!(logger.named("worker"), "disabled"); - logforth::info!(cloned, { "units" => 5i64 },); - logforth::info!(other, "enabled"); - other.flush(); - assert!(ordinary.events.lock().unwrap().is_empty()); - let events = usage_output.events.lock().unwrap(); - assert_eq!(events.len(), 2); - events[0] - .0 - .with(|r| assert_eq!(r.target_static(), Some("usage"))); - events[1] - .0 - .with(|r| assert_eq!(r.target_static(), Some("resource"))); - assert_eq!(usage_output.flushes.load(Ordering::SeqCst), 1); -} - -#[cfg(feature = "serde")] -#[test] -fn failed_serialization_preserves_the_event_and_other_fields() { - struct Broken; - impl serde::Serialize for Broken { - fn serialize(&self, _: S) -> Result { - Err(serde::ser::Error::custom("snapshot unavailable")) - } - } - let output = Capture::default(); - let logger = logforth::builder() .dispatch(|d| d.append(output.clone())) .build(); - logforth::warn!(logger, { - "snapshot" => logforth::kv::serde(&Broken), - "retry" => true, - }, "resource monitor failed"); - let events = output.events.lock().unwrap(); - assert_eq!(events.len(), 1); - events[0].0.with(|r| { - assert_eq!(r.payload().to_string(), "resource monitor failed"); - assert_eq!(r.key_values().get("retry").unwrap().to_bool(), Some(true)); - let value = r.key_values().get("snapshot").unwrap(); - let error = value.to_str().unwrap(); - assert!(error.starts_with(" "worker", "units" => 5i64 }); + usage.flush(); + assert_eq!(output.events.lock().unwrap().len(), 1); + assert_eq!(output.flushes.load(Ordering::SeqCst), 1); }