diff --git a/CHANGELOG.md b/CHANGELOG.md index 596655b..9e16a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. * Bump minimum supported Rust version (MSRV) to 1.91.0. +### New features + +* Add native Logforth logging macros with explicit logger instances, fine-grained levels, and structured key-value fields. + ## [0.30.1] 2026-06-03 ### Improvements diff --git a/README.md b/README.md index 418bc8c..4c3c85c 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,26 @@ fn main() { 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. +### Native Logforth macros + +Applications can use Logforth's native macros when they need fine-grained OpenTelemetry severity levels or want to avoid the `log` facade. Native macros take an explicit logger instance instead of using a second global logger: + +```rust +use logforth::append; +use logforth::record::Level; + +fn main() { + let logger = logforth::core::builder() + .dispatch(|d| d.append(append::Stdout::default())) + .build(); + + logforth::info!(logger: logger, request_id = 42_u64; "request accepted"); + logforth::log!(logger: logger, Level::Info2, "request details"); +} +``` + +The `log` facade remains the recommended API for libraries because it lets the final application choose its logging implementation. + ## Advanced Usage Configure multiple dispatches with different filters and appenders: diff --git a/core/src/kv.rs b/core/src/kv.rs index 4447f2b..9a4fdff 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -384,6 +384,16 @@ enum ValueState<'a> { Display(&'a dyn fmt::Display), } +/// Convert a value into its structured logging representation. +/// +/// Implementations are provided for primitive scalar values, strings, byte slices, [`Option`], +/// references, and [`Value`] itself. Other values can implement this trait or use the `:?` and `:%` +/// capture modifiers in Logforth's logging macros. +pub trait ToValue { + /// Convert this value into a borrowed [`Value`]. + fn to_value(&self) -> Value<'_>; +} + impl fmt::Debug for ValueState<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -512,6 +522,136 @@ impl<'a> Value<'a> { } } +impl ToValue for Value<'_> { + fn to_value(&self) -> Value<'_> { + *self + } +} + +impl ToValue for bool { + fn to_value(&self) -> Value<'_> { + Value::bool(*self) + } +} + +macro_rules! impl_to_value_signed { + ($($ty:ty),+ $(,)?) => { + $( + impl ToValue for $ty { + fn to_value(&self) -> Value<'_> { + Value::i64(*self as i64) + } + } + )+ + }; +} + +impl_to_value_signed!(i8, i16, i32, i64, isize); + +impl ToValue for i128 { + fn to_value(&self) -> Value<'_> { + Value::i128(*self) + } +} + +macro_rules! impl_to_value_unsigned { + ($($ty:ty),+ $(,)?) => { + $( + impl ToValue for $ty { + fn to_value(&self) -> Value<'_> { + Value::u64(*self as u64) + } + } + )+ + }; +} + +impl_to_value_unsigned!(u8, u16, u32, u64, usize); + +impl ToValue for u128 { + fn to_value(&self) -> Value<'_> { + Value::u128(*self) + } +} + +impl ToValue for f32 { + fn to_value(&self) -> Value<'_> { + Value::f64((*self).into()) + } +} + +impl ToValue for f64 { + fn to_value(&self) -> Value<'_> { + Value::f64(*self) + } +} + +impl ToValue for char { + fn to_value(&self) -> Value<'_> { + Value::char(*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 Cow<'_, str> { + fn to_value(&self) -> Value<'_> { + Value::str(self) + } +} + +impl ToValue for [u8] { + fn to_value(&self) -> Value<'_> { + Value::bytes(self) + } +} + +impl ToValue for Vec { + fn to_value(&self) -> Value<'_> { + Value::bytes(self) + } +} + +impl ToValue for Option +where + T: ToValue, +{ + fn to_value(&self) -> Value<'_> { + match self { + Some(value) => value.to_value(), + None => Value::none(), + } + } +} + +impl ToValue for &T +where + T: ToValue + ?Sized, +{ + fn to_value(&self) -> Value<'_> { + (*self).to_value() + } +} + +impl ToValue for &mut T +where + T: ToValue + ?Sized, +{ + fn to_value(&self) -> Value<'_> { + (**self).to_value() + } +} + /// An owned value in a key-value pair. #[derive(Debug, Clone)] pub struct ValueOwned(ValueOwnedState); diff --git a/core/src/lib.rs b/core/src/lib.rs index dc5f75f..07ad0df 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,6 +25,8 @@ pub mod layout; pub mod record; pub mod trap; +mod macros; + pub use self::append::Append; pub use self::diagnostic::Diagnostic; pub use self::filter::Filter; diff --git a/core/src/macros.rs b/core/src/macros.rs new file mode 100644 index 0000000..5cf69a8 --- /dev/null +++ b/core/src/macros.rs @@ -0,0 +1,332 @@ +// 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. + +/// Log a message at a dynamically selected level. +/// +/// A logger instance is required. The target defaults to the caller's module path and can be +/// overridden with `target:`. Structured key-value pairs precede the message and are separated +/// from it by a semicolon. Values retain their native type by default; use `:?` or `:%` to capture +/// a value with [`Debug`](std::fmt::Debug) or [`Display`](std::fmt::Display). +/// +/// Keys can be identifiers, string literals, or parenthesized string expressions. An identifier +/// without `= value` captures the variable with the same name. The message can be omitted for a +/// structured-only record by ending the fields with a semicolon. +/// +/// The message and structured fields are not evaluated when the logger disables the level and +/// target. +/// +/// # Examples +/// +/// ``` +/// use logforth_core::record::Level; +/// +/// let logger = logforth_core::builder().build(); +/// let request_id = 42_u64; +/// logforth_core::log!( +/// logger: logger, +/// target: "http", +/// Level::Info2, +/// request_id, +/// peer:% = "127.0.0.1"; +/// "request accepted" +/// ); +/// ``` +#[macro_export] +#[clippy::format_args] +macro_rules! log { + (logger: $logger:expr, target: $target:expr, $level:expr, $($args:tt)+) => {{ + $crate::__log!(logger: $logger, target: $target, target_method: target, $level, $($args)+) + }}; + (logger: $logger:expr, $level:expr, $($args:tt)+) => {{ + $crate::__log!(logger: $logger, target: ::std::module_path!(), target_method: target_static, $level, $($args)+) + }}; + ($($args:tt)*) => {{ + ::std::compile_error!("Logforth logging macros require `logger: `") + }}; +} + +/// Log a message at the fatal level. +/// +/// This macro records severity only; it does not terminate the process or flush the logger. +/// +/// # Examples +/// +/// ``` +/// let logger = logforth_core::builder().build(); +/// logforth_core::fatal!(logger: logger, "unrecoverable failure"); +/// ``` +#[macro_export] +#[clippy::format_args] +macro_rules! fatal { + (logger: $logger:expr, target: $target:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, target: $target, $crate::record::Level::Fatal, $($args)+) + }}; + (logger: $logger:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, $crate::record::Level::Fatal, $($args)+) + }}; + ($($args:tt)*) => {{ + ::std::compile_error!("Logforth logging macros require `logger: `") + }}; +} + +/// Log a message at the error level. +/// +/// # Examples +/// +/// ``` +/// let logger = logforth_core::builder().build(); +/// logforth_core::error!(logger: logger, "operation failed"); +/// ``` +#[macro_export] +#[clippy::format_args] +macro_rules! error { + (logger: $logger:expr, target: $target:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, target: $target, $crate::record::Level::Error, $($args)+) + }}; + (logger: $logger:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, $crate::record::Level::Error, $($args)+) + }}; + ($($args:tt)*) => {{ + ::std::compile_error!("Logforth logging macros require `logger: `") + }}; +} + +/// Log a message at the warn level. +/// +/// # Examples +/// +/// ``` +/// let logger = logforth_core::builder().build(); +/// logforth_core::warn!(logger: logger, "retrying operation"); +/// ``` +#[macro_export] +#[clippy::format_args] +macro_rules! warn { + (logger: $logger:expr, target: $target:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, target: $target, $crate::record::Level::Warn, $($args)+) + }}; + (logger: $logger:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, $crate::record::Level::Warn, $($args)+) + }}; + ($($args:tt)*) => {{ + ::std::compile_error!("Logforth logging macros require `logger: `") + }}; +} + +/// Log a message at the info level. +/// +/// # Examples +/// +/// ``` +/// let logger = logforth_core::builder().build(); +/// logforth_core::info!(logger: logger, user_id = 42_u64; "user connected"); +/// ``` +#[macro_export] +#[clippy::format_args] +macro_rules! info { + (logger: $logger:expr, target: $target:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, target: $target, $crate::record::Level::Info, $($args)+) + }}; + (logger: $logger:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, $crate::record::Level::Info, $($args)+) + }}; + ($($args:tt)*) => {{ + ::std::compile_error!("Logforth logging macros require `logger: `") + }}; +} + +/// Log a message at the debug level. +/// +/// # Examples +/// +/// ``` +/// let logger = logforth_core::builder().build(); +/// logforth_core::debug!(logger: logger, "state updated"); +/// ``` +#[macro_export] +#[clippy::format_args] +macro_rules! debug { + (logger: $logger:expr, target: $target:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, target: $target, $crate::record::Level::Debug, $($args)+) + }}; + (logger: $logger:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, $crate::record::Level::Debug, $($args)+) + }}; + ($($args:tt)*) => {{ + ::std::compile_error!("Logforth logging macros require `logger: `") + }}; +} + +/// Log a message at the trace level. +/// +/// # Examples +/// +/// ``` +/// let logger = logforth_core::builder().build(); +/// logforth_core::trace!(logger: logger, "entered operation"); +/// ``` +#[macro_export] +#[clippy::format_args] +macro_rules! trace { + (logger: $logger:expr, target: $target:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, target: $target, $crate::record::Level::Trace, $($args)+) + }}; + (logger: $logger:expr, $($args:tt)+) => {{ + $crate::log!(logger: $logger, $crate::record::Level::Trace, $($args)+) + }}; + ($($args:tt)*) => {{ + ::std::compile_error!("Logforth logging macros require `logger: `") + }}; +} + +/// Determine whether a level and target are enabled for a logger. +/// +/// The target defaults to the caller's module path. +/// +/// # Examples +/// +/// ``` +/// use logforth_core::record::Level; +/// +/// let logger = logforth_core::builder().build(); +/// if logforth_core::log_enabled!(logger: logger, Level::Debug) { +/// // Perform expensive diagnostic work. +/// } +/// ``` +#[macro_export] +macro_rules! log_enabled { + (logger: $logger:expr, target: $target:expr, $level:expr) => {{ + let __logforth_logger: &$crate::Logger = &$logger; + let __logforth_level = $level; + let __logforth_target = $target; + let __logforth_criteria = $crate::record::FilterCriteria::builder() + .level(__logforth_level) + .target(__logforth_target) + .build(); + __logforth_logger.enabled(&__logforth_criteria) + }}; + (logger: $logger:expr, $level:expr) => {{ + $crate::log_enabled!(logger: $logger, target: ::std::module_path!(), $level) + }}; + ($($args:tt)*) => {{ + ::std::compile_error!("Logforth logging macros require `logger: `") + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __log { + (logger: $logger:expr, target: $target:expr, target_method: $target_method:ident, $level:expr, $($key:tt $(:$capture:tt)? $(= $value:expr)?),+; $($message:tt)+) => {{ + let __logforth_logger: &$crate::Logger = &$logger; + let __logforth_level = $level; + let __logforth_target = $target; + let __logforth_criteria = $crate::record::FilterCriteria::builder() + .level(__logforth_level) + .target(__logforth_target) + .build(); + if __logforth_logger.enabled(&__logforth_criteria) { + __logforth_logger.log( + &$crate::record::Record::builder() + .level(__logforth_level) + .$target_method(__logforth_target) + .module_path_static(::std::module_path!()) + .file_static(::std::file!()) + .line(::std::option::Option::Some(::std::line!())) + .column(::std::option::Option::Some(::std::column!())) + .payload(::std::format_args!($($message)+)) + .key_values(&[ + $(( + $crate::__log_key!($key), + $crate::__log_value!($key $(:$capture)? $(= $value)?), + )),+ + ][..]) + .build(), + ); + } + }}; + (logger: $logger:expr, target: $target:expr, target_method: $target_method:ident, $level:expr, $($key:tt $(:$capture:tt)? $(= $value:expr)?),+;) => {{ + $crate::__log!(logger: $logger, target: $target, target_method: $target_method, $level, $($key $(:$capture)? $(= $value)?),+; "") + }}; + (logger: $logger:expr, target: $target:expr, target_method: $target_method:ident, $level:expr, $($message:tt)+) => {{ + let __logforth_logger: &$crate::Logger = &$logger; + let __logforth_level = $level; + let __logforth_target = $target; + let __logforth_criteria = $crate::record::FilterCriteria::builder() + .level(__logforth_level) + .target(__logforth_target) + .build(); + if __logforth_logger.enabled(&__logforth_criteria) { + __logforth_logger.log( + &$crate::record::Record::builder() + .level(__logforth_level) + .$target_method(__logforth_target) + .module_path_static(::std::module_path!()) + .file_static(::std::file!()) + .line(::std::option::Option::Some(::std::line!())) + .column(::std::option::Option::Some(::std::column!())) + .payload(::std::format_args!($($message)+)) + .build(), + ); + } + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __log_key { + ($key:ident) => { + $crate::kv::Key::new(::std::stringify!($key)) + }; + ($key:literal) => { + $crate::kv::Key::new($key) + }; + (($key:expr)) => { + $crate::kv::Key::borrowed($key) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __log_value { + ($key:tt = $value:expr) => { + $crate::kv::ToValue::to_value(&$value) + }; + ($key:tt :? = $value:expr) => { + $crate::kv::Value::debug(&$value) + }; + ($key:tt :debug = $value:expr) => { + $crate::kv::Value::debug(&$value) + }; + ($key:tt :% = $value:expr) => { + $crate::kv::Value::display(&$value) + }; + ($key:tt :display = $value:expr) => { + $crate::kv::Value::display(&$value) + }; + ($key:ident) => { + $crate::kv::ToValue::to_value(&$key) + }; + ($key:ident :?) => { + $crate::kv::Value::debug(&$key) + }; + ($key:ident :debug) => { + $crate::kv::Value::debug(&$key) + }; + ($key:ident :%) => { + $crate::kv::Value::display(&$key) + }; + ($key:ident :display) => { + $crate::kv::Value::display(&$key) + }; +} diff --git a/core/tests/macros.rs b/core/tests/macros.rs new file mode 100644 index 0000000..49ec0ec --- /dev/null +++ b/core/tests/macros.rs @@ -0,0 +1,364 @@ +// 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::fmt; +use std::sync::Arc; +use std::sync::Mutex; + +use logforth_core::Append; +use logforth_core::Diagnostic; +use logforth_core::Error; +use logforth_core::Logger; +use logforth_core::kv::KeyView; +use logforth_core::kv::ToValue; +use logforth_core::kv::Value; +use logforth_core::kv::ValueView; +use logforth_core::record::Level; +use logforth_core::record::LevelFilter; +use logforth_core::record::Record; + +#[derive(Debug, PartialEq)] +enum CapturedValue { + None, + Bool(bool), + I64(i64), + U64(u64), + F64(f64), + I128(i128), + U128(u128), + Char(char), + String(String), + Bytes(Vec), + Debug(String), + Display(String), + Other(String), +} + +impl CapturedValue { + fn from_view(value: ValueView<'_>) -> Self { + match value { + ValueView::None => CapturedValue::None, + ValueView::BorrowedStr(value) | ValueView::StaticStr(value) => { + CapturedValue::String(value.to_owned()) + } + ValueView::Bytes(value) => CapturedValue::Bytes(value.to_vec()), + ValueView::Bool(value) => CapturedValue::Bool(value), + ValueView::I64(value) => CapturedValue::I64(value), + ValueView::U64(value) => CapturedValue::U64(value), + ValueView::F64(value) => CapturedValue::F64(value), + ValueView::I128(value) => CapturedValue::I128(value), + ValueView::U128(value) => CapturedValue::U128(value), + ValueView::Char(value) => CapturedValue::Char(value), + ValueView::Debug(value) => CapturedValue::Debug(format!("{value:?}")), + ValueView::Display(value) => CapturedValue::Display(format!("{value}")), + value => CapturedValue::Other(format!("{value:?}")), + } + } +} + +#[derive(Debug, PartialEq)] +struct CapturedRecord { + level: Level, + target: String, + target_static: Option, + module_path: Option, + file: Option, + line: Option, + column: Option, + payload: String, + key_values: Vec<(String, CapturedValue)>, +} + +impl CapturedRecord { + fn from_record(record: &Record<'_>) -> Result { + let mut key_values = Vec::new(); + record + .key_values() + .visit(&mut |key: KeyView<'_>, value: ValueView<'_>| { + key_values.push((key.as_str().to_owned(), CapturedValue::from_view(value))); + Ok(()) + })?; + + Ok(Self { + level: record.level(), + target: record.target().to_owned(), + target_static: record.target_static().map(str::to_owned), + module_path: record.module_path().map(str::to_owned), + file: record.file().map(str::to_owned), + line: record.line(), + column: record.column(), + payload: record.payload().to_string(), + key_values, + }) + } +} + +#[derive(Clone, Debug, Default)] +struct Capture { + records: Arc>>, +} + +impl Capture { + fn take(&self) -> Vec { + std::mem::take(&mut *self.records.lock().unwrap()) + } +} + +impl Append for Capture { + fn append(&self, record: &Record<'_>, _: &[Box]) -> Result<(), Error> { + self.records + .lock() + .unwrap() + .push(CapturedRecord::from_record(record)?); + Ok(()) + } + + fn flush(&self) -> Result<(), Error> { + Ok(()) + } +} + +fn make_logger(capture: Capture) -> Logger { + logforth_core::builder() + .dispatch(|dispatch| dispatch.append(capture)) + .build() +} + +fn make_filtered_logger(capture: Capture) -> Logger { + logforth_core::builder() + .dispatch(|dispatch| { + dispatch + .filter(LevelFilter::MoreSevereEqual(Level::Error)) + .append(capture) + }) + .build() +} + +#[test] +fn captures_fine_grained_level_metadata_and_typed_fields() { + struct DebugOnly(u8); + + impl fmt::Debug for DebugOnly { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DebugOnly({})", self.0) + } + } + + struct DisplayOnly(u8); + + impl fmt::Display for DisplayOnly { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "display({})", self.0) + } + } + + struct CustomValue(u8); + + impl ToValue for CustomValue { + fn to_value(&self) -> Value<'_> { + Value::u64(self.0.into()) + } + } + + let capture = Capture::default(); + let logger = make_logger(capture.clone()); + let shorthand = 7_u32; + let text = String::from("hello"); + let absent: Option = None; + let dynamic_key = String::from("dynamic.key"); + let expected_line = line!() + 1; + logforth_core::log!( + logger: logger, + target: "custom.target", + Level::Info2, + shorthand, + signed = -2_i32, + unsigned = 3_usize, + wide_signed = -4_i128, + wide_unsigned = 5_u128, + float = 1.5_f32, + character = 'x', + text = text, + absent = absent, + bytes = &b"bytes"[..], + "literal.key" = true, + (dynamic_key.as_str()) = 9_u8, + debug:? = DebugOnly(10), + display:% = DisplayOnly(11), + custom = CustomValue(13); + "accepted {}", + 12 + ); + + let records = capture.take(); + assert_eq!(records.len(), 1); + let record = &records[0]; + assert_eq!(record.level, Level::Info2); + assert_eq!(record.target, "custom.target"); + assert_eq!(record.target_static, None); + assert_eq!(record.module_path.as_deref(), Some("macros")); + assert!( + std::path::Path::new(record.file.as_deref().unwrap()) + .ends_with(std::path::Path::new("tests").join("macros.rs")) + ); + assert_eq!(record.line, Some(expected_line)); + assert!(record.column.unwrap() > 0); + assert_eq!(record.payload, "accepted 12"); + assert_eq!( + record.key_values, + [ + ("shorthand".to_owned(), CapturedValue::U64(7)), + ("signed".to_owned(), CapturedValue::I64(-2)), + ("unsigned".to_owned(), CapturedValue::U64(3)), + ("wide_signed".to_owned(), CapturedValue::I128(-4)), + ("wide_unsigned".to_owned(), CapturedValue::U128(5)), + ("float".to_owned(), CapturedValue::F64(1.5)), + ("character".to_owned(), CapturedValue::Char('x')), + ("text".to_owned(), CapturedValue::String("hello".to_owned())), + ("absent".to_owned(), CapturedValue::None), + ("bytes".to_owned(), CapturedValue::Bytes(b"bytes".to_vec())), + ("literal.key".to_owned(), CapturedValue::Bool(true)), + ("dynamic.key".to_owned(), CapturedValue::U64(9)), + ( + "debug".to_owned(), + CapturedValue::Debug("DebugOnly(10)".to_owned()) + ), + ( + "display".to_owned(), + CapturedValue::Display("display(11)".to_owned()) + ), + ("custom".to_owned(), CapturedValue::U64(13)), + ] + ); +} + +#[test] +fn convenience_macros_cover_standard_levels() { + let capture = Capture::default(); + let logger = Arc::new(make_logger(capture.clone())); + + logforth_core::fatal!(logger: logger, "fatal"); + logforth_core::error!(logger: logger, target: "error.target", "error"); + logforth_core::warn!(logger: &logger, "warn"); + logforth_core::info!(logger: logger, "info"); + logforth_core::debug!(logger: logger, "debug"); + logforth_core::trace!(logger: logger, "trace"); + + let records = capture.take(); + assert_eq!( + records + .iter() + .map(|record| (record.level, record.payload.as_str())) + .collect::>(), + [ + (Level::Fatal, "fatal"), + (Level::Error, "error"), + (Level::Warn, "warn"), + (Level::Info, "info"), + (Level::Debug, "debug"), + (Level::Trace, "trace"), + ] + ); + assert_eq!(records[1].target, "error.target"); + assert!( + records + .iter() + .enumerate() + .all(|(index, record)| index == 1 || record.target == "macros") + ); + assert!( + records + .iter() + .enumerate() + .all(|(index, record)| index == 1 || record.target_static.as_deref() == Some("macros")) + ); + assert_eq!(records[1].target_static, None); +} + +#[test] +fn disabled_records_do_not_evaluate_payload_or_fields() { + let capture = Capture::default(); + let logger = make_filtered_logger(capture.clone()); + let evaluations = Cell::new(0); + let expensive = || { + evaluations.set(evaluations.get() + 1); + 42_u64 + }; + + logforth_core::info!( + logger: logger, + value = expensive(); + "value is {}", + expensive() + ); + + assert_eq!(evaluations.get(), 0); + assert!(capture.take().is_empty()); + assert!(!logforth_core::log_enabled!(logger: logger, Level::Info)); + assert!(logforth_core::log_enabled!( + logger: logger, + target: "custom.target", + Level::Error + )); +} + +#[test] +fn macro_inputs_are_evaluated_once() { + let capture = Capture::default(); + let logger = make_logger(capture.clone()); + let logger_evaluations = Cell::new(0); + let level_evaluations = Cell::new(0); + let target_evaluations = Cell::new(0); + + let logger_expression = || { + logger_evaluations.set(logger_evaluations.get() + 1); + &logger + }; + let level_expression = || { + level_evaluations.set(level_evaluations.get() + 1); + Level::Debug3 + }; + let target_expression = || { + target_evaluations.set(target_evaluations.get() + 1); + "evaluated.once" + }; + + logforth_core::log!( + logger: logger_expression(), + target: target_expression(), + level_expression(), + "once" + ); + + assert_eq!(logger_evaluations.get(), 1); + assert_eq!(level_evaluations.get(), 1); + assert_eq!(target_evaluations.get(), 1); + assert_eq!(capture.take()[0].level, Level::Debug3); +} + +#[test] +fn structured_record_may_omit_message() { + let capture = Capture::default(); + let logger = make_logger(capture.clone()); + + logforth_core::info!(logger: logger, answer = 42_u64;); + + let records = capture.take(); + assert_eq!(records[0].payload, ""); + assert_eq!( + records[0].key_values, + [("answer".to_owned(), CapturedValue::U64(42))] + ); +} diff --git a/logforth/src/lib.rs b/logforth/src/lib.rs index 668dcd5..a872e67 100644 --- a/logforth/src/lib.rs +++ b/logforth/src/lib.rs @@ -60,6 +60,21 @@ //! log::info!("Info message."); //! ``` //! +//! Applications that need Logforth's fine-grained severity levels can keep a [`core::Logger`] +//! instance and use the native macros directly, without installing a second global logger: +//! +//! ``` +//! use logforth::append; +//! use logforth::record::Level; +//! +//! let logger = logforth::core::builder() +//! .dispatch(|d| d.append(append::Stdout::default())) +//! .build(); +//! +//! logforth::info!(logger: logger, request_id = 42_u64; "request accepted"); +//! logforth::log!(logger: logger, Level::Info2, "request details"); +//! ``` +//! //! See the [README] file for more details and examples. //! //! [README]: https://github.com/fast/logforth?tab=readme-ov-file @@ -69,11 +84,19 @@ pub use logforth_core::Error; pub use logforth_core::append::Append; +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::log_enabled; pub use logforth_core::record; +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_macros.rs b/logforth/tests/native_macros.rs new file mode 100644 index 0000000..5fd2467 --- /dev/null +++ b/logforth/tests/native_macros.rs @@ -0,0 +1,24 @@ +// 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::record::Level; + +#[test] +fn native_macros_are_reexported() { + let logger = logforth::core::builder().build(); + + logforth::info!(logger: logger, answer = 42_u64; "hello"); + logforth::log!(logger: logger, Level::Info2, "fine-grained"); + assert!(!logforth::log_enabled!(logger: logger, Level::Info)); +}