Skip to content

refactor!: introduce LoggerProvider and record Metadata - #241

Draft
tisonkun wants to merge 1 commit into
codex/native-log-macrosfrom
codex/logger-provider-metadata
Draft

refactor!: introduce LoggerProvider and record Metadata#241
tisonkun wants to merge 1 commit into
codex/native-log-macrosfrom
codex/logger-provider-metadata

Conversation

@tisonkun

Copy link
Copy Markdown
Contributor

This is a stacked design PR on top of #239. Its purpose is to make the proposed native API concrete before either layer is stabilized.

Summary

  • replace LoggerBuilder with LoggerProviderBuilder
  • make LoggerProvider own and share the configured Dispatch graph
  • make Logger a lightweight, cloneable handle obtained with provider.logger() or provider.named_logger(name)
  • replace the prefilter-only FilterCriteria with a complete Metadata value
  • store the same Metadata in the resulting Record, so prefiltering and record construction cannot drift
  • provide complete call-site source metadata to native prefilters before evaluating the message or structured fields
  • adapt the log bridge and RustLogFilter directly to the Logforth metadata model
  • keep Dispatch, Append, Filter, Diagnostic, Layout, and the existing native macro grammar

Proposed final API

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

let logger = provider.logger();
let metering = provider.named_logger("metering");

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

logforth::info!(
    metering,
    tenant_id,
    metering_kind = "compute",
    compute_time_ms;
);

provider.flush();

LoggerProvider is deliberately not called a runtime. It has one concrete responsibility: own shared dispatch configuration and create logger handles. Logger remains the object passed to native macros and used to emit records.

Named and unnamed loggers remain explicit variants of the existing target behavior:

  • provider.logger() uses the call-site module path as the native target
  • provider.named_logger("metering") uses the stable logger name as the native target
  • both retain the actual module/file/line/column as source metadata

A dedicated dispatch graph still uses a dedicated provider. Multiple logger handles from one provider intentionally share its dispatches.

Metadata

pub struct Metadata<'a> {
    level: Level,
    target: RefStr<'a>,
    module_path: Option<RefStr<'a>>,
    file: Option<RefStr<'a>>,
    line: Option<u32>,
    column: Option<u32>,
}

pub struct Record<'a> {
    now: SystemTime,
    metadata: Metadata<'a>,
    payload: fmt::Arguments<'a>,
    kvs: KeyValues<'a>,
}

The macro constructs Metadata before evaluating payload/KV expressions, asks Logger::enabled(&metadata), and then moves the same value into Record::builder().metadata(metadata).

This is different from the older Logforth type that was renamed from Metadata to FilterCriteria: that type contained only level and target and was not part of Record. This PR makes metadata the actual record header and includes all information already present in Logforth records; it does not introduce a parallel event schema.

There is deliberately no public Emit trait or Interest type in this proposal. The native macros continue to depend on the concrete Logger; an abstraction can be added later if a real second implementation requires it.

Event model boundaries

This PR does not add channel, delivery, or a required event_name.

CloudEvents requires id, source, and type because it defines an interoperable event envelope. That requirement does not imply that every diagnostic log call should fill those fields: https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md

OpenTelemetry Logs treats body, attributes, and event name as optional. A non-empty event name identifies a structured Event, while ordinary LogRecords need not have one: https://opentelemetry.io/docs/specs/otel/logs/data-model/

If Logforth later adds an optional event name, it can be evaluated independently without being coupled to this provider/metadata split.

Compatibility

logforth-bridge-log maps log::Metadata and log::Record directly into Logforth Metadata and Record. The core model has no dependency on log types.

A future tracing integration should likewise be a direct adapter into this model; this PR does not add tracing dependencies or route tracing through log.

Breaking changes

  • builder().build() now returns LoggerProvider; call .logger() to obtain an unnamed logger
  • LoggerBuilder becomes LoggerProviderBuilder
  • LoggerBuilder::name becomes LoggerProvider::named_logger
  • Filter::enabled and Logger::enabled receive Metadata instead of FilterCriteria
  • starter builders return LoggerProvider from build()

Validation

  • cargo x test
  • cargo +nightly clippy --tests --all-features --all-targets --workspace -- -D warnings
  • cargo +nightly fmt --all --check
  • Taplo formatting check over workspace TOML sources
  • typos
  • hawkeye check

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.

1 participant