Stop Writing Amateur Python Logs: Use structlog Instead
Learn how to replace flat, unstructured logs with structured, context-rich logging using structlog in Python. This guide walks you through setup, production configuration, and real-world usage patterns.
You know that feeling when you're debugging a production issue, and your logs look like something a confused robot wrote? Just a bunch of [INFO] User logged in messages with no structure, no context, and no way to search them properly? I've been there, and it hurts.
The standard Python logging module is fine for simple scripts, but it wasn't built for modern applications. Your logs should be structured data, not random strings. That's where structlog comes in.
Why structlog Matters
Think about what good logging looks like:
- Instead of
"User 12345 logged in at 2024-01-15", you want{"event": "login", "user_id": 12345, "timestamp": 1705322400} - Instead of digging through text files, you should be querying your logs with tools like Elasticsearch or Datadog
- Instead of guessing what happened during a bug, you should see exactly what state your application was in
structlog makes this easy. It's not replacing the standard library - it wraps it with actual intelligence.
What You'll Need
Before we start, make sure you have Python 3.7+ and pip:
pip install structlog
That's it. One dependency. Let's build something useful.
The Absolute Basics
Here's the simplest structlog setup that actually makes sense:
import structlog
logger = structlog.get_logger()
logger.info("user_login", user_id=42, ip="192.168.1.1")
Run that, and you get:
2024-01-15 14:30:00 [info ] user_login user_id=42 ip=192.168.1.1
Already better than what you're probably using. Every log event is a dictionary with named fields.
Setting It Up Properly
Let's be real - you're going to use this in production. Here's the configuration pattern PythonSkillset recommends for most applications:
import structlog
import logging
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.dev.ConsoleRenderer() # Beautiful colored output for local dev
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
This gives you ISO timestamps, proper exception formatting, and clean output. For production, swap ConsoleRenderer with JSONRenderer:
structlog.processors.JSONRenderer()
Suddenly, every log line is JSON. Your DevOps team will love you.
Real World Example: Web Application Logging
Let's build something practical. Here's a Flask route that logs meaningful data:
from flask import Flask, request
import structlog
app = Flask(__name__)
logger = structlog.get_logger()
@app.route('/api/orders', methods=['POST'])
def create_order():
log = logger.new(request_id=request.headers.get('X-Request-ID', 'unknown'))
try:
data = request.get_json()
log = log.bind(user_id=data.get('user_id'))
# Simulate order creation
order_id = create_order_in_db(data)
log.info("order_created",
order_id=order_id,
amount=data.get('amount'),
currency=data.get('currency', 'USD'))
return {"order_id": order_id}, 201
except ValueError as e:
log.error("order_validation_failed", error=str(e))
return {"error": str(e)}, 400
except Exception as e:
log.error("order_creation_failed", error=str(e), exc_info=True)
return {"error": "Internal server error"}, 500
Notice what's happening: - We bind request_id to the logger context - We add user_id as processing continues - Every log line automatically includes everything bound to context - Exceptions get full tracebacks included
The Context Chain Pattern
This is where structlog really shines. You can pass context through your entire request:
def process_payment(log, order_id, amount):
log = log.bind(payment_processor="stripe")
try:
charge_id = stripe_charge(amount)
log = log.bind(charge_id=charge_id)
log.info("payment_successful")
return charge_id
except stripe.CardError as e:
log.error("payment_declined", code=e.code)
raise
The logger carries context through every function. No more manually passing request IDs everywhere.
When Things Go Wrong
Here's what a real debugging session looks like with structlog:
try:
result = complex_operation()
logger.info("operation_complete", result=result, duration_ms=calc_duration())
except Exception:
logger.exception("operation_crashed", input_data=last_input)
logger.exception() automatically captures the full traceback and includes it in your structured log. In JSON format:
{
"event": "operation_crashed",
"input_data": {...},
"exception": "ValueError: Invalid input...",
"timestamp": "2024-01-15T14:30:00Z",
"logger": "myapp",
"level": "error"
}
Your log aggregator can now filter, search, and alert on specific fields.
Performance Tip
structlog is fast because it defers string formatting until absolutely necessary. But if you're logging inside hot loops, consider:
if logger.isEnabledFor(logging.DEBUG):
logger.debug("expensive_debug", data=build_large_structure())
This prevents building expensive debug data when your log level is set to INFO.
What You Should Actually Do Now
- Install structlog in your existing project:
pip install structlog - Add the configuration block to your app's entry point
- Replace
import loggingwithimport structlog - Start binding context instead of writing string messages
Your future self, debugging at 3 AM, will thank you.
Remember: logs aren't just for showing that something happened. They're for showing exactly what happened, in every context, so you can fix problems before your users even notice.
At PythonSkillset, we've seen teams reduce debugging time by 60% just by switching to structured logging. Give it a try on your next deployment.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.