feat: add native logging macros - #239
Draft
tisonkun wants to merge 5 commits into
Draft
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #205.
Summary
log!,fatal!,error!,warn!,info!,debug!, andtrace!log's optionallogger:control syntaxLoggerBuilder::nameRecord::module_patheven when a logger has a different nametarget:branch while preserving target-basedRustLogFilterdirectives andlogbridge compatibilitylog!entry point:?debug capture, and:%display captureLogger::enabledas a low-level, conservative prefilter and deliberately do not expose alog_enabled!macrologforthfacadelogfacade for reusable librariesAPI
The common case uses an unnamed logger. Its target is the call-site module path:
A stable application channel uses a dedicated named logger:
Here
target == "metering", whilemodule_pathstill identifies the module containing the macro invocation. A directive such asmetering=infotherefore remains valid forRustLogFilter.The first argument accepts a
Logger,&Logger, or a dereferenceable owner such asArc<Logger>.Why this design
The logger is positional because it is required
Most logging APIs bind the logger as the method receiver:
logger.Info(...)logger.info(...)logger.info(...)logger.info(...)/logger.Info(...)logger.LogInformation(...)Rust declarative macros cannot be invoked as methods. The closest Rust representation is therefore a required first argument, which is also the convention established by
slog::info!(logger, ...).The
logcrate'slogger:form has a different purpose: it is an optional override for an API whose default is the global logger. Logforth removed its core global logger in #226, so retaining an override-shaped pseudo-keyword would add syntax without communicating a choice.Target is a logger scope; module path is source location
targethas historically carried two meanings in Rust logging: source identity and an ad hoc routing tag. Treating it as unconditionally equal tomodule_path!()loses legitimate stable channels, while allowing an arbitrarytarget:on every call encourages dynamic application data to leak into a filtering namespace.This PR separates the concepts:
Record::module_pathis always the actual Rust source module captured at the call site.Record::target, preserving ordinaryRUST_LOG=my_crate::module=debugbehavior.Record::target. The name is configured once on the logger, not repeated on events.logbridge retain their original target, so existing facade users are unaffected.This matches the dominant instance-oriented model. Log4j, Python, and .NET attach a category/name to a logger instance; Zap provides
Logger.Named; OpenTelemetry binds a stable instrumentation scope when a logger is obtained. Per-event meaning remains in structured attributes or an event identifier/name.Logger names take
&'static strdeliberately: they define a bounded application namespace such asmetering,audit, orquery, not tenant IDs or other high-cardinality data.A dedicated logger routes a dedicated channel
A metering or audit stream usually has a different reliability policy and destination from diagnostic logs. With an explicit logger API, selecting the logger instance is already the earliest and cheapest routing decision:
The stable logger name remains useful for filtering, layout output, and migration compatibility, but it is not required to discover the destination: the dedicated logger owns that dispatch graph.
This is preferable to the alternatives:
metering_kinddescribes the event and should still be emitted, but routing on it happens after the complete record and fields have been constructed. It also makes metadata prefiltering impossible with the currentFilterCriteriacontract.target:is early enough to filter, but repeats an untyped string at every call and can drift independently of the logger's dispatch topology.If a dedicated event must also reach general logs, that is expressed explicitly by adding another dispatch/appender to that logger. Routing policy remains at construction time rather than hidden in each call site.
RustLogFilter compatibility
No directive syntax changes are required:
my_crate::module=debugmatches the call-site module targetmetering=infomatches the logger namelogfacade throughLogBridge: the originallog::Record::target()is preservedThe test suite exercises a named native logger with
off,metering=info, proving that debug is rejected during prefiltering and info is accepted. This provides an incremental migration path: applications can move dedicated target calls to a named logger without immediately rewriting their filter specifications.No
log_enabled!application APIThe native macro itself performs metadata prefiltering before evaluating:
ToValueconversionsRecordThis makes the normal call a single operation with lazy arguments.
A separate enabled probe creates a two-step check-then-emit protocol. Its result may change before emission, it duplicates filtering work, and it cannot precisely represent filters that require the complete record. The ecosystem evidence is explicit:
slog::Drain::is_enabledis documented as an imprecise best-effort API to avoid;FnValueis preferred: https://docs.rs/slog/latest/slog/trait.Drain.html#method.is_enabledisInfoEnabled()guards as the old approach, because other filters can still reject the event: https://logging.apache.org/log4j/2.x/manual/performance.htmltracing::enabled!documents false-positive and false-negative cases when the probe metadata differs from the actual event: https://docs.rs/tracing/latest/tracing/macro.enabled.htmlslogrecommends lazy values and logger-bound attributes for expensive data: https://pkg.go.dev/log/slog#hdr-Performance_considerationsOpenTelemetry still recommends a low-level
Logger.Enabledoperation for instrumentation implementations, while explicitly describing it as an optional optimization whose result can become stale: https://opentelemetry.io/docs/specs/otel/logs/api/#enabledLogforth therefore retains
Logger::enabled(&FilterCriteria)for filters, bridges, and macro internals, but does not promote it as a normal application macro. Its documentation calls it a conservative prefilter rather than a promise. TheFilter::enabledcontract also states that filters requiring message/field data must returnNeutraland decide inmatches.One generic level macro plus familiar conveniences
The six convenience macros cover the common path. The generic
log!accepts a level expression and makes all 24 Logforth/OpenTelemetry severities usable without addingtrace2!,trace3!, and so on.This shape follows Go's
slog.Logger.Log, Python'sLogger.log, and Log4j'sLogger.log(Level, ...). It also addresses repeated Rust requests for Notice, Critical, Fatal, or otherwise extensible levels:tracing::log!()to accept any log level tokio-rs/tracing#3585: Addtracing::log!()to accept any log level tokio-rs/tracing#3585fatal!records severity only; it does not terminate the process or imply a flush.Structured fields retain their types
Plain values use the public
ToValueconversion trait and retain supported scalar types instead of becoming formatted strings.:?and:%are explicit, lazily formatted escape hatches forDebugandDisplay. Structured-only records may omit the text message.Declarative macros in core, with no feature gate
The implementation uses hygienic
macro_rules!macros inlogforth-core, then re-exports them fromlogforth.This avoids:
LoggerfilteringThe macros are always available, introduce no new dependency, and use
$cratepaths so facade re-exports remain hygienic.Non-goals
logfacade for reusable librariesValidation
cargo x testRustLogFilter, metadata, typed-field, disabled-evaluation, evaluate-once, structured-only, and cross-platform source-path testscargo-semver-checksforlogforth-coreandlogforthagainstorigin/main