Skip to main content

Python’s structlog: Modern Structured Logging for Clean, JSON-Ready Logs

Updated 4 min readMarkdown
Python’s structlog: Modern Structured Logging for Clean, JSON-Ready Logs

Introduction

Logging is one of the most overlooked yet critical aspects of building reliable and scalable applications. In traditional Python applications, developers commonly rely on the built-in logging module, sprinkling simple log messages like print() or logger.info() throughout their code.

However, as systems become distributed, containerized, cloud-native, or integrated into microservices, structured logging has become a necessity. Structured logs — logs with machine-readable key-value data instead of plain text — allow powerful querying, filtering, and visualization in modern observability tools like ELK (Elasticsearch-Logstash-Kibana), Loki, Datadog, and AWS CloudWatch.

This is where structlog comes in. It’s a modern, high-performance Python library for structured logging that integrates cleanly with the existing logging module but transforms your logs into JSON-ready, structured messages.

This article offers a deep dive into structlog, covering:

  • Why structured logging matters

  • How structlog works under the hood

  • Integrating it with Python’s logging

  • Custom processors and contextual logging

  • JSON logs for modern deployments

  • Performance considerations

  • Production-ready logging setups with structlog

Why Structured Logging?

In traditional applications, logs look like this:

[INFO] User Vinay logged in from IP 192.168.1.1

In modern, distributed systems, this is insufficient. Instead, you'd want:

{
  "level": "info",
  "event": "user_logged_in",
  "user": "Vinay",
  "ip": "192.168.1.1",
  "timestamp": "2025-06-20T08:00:00Z"
}

Advantages:

  • Easily query logs by fields (user, IP, timestamp)

  • Structured, consistent logs for dashboards

  • Machine-readable (JSON/YAML/Key-Value pairs)

  • Better correlation in distributed tracing

  • Simplifies debugging, alerting, and anomaly detection

What is structlog?

structlog is a Python logging library designed for structured logging.

Key features:

  • Lightweight and fast

  • Integrates with logging or works independently

  • Supports context-aware, thread-safe logging

  • Serializes logs into JSON or custom formats

  • Pluggable processor pipeline to enrich or transform log events

  • Production-ready for microservices and cloud apps

Installing structlog

Install using pip:

pip install structlog

For JSON logging:

pip install structlog[json]

Basic Usage

A simple example:

import structlog

log = structlog.get_logger()
log.info("user_logged_in", user="Vinay", ip="192.168.1.1")

Output:

2025-06-20 08:00.00 [info     ] user_logged_in          user=Vinay ip=192.168.1.1

This is human-readable, but you can swap the processor chain to output JSON as well.

How structlog Works

At its core, structlog separates concerns:

  • Logger binding: Attach context (key-value data) to logger instances

  • Event processors: Modify, filter, or enrich log events

  • Renderer: Transform the final log event into a string or JSON before emitting it

This pipeline design makes it extremely flexible.

Integrating structlog with Python’s logging Module

To capture logs from third-party libraries or use existing handlers like RotatingFileHandler, integrate with logging.

Example setup:

import logging
import structlog

logging.basicConfig(
    format="%(message)s",
    stream=sys.stdout,
    level=logging.INFO
)

structlog.configure(
    processors=[
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.add_log_level,
        structlog.processors.KeyValueRenderer(key_order=["timestamp", "level", "event"]),
    ],
    context_class=dict,
    logger_factory=structlog.stdlib.LoggerFactory(),
    wrapper_class=structlog.stdlib.BoundLogger,
    cache_logger_on_first_use=True
)

log = structlog.get_logger()

log.info("user_logged_in", user="Vinay", ip="192.168.1.1")

Output:

timestamp=2025-06-20T08:00:00Z level=info event=user_logged_in user=Vinay ip=192.168.1.1

Structured JSON Logs

In modern deployments, JSON logs are preferred for log aggregators.

structlog.configure(
    processors=[
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.add_log_level,
        structlog.processors.JSONRenderer()
    ],
    ...
)

log = structlog.get_logger()

log.info("user_logged_in", user="Vinay", ip="192.168.1.1")

Output:

{
  "timestamp": "2025-06-20T08:00:00Z",
  "level": "info",
  "event": "user_logged_in",
  "user": "Vinay",
  "ip": "192.168.1.1"
}

Perfect for ELK stack or CloudWatch ingestion.

Adding Contextual Metadata

Attach global context to loggers, avoiding repeated parameters.

log = structlog.get_logger().bind(service="authentication")

log.info("user_logged_in", user="Vinay")
log.error("invalid_token", user="Kumar")

Every log will now include service=authentication.

Dynamic context via threadlocal is possible using structlog.threadlocal.wrap_dict.

Custom Event Processors

Create processors to modify events at runtime.

Example: Add a UUID to every log.

import uuid

def add_request_id(logger, method_name, event_dict):
    event_dict["request_id"] = str(uuid.uuid4())
    return event_dict

structlog.configure(
    processors=[
        add_request_id,
        structlog.processors.JSONRenderer()
    ],
    ...
)

Now every log has a unique request_id.

Production Logging Setup Example

A complete logging pipeline for a FastAPI microservice or API.

import logging
import structlog
from structlog.stdlib import LoggerFactory
from structlog.processors import JSONRenderer, TimeStamper, add_log_level
from structlog.threadlocal import wrap_dict

logging.basicConfig(
    format="%(message)s",
    level=logging.INFO
)

structlog.configure(
    processors=[
        TimeStamper(fmt="iso"),
        add_log_level,
        JSONRenderer()
    ],
    context_class=wrap_dict(dict),
    logger_factory=LoggerFactory(),
    wrapper_class=structlog.stdlib.BoundLogger,
    cache_logger_on_first_use=True
)

log = structlog.get_logger().bind(service="api")

log.info("server_started", port=8080)

This setup:

  • Emits JSON logs

  • Includes timestamps and levels

  • Supports dynamic context for API endpoints and job workers

Performance Considerations

  • structlog is fast and efficient for JSON logging.

  • Disabling logs in production:
    Use structlog.dev.ConsoleRenderer() in dev and JSONRenderer() in prod.

  • Can integrate with lru_cache or third-party logging transports.

Migration From logging

Convert existing logging calls easily:

Before:

logger.info("User %s logged in from %s", user, ip)

After:

log.info("user_logged_in", user=user, ip=ip)

Fewer format errors, better structure, no positional argument issues.

Testing Logs

Unit test log outputs via:

from structlog.testing import capture_logs

with capture_logs() as logs:
    log.info("test_event", status="ok")

assert logs[0]["status"] == "ok"

Supports clean, testable logging codebases.

Conclusion

structlog is one of Python’s most valuable libraries for modern, structured, JSON-ready logging — yet surprisingly underused in mainstream projects.

It’s a no-brainer choice for:

  • Microservices

  • FastAPI / Django apps

  • Serverless functions

  • Asynchronous job queues

  • Cloud-native APIs

  • Data pipelines and ETL jobs

About the author

  • I am a Tech Enthusiast having 13+ years of experience in 𝐈𝐓 as a 𝐂𝐨𝐧𝐬𝐮𝐥𝐭𝐚𝐧𝐭, 𝐂𝐨𝐫𝐩𝐨𝐫𝐚𝐭𝐞 𝐓𝐫𝐚𝐢𝐧𝐞𝐫, 𝐌𝐞𝐧𝐭𝐨𝐫, with 12+ years in training and mentoring in 𝐒𝐨𝐟𝐭𝐰𝐚𝐫𝐞 𝐄𝐧𝐠𝐢𝐧𝐞𝐞𝐫𝐢𝐧𝐠, 𝐃𝐚𝐭𝐚 𝐄𝐧𝐠𝐢𝐧𝐞𝐞𝐫𝐢𝐧𝐠, 𝐓𝐞𝐬𝐭 𝐀𝐮𝐭𝐨𝐦𝐚𝐭𝐢𝐨𝐧 𝐚𝐧𝐝 𝐃𝐚𝐭𝐚 𝐒𝐜𝐢𝐞𝐧𝐜𝐞. I have 𝒕𝒓𝒂𝒊𝒏𝒆𝒅 𝒎𝒐𝒓𝒆 𝒕𝒉𝒂𝒏 10,000+ 𝑰𝑻 𝑷𝒓𝒐𝒇𝒆𝒔𝒔𝒊𝒐𝒏𝒂𝒍𝒔 and 𝒄𝒐𝒏𝒅𝒖𝒄𝒕𝒆𝒅 𝒎𝒐𝒓𝒆 𝒕𝒉𝒂𝒏 500+ 𝒕𝒓𝒂𝒊𝒏𝒊𝒏𝒈 𝒔𝒆𝒔𝒔𝒊𝒐𝒏𝒔 in the areas of 𝐒𝐨𝐟𝐭𝐰𝐚𝐫𝐞 𝐃𝐞𝐯𝐞𝐥𝐨𝐩𝐦𝐞𝐧𝐭, 𝐃𝐚𝐭𝐚 𝐄𝐧𝐠𝐢𝐧𝐞𝐞𝐫𝐢𝐧𝐠, 𝐂𝐥𝐨𝐮𝐝, 𝐃𝐚𝐭𝐚 𝐀𝐧𝐚𝐥𝐲𝐬𝐢𝐬, 𝐃𝐚𝐭𝐚 𝐕𝐢𝐬𝐮𝐚𝐥𝐢𝐳𝐚𝐭𝐢𝐨𝐧𝐬, 𝐀𝐫𝐭𝐢𝐟𝐢𝐜𝐢𝐚𝐥 𝐈𝐧𝐭𝐞𝐥𝐥𝐢𝐠𝐞𝐧𝐜𝐞 𝐚𝐧𝐝 𝐌𝐚𝐜𝐡𝐢𝐧𝐞 𝐋𝐞𝐚𝐫𝐧𝐢𝐧𝐠. I am interested in 𝐰𝐫𝐢𝐭𝐢𝐧𝐠 𝐛𝐥𝐨𝐠𝐬, 𝐬𝐡𝐚𝐫𝐢𝐧𝐠 𝐭𝐞𝐜𝐡𝐧𝐢𝐜𝐚𝐥 𝐤𝐧𝐨𝐰𝐥𝐞𝐝𝐠𝐞, 𝐬𝐨𝐥𝐯𝐢𝐧𝐠 𝐭𝐞𝐜𝐡𝐧𝐢𝐜𝐚𝐥 𝐢𝐬𝐬𝐮𝐞𝐬, 𝐫𝐞𝐚𝐝𝐢𝐧𝐠 𝐚𝐧𝐝 𝐥𝐞𝐚𝐫𝐧𝐢𝐧𝐠 new subjects.

No comments yet. Be the first to comment.

More from this blog