mrkeyoor.com_
Sun 20 Sept 11:42 UTC
PyPIInfraupdated 20 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed structlogScreenshot of structlog documentation
Install✓ · 0.2s1 package on disk · 1 MB
Importimport structlog in 0.50s · pure Python · py.typed · requires Python >=3.10
Known vulns0(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.

API stability4/5The established model still uses `get_logger`, `bind`, `unbind`, `new`, global configuration, context variables, processor chains, JSON and console renderers, and a standard logging bridge. Release 26.1 adds compatible snake-case level methods instead of replacing older calls. Runtime support does move across releases, and changing processor order or the final formatter can alter every emitted record even when call sites remain unchanged.
Docs5/5The official documentation has guided sections for processors, context variables, standard logging, performance, exceptions, testing, typing, and recipes. It diagrams the two `ProcessorFormatter` paths and warns about context behavior at sync/async boundaries. Examples explain tradeoffs rather than only showing a quick start, though each team still has to supply its own event vocabulary, sensitive-field rules, and delivery architecture.
Maintenance5/5Release 26.1.0 shipped on 2026-06-06, the repository was pushed on 2026-08-06, and GitHub reports 37 open issues and pull requests in an unarchived project. The release adds Python 3.15, Rich traceback work, and level compatibility while fixing async call-site data, leaked file references, and unpickled write loggers. Published artifact-attestation instructions also give consumers a concrete provenance check.
Ecosystem5/5The supplied registry figure is 26,931,859 weekly downloads, and GitHub reports 4,927 stars. structlog can emit directly or pass through standard logging, which lets application events share levels and handlers with dependency records. Its contextvars, typing, Rich output, testing capture, and pluggable renderers cover common service needs. Delivery integrations remain standard logging or collector concerns rather than a large structlog plugin catalog.

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.
Skip it if

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_dict

Place 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

PackageRegistryPick it when
loguruPyPIUse it for convenient sinks, rotation, formatting, and exception display without designing event dictionaries.
python-json-loggerPyPIUse it to keep standard logging calls and replace only the formatter with JSON output.
LogbookPyPIUse 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.