Skip to content

feat: add native logging macros - #239

Draft
tisonkun wants to merge 2 commits into
mainfrom
codex/native-log-macros
Draft

feat: add native logging macros#239
tisonkun wants to merge 2 commits into
mainfrom
codex/native-log-macros

Conversation

@tisonkun

@tisonkun tisonkun commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Closes #205.

Summary

  • add instance-first native logging macros: log!, fatal!, error!, warn!, info!, debug!, trace!, and log_enabled!
  • support every Logforth severity, including the fine-grained OpenTelemetry levels, through the generic log! entry point
  • preserve typed structured fields with shorthand keys, expression keys, :? debug capture, and :% display capture
  • evaluate the logger, level, target, message arguments, and fields at most once, and skip message/field evaluation when the record is disabled
  • capture target/module/file/line/column metadata and re-export the macros from the logforth facade
  • document when applications should use the native API while continuing to recommend the log facade for libraries

Why this design

Explicit logger, not a second global singleton

The native macros require logger::

logforth::info!(
    logger: logger,
    request_id,
    elapsed_ms = elapsed.as_millis();
    "request completed"
);

PR #226 deliberately removed the global logger from logforth-core. Reintroducing one through macros would create two independent global configuration paths: Logforth's and the log facade's. It would also bring back the singleton, test-isolation, and context-propagation problems that motivated instance-oriented logging APIs across the ecosystem.

The log facade now accepts an explicit logger: argument, but its API still exposes only five levels and its global compile-time/runtime maximum applies even to explicit loggers. Logforth's native macros therefore stay instance-first and use each Logger's filter as the authority.

One generic level macro plus familiar conveniences

logforth::log!(
    logger: logger,
    target: "audit",
    Level::Info2,
    actor = user_id;
    "permission granted"
);

The six convenience macros cover the common path. The generic log! accepts a level expression and makes all 24 Logforth/OpenTelemetry severities usable without adding trace2!, trace3!, and so on.

This shape follows the durable pattern used by Go's slog.Logger.Log, Python's Logger.log, and Log4j's Logger.log(Level, ...): fixed-level conveniences paired with a generic level entry point. It also directly addresses repeated Rust requests for Notice, Critical, Fatal, or otherwise extensible levels:

fatal! is only a severity. It deliberately does not terminate the process or imply a flush.

Structured and lazy by construction

The field grammar stays close to log's key-value syntax to reduce migration cost:

logforth::info!(
    logger: logger,
    user_id,
    "http.status_code" = status,
    (dynamic_key) = value,
    error:? = error,
    latency:% = latency;
    "request failed"
);

Plain values use the public ToValue conversion trait and retain supported scalar types instead of becoming formatted strings. :? and :% are explicit escape hatches for Debug and Display. The macro checks Logger::enabled before formatting the message or evaluating fields.

The macro also captures source metadata and supports structured-only records with an empty message.

Declarative macros in core, with no feature gate

The implementation uses hygienic macro_rules! macros in logforth-core, then re-exports them from logforth.

This avoids:

  • a proc-macro crate and its compile-time/dependency cost
  • an optional feature whose presence would split the public API across dependency graphs
  • another global or compile-time maximum-level mechanism that could override per-Logger filtering

The macros are always available, introduce no new dependency, and use $crate paths so facade re-exports remain hygienic.

Ecosystem research

The API was compared with:

The recurring Rust pain points considered here were:

  1. the five-level ceiling and lossy mappings to syslog, cloud providers, and OpenTelemetry;
  2. global singleton friction in tests, libraries, and contextual logging;
  3. structured-field ergonomics and accidental stringification;
  4. macro expansion/dependency cost and compile-time filtering that leaks through additive Cargo features.

Relevant discussions include rust-lang/log#149, #334, #343, #388, #541, #708 and tokio-rs/tracing#2081, #3585.

API surface

  • log!(logger: ..., [target: ...,] level, [fields;] "message", args...)
  • fatal!/error!/warn!/info!/debug!/trace!(logger: ..., [target: ...,] [fields;] "message", args...)
  • log_enabled!(logger: ..., [target: ...,] level)
  • ToValue for typed conversion of supported Rust values

Logger, target, and level expressions are each evaluated once. The API accepts Logger, &Logger, and dereferenceable owners such as Arc<Logger>.

Non-goals

  • replacing the log facade for reusable libraries
  • adding an implicit Logforth global logger
  • adding convenience macros for every fine-grained severity
  • adding a Cargo-feature-driven compile-time maximum level
  • providing tracing spans or implicit context propagation

Those can be evaluated independently without constraining this base logging API.

Validation

  • cargo x test
  • workspace nightly Clippy for all targets and features with warnings denied
  • rustfmt, Taplo, typos, and license-header checks
  • targeted tests and rustdoc on Rust 1.91.0
  • cargo-semver-checks for logforth-core and logforth against origin/main
  • facade re-export, metadata, typed fields, target lifetimes, disabled evaluation, and evaluate-once regression tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add log macros as APIs

1 participant