Skip to content

blackbull.logger

blackbull.logger

BatchWriteHandler

Bases: Handler

Coalesce formatted records into one write per batch (O2 — batch writes, a.k.a. logging approach 4).

The stdlib StreamHandler issues stream.write() + stream.flush() per record — one flushed write syscall per log line. Under a high-rate access log that is the dominant cost on the listener thread. This handler instead appends each formatted line to an in-memory buffer and lets a single long-lived flusher thread emit the batch as one write() when the buffer reaches batch_size or flush_interval seconds elapse — whichever comes first, so latency is bounded at low rate and syscalls collapse at high rate.

Design notes: - One flusher thread total (not a threading.Timer per batch — that would churn a thread per ~batch_size records under load). It waits on a Condition with the flush interval as its timeout: a full batch notifies it awake, an idle interval wakes it to drain a partial batch. - Formatting runs on the flusher thread, so a deferred-format access record still builds its string off the event loop (as with the plain default). - close() drains the buffer and joins the thread, so a partial trailing batch is never lost at teardown.

Opt-in: only constructed when BB_LOG_BATCH_SIZE > 1.

JsonFormatter

Bases: Formatter

Emit one JSON object per log line (logging approach 3 — structured JSON).

Every record carries timestamp / level / logger / message. Access-log records (blackbull.access) additionally attach the structured fields from :meth:AccessLogRecord.as_extra via extra= — those are lifted to first-class JSON keys (client_ip, method, path, http_version, status, response_bytes, duration_ms, and close_code for WebSocket disconnects). exc_info is rendered as a formatted traceback string when present.

Runs on the QueueListener thread (it is the sink handler's formatter), so the access record's format() string build still happens off the event loop, exactly as with the plain-text default.

enqueue_access_log(msg, extra=None)

O4 fast path: enqueue an access-log record straight onto the async listener queue, bypassing logging.Logger._log (no findCaller stack walk, no filter chain, no callHandlers dispatch).

msg is the self-formatting AccessLogRecord; its __str__/format() still runs on the listener thread (deferred format preserved), because the sink formats the record there. extra (AccessLogRecord.as_extra()) is merged onto the record so structured/JSON sinks keep their documented fields.

Returns True when the record was enqueued (async logging active), or False when the caller must use the synchronous logger path (async logging disabled or torn down). The level gate is the caller's job — emit_access_log checks isEnabledFor(INFO) before calling.

This path does not run the logger's handler/filter chain, so emit_access_log only calls it when blackbull.access has no user-attached handlers or filters (see there); when it does, the standard logger.info path is used so those are honoured.

log(fn)

Decorator: log call arguments at DEBUG level.

Automatically uses the logger of the module where @log is applied, determined by inspecting the caller's frame at decoration time.

When the module logger is not enabled for DEBUG at decoration time (i.e. at import), the decorator is a zero-cost no-op: the original function is returned unwrapped, so there is no extra function-call overhead in production. The trade-off is that raising the log level to DEBUG after modules have been imported will not activate logging for already-decorated functions.

setup_async_logging(handlers=None, *, log_format=None, syslog_addr=None, batch_size=None, batch_timeout_ms=None, log_file=None)

Install a QueueHandler on the blackbull logger hierarchy.

After this call every logger.debug/info/warning in the event loop enqueues a LogRecord and returns immediately. A daemon thread (QueueListener) drains the queue and forwards records to handlers.

Idempotent — a second call before :func:teardown_async_logging is a no-op.

Parameters

handlers: Handlers the background listener should write to. Defaults to the non-NullHandler handlers already on the blackbull logger, or the sink built by :func:_build_sink_handlers when none exist. log_format, syslog_addr, batch_size, batch_timeout_ms, log_file: Sink configuration forwarded to :func:_build_sink_handlers (only used when handlers is None and no handlers are pre-attached). The server startup passes these from Settings (get_settings()); each defaults to None → the matching BB_* env var.

teardown_async_logging()

Stop the QueueListener and restore a NullHandler on blackbull.