Event Handler Context¶
Worker processes — RabbitMQ consumers, Celery tasks, background loops — run without HTTP middleware, so company_id and other tenant fields are never automatically bound to structlog context-vars. Without them, every log line from the worker is anonymous and debugging requires painful cross-referencing.
with_event_context solves this by binding the tenant context for the duration of a single event handler call.
Quick Start¶
from obskit import with_event_context
import structlog
logger = structlog.get_logger()
@with_event_context(lambda event: {
"company_id": str(event.get("company_id", "")),
"company_schema": event.get("company_schema", ""),
})
async def handle(self, event: dict) -> None:
logger.info("processing event")
# → {"event": "processing event", "company_id": "42", "company_schema": "acme_db", ...}
await self.repo.create_record(event["data"])
How It Works¶
- Before the handler is called,
extractor(event)is called to produce a dict of context-var bindings. - The bindings are added to the structlog context via
bind_contextvars(**ctx). - The handler runs — all log calls inside it include the bound fields automatically.
- On exit (normal return or exception), the bound keys are removed via
unbind_contextvars(*ctx.keys()).
Extractor Function¶
The extractor receives the raw event dict and returns a dict of bindings:
def my_extractor(event: dict) -> dict:
company_id = event.get("company_id")
if company_id is None:
return {} # skip binding — no tenant context in this event
return {
"company_id": str(company_id),
"company_schema": event.get("company_schema", ""),
}
@with_event_context(my_extractor)
async def handle(self, event: dict) -> None:
...
Returning {} or None skips all binding — no keys are added or removed.
Standalone Functions¶
The decorator works with standalone async functions too (no self):
@with_event_context(lambda e: {"job_id": e.get("job_id")})
async def process_job(event: dict) -> None:
...
Exception Safety¶
The context is always unbound on exit, even if the handler raises:
@with_event_context(lambda e: {"company_id": str(e.get("company_id"))})
async def handle(self, event: dict) -> None:
raise RuntimeError("unexpected error")
# company_id is removed from context even though an exception was raised
Scope Isolation¶
Only the keys returned by the extractor are unbound on exit. Keys bound inside the handler (or by outer middleware) are not affected:
bind_context(request_id="req-123") # bound before handler
@with_event_context(lambda e: {"company_id": str(e.get("company_id"))})
async def handle(self, event: dict) -> None:
bind_context(extra="info") # bound inside — caller's responsibility
# After handler: request_id is still bound, company_id is unbound
API Reference¶
obskit.logging.event_context.with_event_context ¶
with_event_context(
extractor: Callable[
[dict[str, Any]], dict[str, Any] | None
],
) -> Callable[[Callable[..., Any]], Callable[..., Any]]
Decorator factory that binds structlog context-vars for an async handler.
Extracts context keys from the event dict using extractor, binds them for the duration of the handler call, and unbinds them automatically on exit — whether the handler returns normally or raises an exception.
Parameters¶
extractor : callable
A callable (event: dict) → dict | None that maps the incoming
event to a set of structlog context-var bindings. Return {} or
None to skip binding (e.g. when the event lacks a required field).
Returns¶
callable Decorator that wraps async handler functions.
Example¶
::
@with_event_context(lambda event: {
"company_id": str(event.get("company_id", "")),
"company_schema": event.get("company_schema", ""),
})
async def handle(self, event: dict) -> None:
...
Notes¶
The decorator locates the event dict by searching positional arguments for
the first :class:dict instance (skipping self which is typically a
class instance), then falls back to a keyword argument named "event".
Keys bound inside the handler via :func:obskit.logging.context.bind_context
are not removed on exit — they are the caller's responsibility.
Source code in src/obskit/logging/event_context.py
| Python | |
|---|---|
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | |