Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
140 changes: 140 additions & 0 deletions core/src/kv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<u8> {
fn to_value(&self) -> Value<'_> {
Value::bytes(self)
}
}

impl<T> ToValue for Option<T>
where
T: ToValue,
{
fn to_value(&self) -> Value<'_> {
match self {
Some(value) => value.to_value(),
None => Value::none(),
}
}
}

impl<T> ToValue for &T
where
T: ToValue + ?Sized,
{
fn to_value(&self) -> Value<'_> {
(*self).to_value()
}
}

impl<T> 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);
Expand Down
2 changes: 2 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading