structlog review
structlog 26.1.0 treats a log event as a dictionary that passes through an ordered list of processors before a final renderer or Python logging handler emits it. Bound loggers retain fields such as service or tenant, and context variables can add request data across async calls. It can write JSON or console text directly, or integrate with standard `logging` so third-party records share handlers and formatting. Release 26.1 drops Python 3.8 and 3.9, adds Python 3.15 support, introduces monochrome Rich tracebacks and qualified module call sites, fixes async thread attribution and file-object retention, and adds snake-case level helpers to the stdlib bound logger.
structlog 26.1.0 installed as 1 package and 1 MB in 0.2 seconds on our sandbox, so the dependency cost is tiny; the real cost is agreeing on processors, fields, and redaction. Add it when structured events and contextual fields are part of the service contract, and keep transport, buffering, and retention in the logging stack around it.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 1 MB |
| Import | ✓ | import structlog in 0.50s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does structlog install cleanly?
Yes. In a fresh container with an empty cache, pip install structlog finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does structlog need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import structlog succeeded in 0.50s, and the package ships py.typed for type checkers.
structlog or loguru: which should you use?
loguru: Use it for convenient sinks, rotation, formatting, and exception display without designing event dictionaries. structlog 26.1.0 installed as 1 package and 1 MB in 0.2 seconds on our sandbox, so the dependency cost is tiny; the real cost is agreeing on processors, fields, and redaction.
When should you not use structlog?
A short script only needs logging.basicConfig() and several plain messages. Global processor configuration will add more concepts than value.
Use it if
- Production logs need stable event names and queryable fields instead of parsing values back out of sentences.
- Request IDs, tenant IDs, or job IDs must follow nested asyncio calls through context variables.
- Application events must coexist with third-party standard-library logs under the same handlers and level policy.
- Developers want readable local output while deployed services emit JSON from the same logging calls.
- A short script only needs `logging.basicConfig()` and several plain messages. Global processor configuration will add more concepts than value.
- The team has no event naming or redaction policy. Arbitrary dictionaries remain hard to query and can expose secrets even when encoded as JSON.
- Python 3.9 must remain supported. structlog 26.1 starts at Python 3.10.
- Most logs come from standard `logging` and nobody will maintain the `ProcessorFormatter` bridge. A mistaken two-stage setup can encode rendered JSON as a string.
- You expect file rotation, network delivery, buffering, retention, or collector retries. structlog shapes records; handlers and observability agents transport them.
Setup reality
We installed structlog 26.1.0 in a clean Python 3.12 Bookworm container in 0.2 seconds. It left 1 package using 1 MB, and pip-audit found 0 known vulnerabilities. The package is pure Python, declares 1 direct dependency, requires Python 3.10 or newer, ships py.typed, and reports the Apache Software License. import structlog completed in 0.50 seconds. No service or credential is required to configure it.
Configuration is process-global and processor order changes output. Add context, level, timestamp, stack, and redaction processors before the renderer. JSONRenderer or ConsoleRenderer belongs last because it converts the event dictionary into final output. Configure at startup before modules materialize cached logger proxies. cache_logger_on_first_use=True saves proxy work, but loggers already cached will not follow a later configuration change.
The standard-library bridge has two paths. structlog.stdlib.BoundLogger creates records from structlog calls; ProcessorFormatter can prepare ordinary LogRecord objects from libraries and finish both kinds. End the structlog chain with ProcessorFormatter.wrap_for_formatter when the formatter owns rendering. Putting another JSON formatter after a rendered JSON string causes double encoding. Test one native event, one dependency log, and one exception through the actual production handler stack.
Normal asyncio tasks copy context variables, while sync/async handoffs in hybrid frameworks may not. Clear context at the start of every request or job, then bind its identifiers, so reused workers cannot retain prior values. Async methods such as ainfo move synchronous logging work to an executor; 26.1 corrects the reported caller thread, but it does not add a delivery queue. Redact tokens and personal data before the final renderer and before any processor copies the event elsewhere.
Patterns
Render application events as JSON configure-json
import structlog
structlog.configure(processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.JSONRenderer(),
])
log = structlog.get_logger()`JSONRenderer` must be last because it turns the event dictionary into the emitted value.
Attach fields reused by later events bind-logger-fields
log = structlog.get_logger().bind(service="billing", release="26.1")
log.info("invoice_created", invoice_id=42, amount_cents=500)Keep the event name stable and place changing values in fields so queries do not depend on parsing prose.
Set context at request entry bind-request-context
from structlog.contextvars import bind_contextvars, clear_contextvars
def begin_request(request_id: str, tenant_id: str) -> None:
clear_contextvars()
bind_contextvars(request_id=request_id, tenant_id=tenant_id)Clear before binding on reused workers; otherwise a missing field in the new request can expose a value from the previous one.
Replace secret-bearing fields before rendering redact-fields
def redact(_logger, _method_name, event_dict):
for name in ("password", "token", "authorization"):
if name in event_dict:
event_dict[name] = "[redacted]"
return event_dictPlace redaction before the renderer and before any processor that copies the dictionary to another destination.
Use a developer-oriented console renderer configure-console
structlog.configure(processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.dev.ConsoleRenderer(colors=True),
])ANSI console output is meant for terminals. Production collectors should receive JSON or another stable machine format.
Record the active exception log-exception
try:
gateway.charge(card)
except PaymentError:
log.exception("charge_failed", order_id=42)
raise`exception()` captures the active exception. Do not add card data, tokens, or request bodies to the event fields.
Discard disabled levels before processors run filter-levels
import logging
import structlog
structlog.configure(
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
processors=[structlog.processors.JSONRenderer()],
)Early filtering avoids timestamp, stack, and serialization work for events below the configured threshold.
Assert structured fields in a test capture-test-events
import structlog
def test_invoice_event():
with structlog.testing.capture_logs() as events:
create_invoice(42)
assert events[0]["event"] == "invoice_created"
assert events[0]["invoice_id"] == 42`capture_logs` temporarily changes global configuration and can interfere with parallel tests that emit logs concurrently.
Use an async logging method log-from-async
async def handle(order_id: int) -> None:
log = structlog.get_logger().bind(order_id=order_id)
await log.ainfo("order_received")Async methods offload the synchronous logging call to an executor; they do not provide buffering or collector backpressure control.
Render structlog and standard records together bridge-stdlib-logs
import logging
import structlog
shared = [
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
]
structlog.configure(
processors=[*shared, structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
)
formatter = structlog.stdlib.ProcessorFormatter(
foreign_pre_chain=shared,
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.processors.JSONRenderer(),
],
)Attach `formatter` to the real logging handler. `wrap_for_formatter` must end the structlog processor chain in this layout.
Discard a noisy event in a processor drop-health-events
import structlog
def drop_health(_logger, _method_name, event_dict):
if event_dict.get("path") == "/health":
raise structlog.DropEvent
return event_dict`DropEvent` stops the pipeline. Put sampling or filtering before expensive processors and never drop audit events by an accidental broad condition.
Include source location fields add-callsite
from structlog.processors import CallsiteParameter, CallsiteParameterAdder
add_callsite = CallsiteParameterAdder({
CallsiteParameter.QUAL_MODULE,
CallsiteParameter.FUNC_NAME,
CallsiteParameter.LINENO,
})Qualified module support arrives in 26.1 for structlog-originated events; standard `LogRecord` objects do not carry an equivalent field.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| loguru | PyPI | Use it for convenient sinks, rotation, formatting, and exception display without designing event dictionaries. |
| python-json-logger | PyPI | Use it to keep standard logging calls and replace only the formatter with JSON output. |
| Logbook | PyPI | Use it when an alternative handler and record system fits better than structlog's processor model. |
More infra guides
boto3 · opentelemetry-api · psutil · distro · @opentelemetry/api · google-cloud-storage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

