--- name: async-logger group: api category: async update-time: 20260707 description: Create an async logger with bounded queueing, overflow policy, lifecycle helpers, background run control, and a raising flush callback. key-word: - async - logger - queue - public --- ## Async-logger Create an `AsyncLogger[S]` on top of a sink and async queue configuration. This API is the main entry for queue-backed async logging, including overflow policy, batching, lifecycle control, and runtime observability. ### Interface ```moonbit pub fn[S] async_logger( sink : S, config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(), min_level~ : @bitlogger.Level = @bitlogger.Level::Info, target~ : String = "", flush~ : (S) -> Unit raise = fn(_) { () }, ) -> AsyncLogger[S] {} ``` #### input - `sink : S` - Underlying sink used after queue drain. - `config : AsyncLoggerConfig` - Queue size, overflow behavior, batching, linger, and flush policy. - `min_level : Level` - Level gate applied before enqueue. - `target : String` - Default target for emitted records. - `flush : (S) -> Unit raise` - Flush callback used by batch/shutdown flush policies and allowed to raise if sink flushing fails. #### output - `AsyncLogger[S]` - A queue-backed async logger with lifecycle and state helpers. ### Explanation Detailed rules explaining key parameters and behaviors - `async_logger(...)` only builds the logger. Actual background draining is started by `run()`. - `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 in lifecycle phase `ready`, with `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. - `src-async` is designed for `native / llvm / js / wasm / wasm-gc`, but current release-facing local verification is stronger for `native / js / wasm / wasm-gc` than for `llvm`. - `llvm` should currently be read as experimental and locally unverified in this environment rather than as a stable checked target. - `flush` is used only when batch or shutdown policy wants explicit flushing. - The callback expresses attempted flush timing and failure, not a returned progress count contract. - If the supplied flush callback raises, worker failure state is recorded through `has_failed()` and `last_error()`. - `wait_idle()` is failure-aware rather than a pure backlog-to-zero guarantee. If a worker failure sets `has_failed=true`, waiting stops early and the logger can still report `pending_count() > 0` until later cleanup or restart work happens. - A later `run()` attempt starts by clearing stale failure state back to `has_failed=false` and `last_error=""` before it resumes draining any backlog still left in the queue. - `state()` exposes the stronger lifecycle conclusion through `phase`, plus derived reads such as `backlog_retained`, `can_rerun`, and `terminal` so callers do not have to infer these states from raw flag combinations. - Late log attempts after closure are rejected in the shared async log path before patch/filter work runs, so post-close side effects no longer vary by backend. - `run()` now enforces a single-worker contract and raises `AsyncLoggerAlreadyRunning` when a second worker startup is attempted for the same live logger. - `run()` also rejects already-closed loggers with `AsyncLoggerClosed`, which keeps rerun semantics aligned with the stronger lifecycle model: rerun is for retained `failed` backlog, not for terminal closed phases. - `shutdown()` now always waits for any active worker to finish and converts retained post-failure backlog into dropped records before returning. - Queue overflow behavior depends on `AsyncOverflowPolicy`. ### How to Use Here are some specific examples provided. #### When Need Background Queue Drain When your sink should not be written directly on the caller path: ```moonbit let logger = async_logger(callback_sink(fn(rec) { println(rec.message) })) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) logger.info("hello") logger.shutdown() }) ``` In this example, the worker drains queued records in the background and `shutdown()` waits for completion. And the logging call path stays queue-oriented rather than direct-sink oriented. #### When Need A One-call Target Override On The Root Async Logger When async code should keep one logger value but emit a single record under a different target: ```moonbit logger.log(@bitlogger.Level::Error, "boom", target="app.async.audit") ``` In this example, the emitted record uses `app.async.audit` only for that one call. 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: ```moonbit let logger = async_logger( console_sink(), config=AsyncLoggerConfig::new( max_pending=128, overflow=AsyncOverflowPolicy::DropOldest, max_batch=8, flush=AsyncFlushPolicy::Batch, ), ) ``` In this example, queue pressure and flush timing are both explicit. ### Error Case e.g.: - If the logger is closed, further enqueue attempts stop being normal active logging operations. - If queue drain fails internally, runtime state can reflect that through `has_failed()` and `last_error()`. - If a second worker startup is attempted while `is_running()` is already true, `run()` raises `AsyncLoggerAlreadyRunning`. ### Notes 1. `async_logger(...)` is the async counterpart to `Logger::new(...)`. 2. Use `state()`, `pending_count()`, and `dropped_count()` for runtime diagnostics. 3. Example entrypoint limitations such as `async fn main` support are separate from the library-level portability of this API. 4. See [target-verification.md](./target-verification.md) for the current local verification matrix. 5. Pair this constructor with `run()` and `shutdown()` when you need the full worker lifecycle rather than just a configured async logger value. 6. Choose the facade name based on boundary intent: use `AsyncLogger[S]` for the full surface, `ApplicationAsyncLogger` or `ApplicationTextAsyncLogger` for application-facing alias names, and `LibraryAsyncLogger[S]` when a package boundary should intentionally narrow what downstream code can call directly.