February 13, 2026 · 5 min · 912 words · Rob Washington
Table of Contents
When your application spans multiple services, containers, and regions, print("something went wrong") doesn’t cut it anymore. Structured logging transforms your logs from walls of text into queryable data.
Structured logs are data meant for machines (and humans):
1
2
3
4
5
6
7
8
9
10
{"timestamp":"2026-02-13T14:00:00Z","level":"error","message":"Failed to process order","order_id":"12345","user_email":"john@example.com","service":"order-processor","trace_id":"abc123","duration_ms":2340}
The second version lets you query: “Show me all errors for order 12345 across all services” or “What’s the p99 duration for failed orders?”
importstructlogimportuuid# Configure structlogstructlog.configure(processors=[structlog.stdlib.filter_by_level,structlog.stdlib.add_logger_name,structlog.stdlib.add_log_level,structlog.processors.TimeStamper(fmt="iso"),structlog.processors.StackInfoRenderer(),structlog.processors.JSONRenderer()],wrapper_class=structlog.stdlib.BoundLogger,context_class=dict,logger_factory=structlog.stdlib.LoggerFactory(),)log=structlog.get_logger()# Bind context that persists across log callsdefprocess_request(request):# Create a trace ID for this requesttrace_id=str(uuid.uuid4())[:8]# Bind context - all subsequent logs include these fieldslogger=log.bind(trace_id=trace_id,user_id=request.user_id,endpoint=request.path)logger.info("request_started")try:result=do_work(request,logger)logger.info("request_completed",status="success")returnresultexceptExceptionase:logger.error("request_failed",error=str(e),error_type=type(e).__name__)raise
# Service A: API Gateway@app.middleware("http")asyncdefadd_trace_id(request,call_next):trace_id=request.headers.get("X-Trace-ID")orstr(uuid.uuid4())# Store in context for this requeststructlog.contextvars.bind_contextvars(trace_id=trace_id)response=awaitcall_next(request)response.headers["X-Trace-ID"]=trace_idreturnresponse# Service B: Order Processorasyncdefprocess_order(order_data:dict,trace_id:str):logger=log.bind(trace_id=trace_id,order_id=order_data["id"])logger.info("order_processing_started")# Call downstream service, passing trace IDasyncwithhttpx.AsyncClient()asclient:response=awaitclient.post("http://inventory-service/reserve",json=order_data,headers={"X-Trace-ID":trace_id})logger.info("inventory_reserved",response_status=response.status_code)
Now when something fails, you search for the trace ID and see the complete journey across all services.
# DEBUG: Detailed diagnostic info (off in production)logger.debug("cache_lookup",key=cache_key,hit=True)# INFO: Normal operations worth recordinglogger.info("order_created",order_id=order_id,total=total)# WARNING: Unexpected but handled situationslogger.warning("rate_limit_approaching",current=950,limit=1000,user_id=user_id)# ERROR: Failures that need attentionlogger.error("payment_failed",order_id=order_id,error_code=error_code,retry_count=3)# CRITICAL: System-level failureslogger.critical("database_connection_lost",host=db_host,reconnect_attempts=5)
Structured logging takes more thought upfront, but when it’s 3 AM and something’s broken, being able to query “show me the exact sequence of events for this failed request across all services” is worth every minute invested.
Your future on-call self will thank you.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.