structlog
raw JSON → 25.5.0 verified Tue May 12 auth: no python install: verified quickstart: verified
Structured logging for Python. Current version: 25.5.0 (Mar 2026). No level filtering by default — ALL log levels emitted until configured. structlog.configure() must be called before first use; loggers obtained before configure() use default settings. bind() is immutable — returns a new logger, does not mutate in place. structlog.contextvars.merge_contextvars needed for request-scoped context. stdlib interop requires ProcessorFormatter. _context attribute deprecated — use structlog.get_context().
pip install structlog Common errors
error ModuleNotFoundError: No module named 'structlog' ↓
cause The structlog library has not been installed in your Python environment.
fix
Run
pip install structlog to install the library. error RuntimeError: structlog.configure() can only be called once. ↓
cause The `structlog.configure()` function was invoked multiple times, which is prohibited as it's intended for a single, early application-wide setup.
fix
Ensure
structlog.configure() is called only once at the very beginning of your application's lifecycle, typically during startup. error AttributeError: '_Logger' object has no attribute '_context' ↓
cause You are attempting to access the `_context` attribute directly on a `structlog` logger instance, which is deprecated and no longer accessible in current versions.
fix
Use
structlog.get_context() or structlog.contextvars.get_context() to retrieve the current context, or use logger.bind(...) to add context for specific log calls. error structlog bind not working ↓
cause The `logger.bind()` method is immutable; it returns a *new* logger instance with the added context, and does not modify the original logger in-place.
fix
Always use the logger instance returned by
logger.bind() for subsequent logging calls that should include the new context, e.g., bound_logger = logger.bind(key='value'); bound_logger.info('message'). error structlog contextvars context missing ↓
cause Context set via `structlog.contextvars` will not automatically appear in logs unless `structlog.contextvars.merge_contextvars` is included as a processor in your `structlog.configure()` setup.
fix
Add
structlog.contextvars.merge_contextvars to your list of processors when calling structlog.configure(), for example: structlog.configure(processors=[..., structlog.contextvars.merge_contextvars, ...]). Warnings
breaking No log level filtering by default — ALL levels (debug, info, warning, error) are emitted. Must use make_filtering_bound_logger(logging.INFO) in wrapper_class to filter. ↓
fix wrapper_class=structlog.make_filtering_bound_logger(logging.INFO) in structlog.configure()
breaking bind() is immutable — returns a new logger, does NOT mutate the original. log.bind(key='val') without reassignment is silently discarded. ↓
fix Always reassign: log = log.bind(key='val') or request_log = log.bind(request_id='123')
breaking structlog.configure() must be called before first use. With cache_logger_on_first_use=True (default in prod), loggers cached before configure() use default settings permanently. ↓
fix Call structlog.configure() at app startup, before any module-level get_logger() calls are invoked.
gotcha _context attribute on bound loggers is deprecated since v21.1. Raises DeprecationWarning. ↓
fix Use structlog.get_context(log) instead of log._context
gotcha contextvars (bind_contextvars) context is NOT automatically cleared between requests. Forgetting clear_contextvars() leaks context from one request to the next. ↓
fix Call structlog.contextvars.clear_contextvars() at the start or end of each request in middleware.
gotcha structlog.stdlib.AsyncBoundLogger deprecated since v23.1. Use native async methods (ainfo, adebug, etc.) on the standard bound logger instead. ↓
fix await log.ainfo('event') instead of configuring AsyncBoundLogger as wrapper_class.
Install compatibility verified last tested: 2026-05-12
python os / libc status wheel install import disk
3.10 alpine (musl) - - 0.01s 18.6M
3.10 slim (glibc) - - 0.01s 19M
3.11 alpine (musl) - - 0.03s 20.2M
3.11 slim (glibc) - - 0.03s 21M
3.12 alpine (musl) - - 0.02s 12.1M
3.12 slim (glibc) - - 0.02s 13M
3.13 alpine (musl) - - 0.02s 11.7M
3.13 slim (glibc) - - 0.02s 12M
3.9 alpine (musl) - - 0.01s 18.1M
3.9 slim (glibc) - - 0.01s 19M
Imports
- structlog.configure + get_logger wrong
import structlog # Wrong: using logger before configure() — uses default unconfigured settings log = structlog.get_logger() # obtained before configure log.info('too early') # Later... structlog.configure(processors=[...]) # too late for above logger if cache=True # Wrong: expecting bind() to mutate in place log = structlog.get_logger() log.bind(user_id='123') # no-op — bind() returns new logger, original unchanged log.info('event') # user_id NOT in this logcorrectimport logging import structlog # Configure BEFORE any get_logger() calls structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.stdlib.add_log_level, structlog.stdlib.add_logger_name, structlog.processors.TimeStamper(fmt='iso'), structlog.processors.StackInfoRenderer(), structlog.processors.JSONRenderer(), # JSON output ], wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), # filter by level logger_factory=structlog.PrintLoggerFactory(), cache_logger_on_first_use=True, ) log = structlog.get_logger() log.info('app_started', version='1.0.0') log.warning('high_memory', usage_pct=95) # bind() returns NEW logger — immutable request_log = log.bind(request_id='req-123', user_id='usr-456') request_log.info('payment_initiated', amount=500) # original log is unchanged - stdlib interop with ProcessorFormatter wrong
# Using structlog alongside stdlib without ProcessorFormatter # results in two different log formats in the same output import logging import structlog logging.basicConfig() # plain text format log = structlog.get_logger() # different formatcorrectimport logging import sys import structlog # Make stdlib logging go through structlog processors timestamper = structlog.processors.TimeStamper(fmt='iso') pre_chain = [ structlog.stdlib.add_log_level, timestamper, ] handler = logging.StreamHandler(sys.stdout) handler.setFormatter( structlog.stdlib.ProcessorFormatter( processor=structlog.processors.JSONRenderer(), foreign_pre_chain=pre_chain, ) ) root_logger = logging.getLogger() root_logger.addHandler(handler) root_logger.setLevel(logging.INFO) # Now both structlog AND stdlib logging output same JSON format structlog.configure( processors=[ structlog.stdlib.add_log_level, timestamper, structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), ) log = structlog.get_logger() log.info('structured', key='val') logging.getLogger('uvicorn').info('stdlib log') # also JSON
Quickstart verified last tested: 2026-04-23
# pip install structlog
import logging
import structlog
# Configure at app startup
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt='iso'),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
cache_logger_on_first_use=True,
)
log = structlog.get_logger()
# Basic structured logging
log.info('user_login', user_id='usr-123', method='oauth')
log.error('payment_failed', order_id='ord-456', reason='insufficient_funds')
# bind() — immutable, returns new logger
req_log = log.bind(request_id='req-789', ip='1.2.3.4')
req_log.info('request_received', path='/api/checkout')
req_log.info('request_completed', status=200, duration_ms=42)
# Request-scoped context via contextvars (for async/web frameworks)
structlog.contextvars.bind_contextvars(trace_id='abc123')
log.info('db_query', table='orders') # trace_id auto-included
structlog.contextvars.clear_contextvars()
# Drop event from pipeline
def filter_health_checks(logger, method, event_dict):
if event_dict.get('path') == '/health':
raise structlog.DropEvent()
return event_dict