Steven's Knowledge

Logging & Observability

Production-grade logging -- the logging hierarchy, library vs application config, structured JSON logs, contextvars trace ids, and OpenTelemetry

Logging & Observability

print does not survive contact with production. The logging module is hierarchical and its configuration belongs to the application, not the library. This page covers doing it right: lazy logging, structured JSON output, per-request context via contextvars, and the three pillars of observability with OpenTelemetry.

Logging and Observability

print does not survive contact with production. The logging module is hierarchical: loggers form a tree by dotted name, records propagate up to handlers, and configuration belongs to the application -- not the library.

import logging

# Library code: get a named logger, never configure handlers or levels.
log = logging.getLogger(__name__)     # e.g. "myservice.payments"
log.info("charge created", extra={"amount": 1000, "currency": "nzd"})

# Application entry point: configure ONCE, at startup.
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")

The classic mistakes: calling basicConfig from a library (it hijacks the host app's logging), and using f-strings in log calls -- log.info(f"user {expensive()}") evaluates expensive() even when the level is disabled. Pass args lazily instead: log.info("user %s", expensive).

For services, prefer structured (JSON) logging so logs are queryable, and attach request context via contextvars so every line in a request carries its trace id:

import logging, json
from contextvars import ContextVar

trace_id: ContextVar[str] = ContextVar("trace_id", default="-")

class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        return json.dumps({
            "ts": self.formatTime(record),
            "level": record.levelname,
            "logger": record.name,
            "msg": record.getMessage(),
            "trace_id": trace_id.get(),
            **getattr(record, "extra_fields", {}),
        })

Beyond logs, the three pillars of observability are logs, metrics, and traces. OpenTelemetry is the vendor-neutral standard: it auto-instruments FastAPI, httpx, and SQLAlchemy to emit distributed traces, so you can follow a single request across services. Wire it once at startup; the value compounds the moment you have more than one service.

On this page