JSON logs with structlog or stdlib logging

A log line is either prose or data. Prose is written for humans reading a terminal. Data is written for machines, shipping tools, and your future self at 2am. For any service that crosses a network, pick data. JSON is the boring, universal default.

agent/nodes.py (before)
python
def sql_node(state: dict) -> dict:
    print("[sql_node] Generating and executing SQL …")
    result = run_sql_tool(
        user_question=state["user_message"],
        conversation_history=state["conversation_history"],
    )
    print(f"[sql_node] SQL: {result['sql']}")
    if result["error"]:
        print(f"[sql_node] ERROR: {result['error']}")
    else:
        print(f"[sql_node] {len(result['rows'])} rows returned.")
    return {"sql_result": result}

Four print calls per SQL turn. Readable on your laptop, useless in a log shipper.

observability/logger.py
python
import logging
import structlog

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

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

log = structlog.get_logger("agent")

# Usage
log.info("sql_node.start", route="sql", user_id=user_id)
log.info("sql_node.done", rows=len(rows), duration_ms=elapsed_ms)

structlog wraps the stdlib logger and produces JSON with consistent keys. Every log line becomes grep-friendly and dashboard-ready.

stdout (one line per event)
json
{"event": "sql_node.start", "route": "sql", "user_id": "u_482", "level": "info", "timestamp": "2026-04-14T10:22:05.112Z"}
{"event": "sql_node.done", "rows": 12, "duration_ms": 84.3, "level": "info", "timestamp": "2026-04-14T10:22:05.197Z"}

The same two events, now queryable. You can filter by event name, aggregate on duration_ms, or pipe the stream into any log backend.

For a service that is already making LLM calls measured in seconds, JSON encoding costs fractions of a millisecond per line. The cost is real only if you log inside a tight loop. In that case, sample or aggregate before logging. For LLM apps, default to JSON and move on.

Quiz: Quiz

Loading practice…