From 296901d2ba377789607c7837c4628fca8ed428e6 Mon Sep 17 00:00:00 2001 From: Nanaloveyuki Date: Sun, 14 Jun 2026 08:43:18 +0800 Subject: [PATCH] :memo: clarify root context shape contrast --- docs/api/async-logger.md | 12 ++++++++++++ docs/api/logger.md | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/docs/api/async-logger.md b/docs/api/async-logger.md index fecc9ae..53661e8 100644 --- a/docs/api/async-logger.md +++ b/docs/api/async-logger.md @@ -47,6 +47,7 @@ Detailed rules explaining key parameters and behaviors - `async_logger(...)` returns the full `AsyncLogger[S]` surface directly. It is therefore the underlying constructor used by both application-facing async aliases and the narrower `LibraryAsyncLogger[S]` wrapper line. - The constructed logger starts with `is_closed=false`, `is_running=false`, `has_failed=false`, `last_error=""`, and zeroed pending/dropped counters. - The constructed logger also keeps the core async target contract unchanged: `log(..., target=...)` can override the target for one call, while fixed-level helpers such as `info(...)`, `warn(...)`, and `error(...)` continue using the stored logger target unless code derives another logger first with `with_target(...)` or `child(...)`. +- Unlike synchronous `Logger`, async `with_context_fields(...)` and `bind(...)` preserve the visible `AsyncLogger[S]` type because shared fields are stored directly on the async logger value instead of being modeled as a separate sink wrapper. - `ApplicationAsyncLogger` and `ApplicationTextAsyncLogger` are only alias names over concrete `AsyncLogger[...]` shapes, so they keep the same lifecycle, queue, failure, and state helpers without adding a wrapper layer. - `LibraryAsyncLogger[S]` wraps an `AsyncLogger[S]` value instead of aliasing it. That library facade preserves queue-backed logging behavior, but it narrows the directly exposed helper surface until callers recover the full logger with `to_async_logger()`. - In non-native targets, the implementation uses compatibility behavior while keeping the same public surface. @@ -88,6 +89,17 @@ In this example, the emitted record uses `app.async.audit` only for that one cal And later `info(...)`, `warn(...)`, or `error(...)` calls still use the logger's stored target unless code derives another logger first with `with_target(...)` or `child(...)`. +#### When Need Shared Context On A Root Async Logger + +When async code should attach stable metadata to later queued records: +```moonbit +let contextual = logger.with_context_fields([@bitlogger.field("service", "billing")]) +``` + +In this example, the returned value still has the visible type `AsyncLogger[S]`. + +And that shape preservation is intentional because async context binding updates stored logger metadata instead of changing the exposed sink type. + #### When Need Configurable Overflow And Flush Behavior When queue semantics matter for service durability and load: diff --git a/docs/api/logger.md b/docs/api/logger.md index b91f99d..1827b26 100644 --- a/docs/api/logger.md +++ b/docs/api/logger.md @@ -38,6 +38,7 @@ Detailed rules explaining key parameters and behaviors - The current fields are `min_level : Level`, `sink : S`, `target : String`, and `timestamp : Bool`. - The sink type parameter is preserved across composition, which is why helpers such as `with_context_fields(...)`, `with_filter(...)`, `with_patch(...)`, and `with_queue(...)` can return more specific logger shapes. - The root logger also preserves the core target contract used across the sync API surface: `log(..., target=...)` can override the target for one call, while fixed-level helpers such as `trace(...)`, `debug(...)`, `info(...)`, `warn(...)`, and `error(...)` continue using the stored logger target unless code derives another logger first with `with_target(...)` or `child(...)`. +- In particular, synchronous `with_context_fields(...)` and `bind(...)` change the visible logger type to `Logger[ContextSink[S]]` because shared fields are implemented by extending the sink pipeline rather than by storing extra root-level context metadata on `Logger[S]` itself. - `Logger::new(...)` constructs this type as the main synchronous entry point. - This root type is also what sits underneath both facade families: `ApplicationLogger` is a direct alias over the configured `Logger[RuntimeSink]` line, while `LibraryLogger[S]` is a narrowing wrapper around a `Logger[S]` value. @@ -65,6 +66,17 @@ In this example, the emitted record uses `app.audit` only for that one call. And later `trace(...)`, `debug(...)`, `info(...)`, `warn(...)`, or `error(...)` calls still use the logger's stored target unless code derives another logger first with `with_target(...)` or `child(...)`. +#### When Need Shared Context On A Sync Root Logger + +When sync code should attach stable metadata to later records: +```moonbit +let contextual = logger.with_context_fields([field("service", "billing")]) +``` + +In this example, the returned value has the visible type `Logger[ContextSink[S]]`. + +And that type change is expected because sync context binding extends the sink pipeline instead of only updating root-level metadata. + #### When Need To Build A Composed Logging Pipeline When code should start from one root logger and then derive more specific wrapped forms: