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.