Files
BitLogger/docs/api/async-logger.md
T
2026-07-17 15:53:21 +08:00

7.9 KiB

name, group, category, update-time, description, key-word
name group category update-time description key-word
async-logger api async 20260707 Create an async logger with bounded queueing, overflow policy, lifecycle helpers, background run control, and a raising flush callback.
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

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:

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:

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:

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:

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 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.