Context¶
Design principle¶
Context belongs at runtime boundaries, not at logger creation time. Instead of creating loggers with bound fields, you bind context at request, job, or task boundaries and it flows into every log record within that scope.
Context manager¶
from ultilog import get_logger, logging_context
log = get_logger("worker")
with logging_context(job_id="job_1", queue="emails"):
log.info("job.started") # includes job_id and queue
log.info("job.finished") # same context
# context automatically restored
Nesting¶
Context scopes nest correctly. Inner scopes merge with outer values, and restoration is token-based:
with logging_context(outer="1"):
with logging_context(inner="2"):
get_context() # {"outer": "1", "inner": "2"}
get_context() # {"outer": "1"}
Async safety¶
Context uses Python's contextvars.ContextVar, which is natively safe with asyncio. Concurrent tasks get isolated context:
async def handle_request(request_id: str):
with logging_context(request_id=request_id):
await do_work() # context stays with this task
Low-level API¶
For integrations that need direct control:
from ultilog import bind_context, clear_context, get_context
token = bind_context(request_id="req_123")
get_context() # {"request_id": "req_123"}
clear_context() # {}
Decorator¶
Wrap functions with automatic context:
from ultilog.context.decorators import with_logging_context
@with_logging_context(component="worker")
def process_job():
log.info("processing") # component=worker
How context reaches log records¶
The ContextFilter (installed automatically by bootstrap) reads the current context and injects it into each LogRecord as:
ultilog_context-- dictionary of context valuesultilog_context_text-- compact text suffix likerequest_id=r1- Individual record attributes for safe keys