structlog
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().
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.
- breaking bind() is immutable — returns a new logger, does NOT mutate the original. log.bind(key='val') without reassignment is silently discarded.
- 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.
- gotcha _context attribute on bound loggers is deprecated since v21.1. Raises DeprecationWarning.
- gotcha contextvars (bind_contextvars) context is NOT automatically cleared between requests. Forgetting clear_contextvars() leaks context from one request to the next.
- gotcha structlog.stdlib.AsyncBoundLogger deprecated since v23.1. Use native async methods (ainfo, adebug, etc.) on the standard bound logger instead.
Install
-
pip install structlog
Imports
- structlog.configure + get_logger
import 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
import 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
# 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