Intermediate

Standard Python logging gives you timestamped text messages, but when you are debugging a production incident at 2am, those messages rarely give you the context you need. Which user triggered the error? Which request ID? What was the state of the application at that moment? Structured logging solves this by making every log event a machine-readable data record — a dict of key-value pairs instead of a raw string. structlog is the leading Python library for structured logging, and it makes this easy while staying fully compatible with the standard logging module.

structlog works by building up a context dictionary as your code executes, then rendering it as JSON (or pretty-printed for development) when you emit a log event. You bind values once — like a request ID or user ID — and they appear automatically in every subsequent log message from that context. No more copy-pasting request IDs into every log call.

In this article, you will learn how to configure structlog, bind context with bind and new, set up processors for development and production output, integrate with the standard logging module, add request context in a Flask app, and build a practical async worker with rich structured logging. Install structlog with pip install structlog before starting.

Structured Logging: Quick Example

# structlog_quick.py
import structlog

log = structlog.get_logger()

log.info("user_login", user_id=42, email="alice@example.com", source="web")
log.warning("login_failed", user_id=99, reason="invalid_password", attempt=3)

Output (development mode — pretty printed):

2026-05-02 10:00:00 [info     ] user_login         email=alice@example.com source=web user_id=42
2026-05-02 10:00:00 [warning  ] login_failed       attempt=3 reason=invalid_password user_id=99

Each log call takes a positional event string followed by keyword arguments that become fields in the structured record. In development, structlog renders these in a readable format; in production, you configure it to output JSON. The same code, different renderers — you never change your log calls based on environment.

What Is structlog and Why Use It?

structlog replaces the pattern of building log messages from string concatenation (f"User {user_id} failed to login with error {error}") with a pattern of logging discrete fields. This makes logs searchable and filterable in tools like Elasticsearch, Datadog, or CloudWatch Logs Insights — you can query user_id=42 and get all events for that user, rather than running regex searches over raw strings.

Featurestdlib loggingstructlog
Output formatText stringsJSON or text, configurable
Context bindingLoggerAdapter (verbose)bind() / new() (clean)
Processors/middlewareHandlers and FiltersProcessor pipeline
stdlib compatibilityNativeFull integration available
Async supportThread-safe onlyasync-aware with asyncio

structlog does not replace stdlib logging — it wraps around it. In most production setups, structlog acts as the frontend you write to, while stdlib logging handles the backend (file handlers, syslog, etc.). This means you can adopt structlog incrementally in an existing codebase.

Sudo Sam monitoring structured log streams
event=payment_failed user_id=42 — now FIND that in your grep.

Configuring structlog for Development and Production

structlog is configured once at application startup. The key is the processor chain — a list of functions that transform the log event dict before rendering. Here is a complete configuration for both environments:

# structlog_config.py
import structlog
import logging
import sys
import os

def configure_logging() -> None:
    """Configure structlog for the current environment."""
    is_production = os.getenv('ENV', 'development') == 'production'

    shared_processors = [
        structlog.contextvars.merge_contextvars,       # Thread/async context
        structlog.stdlib.add_log_level,                # Add 'level' field
        structlog.stdlib.add_logger_name,              # Add 'logger' field
        structlog.processors.TimeStamper(fmt='iso'),   # ISO 8601 timestamp
        structlog.processors.StackInfoRenderer(),      # Stack traces
        structlog.processors.format_exc_info,          # Exception formatting
    ]

    if is_production:
        # JSON output for log aggregation tools
        renderer = structlog.processors.JSONRenderer()
    else:
        # Human-readable colored output for terminals
        renderer = structlog.dev.ConsoleRenderer(colors=True)

    structlog.configure(
        processors=shared_processors + [renderer],
        wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
        context_class=dict,
        logger_factory=structlog.PrintLoggerFactory(),
        cache_logger_on_first_use=True,
    )

configure_logging()
log = structlog.get_logger("myapp")

log.info("app_started", version="1.2.3", environment=os.getenv('ENV', 'development'))
log.debug("config_loaded", database="postgres", cache="redis")

Output (development):

2026-05-02T10:00:00Z [debug    ] config_loaded      cache=redis database=postgres logger=myapp
2026-05-02T10:00:00Z [info     ] app_started        environment=development logger=myapp version=1.2.3

Output (production, ENV=production):

{"cache": "redis", "database": "postgres", "event": "config_loaded", "level": "debug", "logger": "myapp", "timestamp": "2026-05-02T10:00:00Z"}
{"environment": "production", "event": "app_started", "level": "info", "logger": "myapp", "timestamp": "2026-05-02T10:00:00Z", "version": "1.2.3"}

Binding Context with bind() and new()

The most powerful feature of structlog is context binding. Use bind() to add fields that persist for the lifetime of a logger, and new() to create a fresh logger with a clean context:

# structlog_bind.py
import structlog

log = structlog.get_logger()

def process_order(order_id: str, user_id: int) -> None:
    # Bind context once -- all subsequent logs from this logger include it
    order_log = log.bind(order_id=order_id, user_id=user_id)

    order_log.info("order_processing_started")

    # Further bind additional context
    item_log = order_log.bind(item_count=3, total_amount=99.99)
    item_log.info("items_validated")

    try:
        # Simulate an operation
        if order_id == "ORD-999":
            raise ValueError("Invalid payment method")
        item_log.info("payment_processed", gateway="stripe")
    except ValueError as e:
        item_log.error("payment_failed", error=str(e), retry=False)
        raise

    order_log.info("order_processing_complete")

process_order("ORD-101", user_id=42)

Output:

2026-05-02 [info ] order_processing_started  order_id=ORD-101 user_id=42
2026-05-02 [info ] items_validated           item_count=3 order_id=ORD-101 total_amount=99.99 user_id=42
2026-05-02 [info ] payment_processed         gateway=stripe item_count=3 order_id=ORD-101 total_amount=99.99 user_id=42
2026-05-02 [info ] order_processing_complete order_id=ORD-101 user_id=42

Notice how order_id and user_id appear in every log message without being passed repeatedly. When the error occurs, the full context is already present — you see exactly which order failed, for which user, with which item count. This is what makes structured logging so powerful for debugging production issues.

Debug Dee filtering JSON log data
bind() once. Every log carries context forward. No excuses.

Integrating with stdlib logging

In large applications, you often have third-\�HX��\�Y\�]\�H�X���O����[�� ���O����X����[�[�\��\��H���[����\��[H��Y�]�\[[�H���]�[��[�HH[�Y�YY��X�\�Y����X[N� �����O���O����X�����X��B�[\ܝ��X��š[\ܝ���[�‚���ۙ�Y�\�H��X�����ܚ��U�X����[�œ��X��˘�ۙ�Y�\�J����\��ܜ�Vˆ��X��˜�X���[\�؞W�]�[ ���X��˜�X��Y����]�[ ���X��˜���\��ܜ˕[YT�[\\��]I�\���K���X��˜�X����][ۘ[\��[Y[�ћܛX]\� K���X��˜���\��ܜ˔�X��[��ԙ[�\�\� K���X��˜���\��ܜ˙�ܛX]�^��[������X��˜�X�����\��ܑ�ܛX]\��ܘ\ٛܗٛܛX]\��K����\�٘X�ܞO\��X��˜�X�����\��X�ܞJ K�ܘ\\���\��\��X��˜�X����[����\���X�W����\��ۗٚ\���\�OU�YK�B����ۙ�Y�\�H�X���ܝ�\����X������[�\�\���ܛX]\�H��X��˜�X�����\��ܑ�ܛX]\�����\��܏\��X��˙]���ۜ��T�[�\�\� K�B�[�\�H���[�˔��X[R[�\� B�[�\���]�ܛX]\��ܛX]\�B��������\�H���[�˙�]���\� B��������\��Y[�\�[�\�B��������\���]]�[ ���[�ˑP�Q�B����������X���[��X��������Y���X��›��H��X��˙�]����\� B��˚[������X����]�[����\ۙ[�H�^X\�B���\� \\�HX��\�H\�[���X����[�œ�X����H���[�˙�]���\���\]Y\�ȊB��X���˚[�����U \ȋ�΋��\K�^[\K���K�\�\�ȊB� ���O� ��O������ۙϓ�]]� ���ۙϏ ����O���O�� ��L KL � L� � ��[���H��X����]�[���\ۙ[�[^X\�� ��L KL � L� � ��[���H�U΋��\K�^[\K���K�\�\�����\�\�\]Y\� ���O� ��O��� �YH��X[ Y^[\H���X[ SY�H^[\N��X��ܛ�[�\���ܚ�\��]�[�۝^ � ����KKHSPQ�W�P�R�T��\��]K�ܝ����H�^K�Z�H��Y��Y[�[YHZ\��YHܚ[��Y�^Y\�Y[����YH�]]ۈۘZ�H���\���YH�\���[���]HۙXZ�\��TЈ[�X\� �H\�^�]YH[ۚ]ܚ[��H�[و���[������X[\�[�H�]\�\�X��X�ܞH�۝�����H�]\�[[�۝�^[܈�[�[����[���]\�[�X�]ܜˈ��^�\�X�K��YH[���\H��[�K��[�\�]H[�H�[Hو�\��ۈ��Y]�Y�\� ���] �\��ܚ��� � ��^\�����K����X���][�\��[ \�YY��ܚ[�ˈ�\[ێ��\�[��\����]�]�۝^�[�[�Έ���X�ˈ�]]�\�H�Yۘ[ �� KO����\��ܚ�\����\��\�\������HH]Y]YH[����]�\�H�\�]�[��X�\�Y�۝^ XZ�[��]�]�X[��X�H[�H\������\]HY�X�X�H[���X�[ۈ��Ώ �����O���O��\����ܚ�\��B�[\ܝ��X��š[\ܝ[YB�[\ܝ]ZY�[\ܝ�[��B����H��X��˙�]����\� B��Y����\���\��\���Y���\���\N���^[�Y�X� H O�X���������\��H�[��H\���]�[��X�\�Y���[�ˈ����\�����H�˘�[� �\���Y]\���Y �\���\O]\���\K��ܚ�\��YH��ܚ�\�LH�� B�\����˚[����\����\�Y�^[�Y��^�O[[���^[�Y JJB���\�H[YK�[ۛ�ۚX� B��N����[][]HY��\�[�\��\\ˆY�\���\HOH�[XZ[���[YK��Y\ � JB��X�\Y[�H^[�Y ��] �ȋ�[�ۛ�ۈ�B�\����˚[����[XZ[��[���X�\Y[�\�X�\Y[� �X��X�\^[�Y ��] ��X��X��JB��\�[HȜ�]\Ȏ���[���Y\��Y�W�Y����\��^�]ZY �]ZY K�^Ύ_H�B��[Y�\���\HOH��\ܝ���������[�H�[��K��[�[� L  L B�\����˚[�����\ܝ��[�\�][�ȋ������[�\������[� B�[YK��Y\ � �B��\�[HȜ�]\Ȏ����\]H�����Ȏ�������[� ��ܛX]�����B��[�N���Z\�H�[YQ\��܊��[�ۛ�ۈ\��\N��\���\_H�B��\�][ۗ�\�H[� [YK�[ۛ�ۚX� H H�\� H � L B�\����˚[����\�����\]Y�\�][ۗ�\�Y\�][ۗ�\� ���\�[ B��]\���\�[��^�\^�\[ۈ\�N��\�][ۗ�\�H[� [YK�[ۛ�ۚX� H H�\� H � L B�\����˙\��܊�\��٘Z[Y�\��܏\��JK\�][ۗ�\�Y\�][ۗ�\�^��[���U�YJB��Z\�B����[][]H���\��[��H]Y]YB�\���Hˆ �[XZ[�ȝȎ��[X�P^[\K���H���X��X�����[��YHH�JK� ��\ܝ�Ȝ�\ܝ�Y����L� H��\�[����[۝H�JK� �[XZ[�ȝȎ���ؐ^[\K���H���X��X����[���X�H�JK�B���܈\���\K^[�Y[�\��΂�\���YH��\��^�]ZY �]ZY K�^Ύ_H�����\���\��\���Y \���\K^[�Y B� ���O� ��O������ۙϓ�]]� ���ۙϏ ����O���O��[���H\����\�Y\���Y]\��XLX���� \���\OY[XZ[�ܚ�\��Y]�ܚ�\�LH^[�Y��^�OM B��[���H[XZ[��[�\���Y]\��XLX���� \���\OY[XZ[�ܚ�\��Y]�ܚ�\�LH�X�\Y[�X[X�P^[\K���H�X��X�U�[��YHB��[���H\�����\]Y\���Y]\��XLX���� \���\OY[XZ[�ܚ�\��Y]�ܚ�\�LH\�][ۗ�\�LL��]\�\�[�Y\��Y�W�Y[\��Y��N ��Y��[���H\����\�Y\���Y]\��YMY����\���\O\�\ܝ�ܚ�\��Y]�ܚ�\�LH^[�Y��^�OL���[���H�\ܝ��[�\�][��\���Y]\��YMY����\���\O\�\ܝ�ܚ�\��Y]�ܚ�\�LH������[�L� –�[���H\�����\]Y\���Y]\��YMY����\���\O\�\ܝ�ܚ�\��Y]�ܚ�\�LH\�][ۗ�\�L�H�]\�X��\]H����L� ��ܛX]\�� ���O� ��O��� �YH��\H����\]Y[�H\��Y]Y\�[ۜ� � ���� �YH��\KZ��ۈ�����H�]]��ӈ���[���X�[ۏ� � ς���\X�H��O���X��˙]���ۜ��T�[�\�\� O ���O��]��O���X��˜���\��ܜ˒��Ӕ�[�\�\� O ���O�[�[�\����\��܈�Z[��]X�H[��\�ۛY[��XH[�[��\�ۛY[��\�XX�HZ�H��O�S��\��X�[ۏ ���O�[��ۙ�Y�\�HH�[�\�\�]�\�\ �H���[�[�[�\�\X�][ۈ��H�^HY[�X�[ KHۛHH�]]�ܛX]�[��\ˈX[�HX[\�\�H��O���X��˜���\��ܜ˒��Ӕ�[�\�\� O ���O�]�\�]�\�H[��ۙ�Y�\�HZ\�\�Z[�[[][]܈��]K\�[���ӈ�܈��[]�[�Y[� � ���� �YH��\KX�۝^�\�ȏ����\��۝^�[�[���ܚ��]\�[����O� � ς���܈\�[��[�\X�][ۜ�\�H��O���X��˘�۝^�\�˘�[���۝^�\�� O ���O�[���O���X��˘�۝^�\�˛Y\��W��۝^�\�� ���O����\��܈[��XYو��O��[� O ���O��H�۝^�\��\��X�\�\�]ۉ����O��۝^�\�ː�۝^�\� ���O�[�\�H�� �X�\����Y�XX�\�[��\��[��\���XZ��۝^�]�Y[��ۘ�\��[��\]Y\�ˈ\�\�ܚ]X�[�܈�X���[Y]�ܚ��Z�H�\�TH܈Z[��\�H][\H�\]Y\���[��ۘ�\��[�K� ���� �YH��\K\\��ܛX[��H���\���X�\�Y���[��]�HH\��ܛX[��H���� � ς����X���Y�Z[�[X[ݙ\�XY��\\�Y��X����[�� KH�[��X\���\X�[H��� KLMIH���\�[��\�H�X�ۈ�]ˈ�܈[��\X�][ۜ�\�\��Y�Y�X�K�Y�[�H\�H���[��[��YHY����[ݙHH���[�]�YHH��[��]�[�\�]�[�ˈ[�H�[�[��\�H��O��X�W����\��ۗٚ\���\�OU�YO ���O�[��ۙ�Y�\�][ۋ�X��X�\�H���\��܈�Z[�Y�\�H�\���[[�]��Y��\X]Y\[[�H�ۜ��X�[ۋ� ���� �YH��\KY�[\������H�[\���]�[�[���X���� � ς���[�\�[����X������]]�H���\��X�ܞK�ۙ�Y�\�HHZ[�[][H]�[�]��O���X��˛XZ�Wٚ[\�[��؛�[�����\����[�˒S���O ���O�[�H��O�ܘ\\���\�� ���O�\�[Y]\��\�ܙX]\�H���\��\���\�HX�Y��[��[��S���\�H��[��]�\����� ��[�\�[��H�X�[�Yܘ][ۋ�۝��]�[���Y�H�X���O����[�˘�\�X��ۙ�Y�]�[[���[�˒S���O ���O�\�\�X[ � ���� �YH��\KY^\�[�ȏ��[�HZYܘ]H[�^\�[���ڙX����X���ܘYX[O� � ς��Y\ˈ�ۙ�Y�\�H��X������]H��Y��X����[�� �YHH�X�[�Yܘ][ۈ�X�[ۊK[��\X�H[�]�YX[��O����\��[����Y\��Y�H \ȋ�[YJO ���O��[��]��O��˚[����]�[�ۘ[YH��^O]�[YJO ���O�ۙH[�[H]H[YK�\�[��H�[��][ۋ��]\����ܚ�[��]]�H�[YH\�[�][ۋ�\�XZ�\���X���Y�[ۈH�\��\�\��ܘYX[�Y�X�܈�]\�[�H�Y�X�[���]ܚ]K� ���� �YH��ۘ�\�[ۈ���ۘ�\�[ۏ � �����[�H���ۛ�����\�H��X����܈��X�[ۋYܘYH��X�\�Y���[��[�]ۋ��H�ݙ\�Y�\�X�\�Y�H[�]�[� X�\�Y���[��ۙ�Y�\�[���\\�]H]�[�Y[�[���X�[ۈ�[�\�\���[�[��\��\�[��۝^�]��O��[� O ���O���]�\�H��Y\��Y�H�\��Y\��[]�[��Y[�]]�X]X�[K[�Yܘ][���]�X����[����\\�H\� \\�HX��\�H�]] [�H��\]H�X��ܛ�[�\���ܚ�\��]�[�۝^��Y�][ۋ� �����H�Y��\��[����H��X�����Y\��[�[�H\�HX�Y��[��H��X�[ۈ[��Y[� KH[��XYو�X\��[����Y�^���[�H]Y\�H[�\���Y�ܙY�][ۈ���܈��O�\���YXX��L�� ���O�܈��O�\�\��YM � ���O�[�[��[�H�YHH��\]H[Y[[�H�܈][�]K�]\�HY��\�[��H�]�Y[���X�\�Y[�[���X�\�Y���[��[��X�X�K� ������YHHH�Y�H�΋����˜��X��˛ܙ��[���X�Kȏ���X�����[Y[�][ۏ �O��܈Y�[��Y���\��܈�ۙ�Y�\�][ۋ\�[��]\���[�[�Yܘ][ۈ�ZY\��܈�X�Y�X���[Y]�ܚ��Z�H�[����\��[��\�TK� ���� �YH��[]Y X\�X�\ȏ��[]Y\�X�\� � ���[��O�H�Y�H�΋��]ۚ�����ܘ[K���K���]�]\�K]K\]ۋ[���[��[[�[KY�܋X\X�][ۋ[���[��ȏ����\�HH]ۈ���[��[�[H�܈\X�][ۈ���[�� �O� �O��O�H�Y�H�΋��]ۚ�����ܘ[K���K���]�]\�K\]ۋ[��\�KY�܋[[�\��X\X�][ۋ[���[��ȏ����\�H]ۈ��\�H�܈[�\��\X�][ۈ���[�� �O� �O��O�H�Y�H�΋��]ۚ�����ܘ[K���K���]�]\�K\]ۋ]�X�X�X��[[�[KY�܋Y\��܋\�\ܝ[��ȏ����\�H]ۈ�X�X�X��[�[H�܈\��܈�\ܝ[��

Pyro Pete monitoring async log streams
Async tasks without context binding: good luck. With it: pure signal.
�O� �O�� �[����]���^V��]�����[[�V��]��ܛ��V��]����X�[ۗB�KKH ���]�K�X�Z�\� KO�