Skip to main content

Command Palette

Search for a command to run...

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

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

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.

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

More from this blog

Naveen P.N's Tech Blog

94 posts