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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file.
### Breaking changes

* Bump minimum supported Rust version (MSRV) to 1.91.0.
* Replace `LoggerBuilder` with `LoggerProviderBuilder`; build a `LoggerProvider` first and obtain lightweight `Logger` handles from it.
* Replace the prefilter-only `FilterCriteria` with `Metadata`, which is shared by prefiltering and the resulting `Record`.

### New features

Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,23 @@ use logforth::append;
use logforth::record::Level;

fn main() {
let logger = logforth::core::builder()
let provider = logforth::core::builder()
.dispatch(|d| d.append(append::Stdout::default()))
.build();
let logger = provider.logger();

logforth::info!(logger, request_id = 42_u64; "request accepted");
logforth::log!(logger, Level::Info2, "request details");
}
```

The logger is the first argument, following the same instance-first convention as `slog`. An unnamed logger uses the call-site module path as its target. A dedicated logger can instead carry one stable name, which keeps target-based `RustLogFilter` directives without repeating `target:` at every call site:
The logger is the first argument, following the same instance-first convention as `slog`. A `LoggerProvider` owns the configured dispatches and creates lightweight logger handles. An unnamed logger uses the call-site module path as its target. A logger can instead carry one stable name, which keeps target-based `RustLogFilter` directives without repeating `target:` at every call site:

```rust
let metering = logforth::core::builder()
.name("metering")
let provider = logforth::core::builder()
.dispatch(|d| d.append(logforth::append::Stdout::default()))
.build();
let metering = provider.named_logger("metering");

logforth::info!(
metering,
Expand All @@ -77,7 +78,7 @@ logforth::info!(
);
```

The logger name is a stable channel or source scope, so a directive such as `RUST_LOG=metering=info` keeps working. Event kinds, tenant IDs, and other varying classifications remain structured fields. The record still carries the call-site module path separately.
The logger name is a stable filtering namespace, so a directive such as `RUST_LOG=metering=info` keeps working. Event kinds, tenant IDs, and other varying classifications remain structured fields. The record still carries the call-site module path separately.

The macros check the logger before evaluating the message or its fields, so an extra enabled check is unnecessary. The `log` facade remains the recommended API for libraries because it lets the final application choose its logging implementation.

Expand Down
3 changes: 2 additions & 1 deletion appenders/file/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
//!
//! let logger = logforth_core::builder()
//! .dispatch(|d| d.filter(LevelFilter::All).append(rolling))
//! .build();
//! .build()
//! .logger();
//! ```

#![cfg_attr(docsrs, feature(doc_cfg))]
Expand Down
3 changes: 2 additions & 1 deletion appenders/syslog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
//!
//! let logger = logforth_core::builder()
//! .dispatch(|d| d.filter(LevelFilter::All).append(append))
//! .build();
//! .build()
//! .logger();
//! ```

#![cfg_attr(docsrs, feature(doc_cfg))]
Expand Down
32 changes: 17 additions & 15 deletions bridges/log/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::sync::Arc;
use log::Metadata;
use log::Record;
use logforth_core::Logger;
use logforth_core::record::FilterCriteria;
use logforth_core::record::Metadata as LogforthMetadata;

/// Adapter to use a `logforth` logger instance as a `log` crate logger.
#[derive(Debug)]
Expand Down Expand Up @@ -63,36 +63,38 @@ impl log::Log for LogBridge {
}

fn forward_enabled(logger: &Logger, metadata: &Metadata) -> bool {
let criteria = FilterCriteria::builder()
let metadata = LogforthMetadata::builder()
.target(metadata.target())
.level(level_to_level(metadata.level()))
.build();

Logger::enabled(logger, &criteria)
Logger::enabled(logger, &metadata)
}

fn forward_log(logger: &Logger, record: &Record) {
if !forward_enabled(logger, record.metadata()) {
return;
}

// basic fields
let mut builder = logforth_core::record::Record::builder()
let mut metadata = LogforthMetadata::builder()
.level(level_to_level(record.level()))
.target(record.target())
.line(record.line());

// optional static fields
builder = if let Some(module_path) = record.module_path_static() {
builder.module_path_static(module_path)
metadata = if let Some(module_path) = record.module_path_static() {
metadata.module_path_static(module_path)
} else {
builder.module_path(record.module_path())
metadata.module_path(record.module_path())
};
builder = if let Some(file) = record.file_static() {
builder.file_static(file)
metadata = if let Some(file) = record.file_static() {
metadata.file_static(file)
} else {
builder.file(record.file())
metadata.file(record.file())
};
let metadata = metadata.build();

if !Logger::enabled(logger, &metadata) {
return;
}

let mut builder = logforth_core::record::Record::builder().metadata(metadata);

// payload
builder = builder.payload(*record.args());
Expand Down
19 changes: 7 additions & 12 deletions core/src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
use std::fmt;

use crate::Diagnostic;
use crate::record::FilterCriteria;
use crate::record::LevelFilter;
use crate::record::Metadata;
use crate::record::Record;

/// The result of a filter check.
Expand All @@ -34,28 +34,23 @@ pub enum FilterResult {

/// A filter that can be applied to log records.
pub trait Filter: fmt::Debug + Send + Sync + 'static {
/// Prefilter a record using criteria available before the complete record is constructed.
/// Prefilter a record using metadata available before the complete record is constructed.
///
/// A filter that needs the message or structured fields to decide must return
/// [`FilterResult::Neutral`] here and make that decision in [`Filter::matches`]. Returning
/// [`FilterResult::Reject`] promises that every record with these criteria can be rejected
/// [`FilterResult::Reject`] promises that every record with this metadata can be rejected
/// without constructing it.
fn enabled(&self, criteria: &FilterCriteria, diags: &[Box<dyn Diagnostic>]) -> FilterResult;
fn enabled(&self, metadata: &Metadata, diags: &[Box<dyn Diagnostic>]) -> FilterResult;

/// Whether the record is filtered.
fn matches(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> FilterResult {
let criteria = FilterCriteria::builder()
.level(record.level())
.target(record.target())
.build();

self.enabled(&criteria, diags)
self.enabled(record.metadata(), diags)
}
}

impl Filter for LevelFilter {
fn enabled(&self, criteria: &FilterCriteria, _: &[Box<dyn Diagnostic>]) -> FilterResult {
if self.test(criteria.level()) {
fn enabled(&self, metadata: &Metadata, _: &[Box<dyn Diagnostic>]) -> FilterResult {
if self.test(metadata.level()) {
FilterResult::Neutral
} else {
FilterResult::Reject
Expand Down
88 changes: 30 additions & 58 deletions core/src/logger/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,89 +15,56 @@
use crate::Append;
use crate::Diagnostic;
use crate::Filter;
use crate::Logger;
use crate::LoggerProvider;
use crate::logger::log_impl::Dispatch;

/// Create a new empty [`LoggerBuilder`] instance for configuring log dispatching.
/// Create a new empty [`LoggerProviderBuilder`] for configuring log dispatching.
///
/// # Examples
///
/// ```
/// use logforth_core::append;
///
/// let logger = logforth_core::builder()
/// let provider = logforth_core::builder()
/// .dispatch(|d| d.append(append::Stderr::default()))
/// .build();
/// let logger = provider.logger();
/// ```
pub fn builder() -> LoggerBuilder {
LoggerBuilder {
name: None,
dispatches: vec![],
}
pub fn builder() -> LoggerProviderBuilder {
LoggerProviderBuilder { dispatches: vec![] }
}

/// A builder for configuring log dispatching.
/// A builder for configuring a [`LoggerProvider`].
///
/// # Examples
///
/// ```
/// use logforth_core::append;
///
/// let logger = logforth_core::builder()
/// let provider = logforth_core::builder()
/// .dispatch(|d| d.append(append::Stdout::default()))
/// .build();
/// let logger = provider.logger();
/// ```
#[must_use = "call `build` to construct a logger instance"]
#[must_use = "call `build` to construct a logger provider"]
#[derive(Debug)]
pub struct LoggerBuilder {
// optional stable logger name
name: Option<&'static str>,

pub struct LoggerProviderBuilder {
// stashed dispatches
dispatches: Vec<Dispatch>,
}

impl LoggerBuilder {
/// Assign a stable name to the logger.
///
/// Native logging macros use this name as the record target. An unnamed logger instead uses
/// the call-site module path. The source module is recorded separately in both cases.
///
/// A name is useful for a dedicated event channel such as `metering` or `audit`: routing is
/// selected by the logger instance, while target-based filters such as `RustLogFilter` can
/// retain the same stable namespace. Per-event classifications should remain structured
/// fields rather than logger names.
///
/// Names must be static because they describe a bounded, application-defined namespace rather
/// than dynamic event data.
///
/// # Examples
///
/// ```
/// use logforth_core::append;
///
/// let metering = logforth_core::builder()
/// .name("metering")
/// .dispatch(|d| d.append(append::Stdout::default()))
/// .build();
///
/// assert_eq!(metering.name(), Some("metering"));
/// ```
pub fn name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}

/// Register a new dispatch with the [`LoggerBuilder`].
impl LoggerProviderBuilder {
/// Register a new dispatch with the [`LoggerProviderBuilder`].
///
/// # Examples
///
/// ```
/// use logforth_core::append;
///
/// let logger = logforth_core::builder()
/// let provider = logforth_core::builder()
/// .dispatch(|d| d.append(append::Stderr::default()))
/// .build();
/// let logger = provider.logger();
/// ```
pub fn dispatch<F>(mut self, f: F) -> Self
where
Expand All @@ -107,21 +74,22 @@ impl LoggerBuilder {
self
}

/// Build the [`Logger`].
/// Build the [`LoggerProvider`].
///
/// # Examples
///
/// ```
/// use logforth_core::record::Record;
///
/// let l = logforth_core::builder().build();
/// let r = Record::builder()
/// let provider = logforth_core::builder().build();
/// let logger = provider.logger();
/// let record = Record::builder()
/// .payload(format_args!("hello world!"))
/// .build();
/// l.log(&r);
/// logger.log(&record);
/// ```
pub fn build(self) -> Logger {
Logger::new(self.name, self.dispatches)
pub fn build(self) -> LoggerProvider {
LoggerProvider::new(self.dispatches)
}
}

Expand All @@ -134,12 +102,13 @@ impl LoggerBuilder {
/// use logforth_core::record::Level;
/// use logforth_core::record::LevelFilter;
///
/// let logger = logforth_core::builder()
/// let provider = logforth_core::builder()
/// .dispatch(|d| {
/// d.filter(LevelFilter::MoreSevereEqual(Level::Info))
/// .append(append::Stdout::default())
/// })
/// .build();
/// let logger = provider.logger();
/// ```
#[derive(Debug)]
pub struct DispatchBuilder<const APPEND: bool> {
Expand All @@ -166,12 +135,13 @@ impl DispatchBuilder<false> {
/// use logforth_core::record::Level;
/// use logforth_core::record::LevelFilter;
///
/// let logger = logforth_core::builder()
/// let provider = logforth_core::builder()
/// .dispatch(|d| {
/// d.filter(LevelFilter::MoreSevereEqual(Level::Error))
/// .append(append::Stderr::default())
/// })
/// .build();
/// let logger = provider.logger();
/// ```
pub fn filter(mut self, filter: impl Into<Box<dyn Filter>>) -> Self {
self.filters.push(filter.into());
Expand All @@ -188,13 +158,14 @@ impl DispatchBuilder<false> {
/// use logforth_core::record::Level;
/// use logforth_core::record::LevelFilter;
///
/// let logger = logforth_core::builder()
/// let provider = logforth_core::builder()
/// .dispatch(|d| {
/// d.filter(LevelFilter::MoreSevereEqual(Level::Error))
/// .diagnostic(diagnostic::ThreadLocalDiagnostic::default())
/// .append(append::Stderr::default())
/// })
/// .build();
/// let logger = provider.logger();
/// ```
pub fn diagnostic(mut self, diagnostic: impl Into<Box<dyn Diagnostic>>) -> Self {
self.diagnostics.push(diagnostic.into());
Expand All @@ -216,9 +187,10 @@ impl<const APPEND: bool> DispatchBuilder<APPEND> {
/// ```
/// use logforth_core::append;
///
/// let logger = logforth_core::builder()
/// let provider = logforth_core::builder()
/// .dispatch(|d| d.append(append::Stdout::default()))
/// .build();
/// let logger = provider.logger();
/// ```
pub fn append(mut self, append: impl Into<Box<dyn Append>>) -> DispatchBuilder<true> {
self.appends.push(append.into());
Expand Down
Loading