Skip to content

blackbull.server.access_log

blackbull.server.access_log

Per-request access-log helpers shared by the HTTP/1.1 and HTTP/2 paths.

AccessLogRecord dataclass

Per-request record populated in two phases.

Phase 1 (after parse): client_ip, method, path, http_version. Phase 2 (during send): status, response_bytes. For WebSocket sessions, close_code is captured on disconnect instead. Emitted as one INFO line on 'blackbull.access' after the response completes.

__str__()

Self-formatting message body. emit_access_log hands the record to logger.info as the message so this — and the format() string build it wraps — runs on the logging listener thread, not the event loop. Cached because several sink handlers may format the same record.

finalize()

Snapshot the duration at completion so a later (deferred) format() reports the request's real duration rather than duration + the time the record waited in the logging queue. Idempotent; returns self.

from_conn(conn) classmethod

Build directly from a :class:~blackbull.connection.Connection (Sprint 80 Tier-2) so the self-hosted actor never materializes the ASGI scope just to record the access line.

mark(name)

Capture wall + CPU clocks for name. No-op when phase tracing is disabled, so callers don't need to guard themselves.

phase_summary()

Format the phase deltas as a→b=Wus|Cus a→b=....

disconnect_events_observed(aggregator)

Whether the disconnect-detecting receive wrapper is observed.

The wrapper (a per-request closure) exists to (a) emit request_disconnected and (b) mark_disconnected so request_completed can suppress itself on a dropped request. With neither listener present nothing observes either effect, so the actor dispatches the raw receive directly and saves the closure. Body-level disconnect detection (conn.body()ClientDisconnected) is independent of this wrapper and unaffected.

emit_access_log(record)

Emit record on the access logger if INFO is enabled.

The isEnabledFor gate matters: record.as_extra() and (formerly) record.format() are evaluated before logger.info decides to discard the call. Profiling at -R 5000 with BB_ACCESS_LOG=0 showed these calls still costing ~1.2% of CPU. Peers (uvicorn / granian / daphne) skip the work entirely when access logging is disabled; gating here matches that behaviour.

The record itself is the message (it is self-formatting via __str__), so the expensive format() string build is deferred to the logging listener thread instead of running on the event loop. finalize() snapshots the duration first so that deferred format still reports the request's real duration, not duration + queue latency. The structured extra fields stay eager — they are the documented public access-log API (guide.md §14; tests/integration/test_access_log.py).

When async logging is active and the access logger has not been customised, the record is enqueued directly onto the listener queue via :func:~blackbull.logger.enqueue_access_log (O4), which bypasses logging.Logger._log — ~93% of the loop-side emit cost lives in that stdlib machinery. The fast path is skipped (and the standard synchronous logger.info path used) when async logging is off or the user has attached their own handlers/filters to blackbull.access — those would be bypassed by a direct enqueue, so we defer to the full path to keep the documented "extend the access log via a custom handler/filter" pattern working (see docs/guide/logging.md).

request_record_needed(aggregator)

Whether the per-request :class:AccessLogRecord will be consumed.

The record (and the conn.state['access_log'] write it forces, plus the emit at request end) exists only for three consumers: the access log (blackbull.access at INFO), phase tracing, and the request_completed event's wire fields. When none is active the record is dead weight on every request — a per-request allocation the v0.60.0 Connection graph makes more costly under concurrency (extra live objects for the cyclic GC to scan) — so the actor skips building it. Consumers already tolerate its absence: the sender guards if self._log_record is not None and request_completed reads conn.state.get('access_log') with '-'/0 placeholders.