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.