← назад к разделу

Когда сервис в продакшне ведёт себя странно — тормозит, отдаёт ошибки, падает — первый вопрос: «что происходит?» Если ответить нечем, остаётся угадывать. Наблюдаемость — это три инструмента (логи, трейсы, метрики) плюс health-checks, которые дают конкретные ответы. FastAPI не навязывает реализацию, но хорошо стыкуется со стандартными инструментами Python.

Логи: от print() к структурным событиям

Когда пишут print("что-то пошло не так"), это бесполезно в продакшне: нет времени, нет контекста, нет способа найти все строки одного запроса. Система сбора логов (Loki, Elasticsearch) ожидает JSON-строки с полями, которые можно фильтровать и агрегировать.

Правильный лог — это событие с атрибутами, а не фраза в консоли:

import logging

logger = logging.getLogger("app")

@router.post("/", status_code=201)
async def create_product(body: CreateProductRequest, handler: CreateProductHandlerDep):
    product = await handler.handle(body.to_command())
    logger.info("product_created", extra={"product_id": product.id})
    return ProductResponse.from_domain(product)

Формат JSON задаётся форматтером один раз на старте — через python-json-logger или structlog. После этого каждый вызов logger.info(...) уходит как JSON-строка с полем event, временем, уровнем и всеми переданными extra-полями.

Ключевое поле — идентификатор запроса (request_id). Его проставляют в middleware и добавляют в каждую строку лога. Тогда все события одного запроса можно найти за секунду одним фильтром.

Трейсинг: куда уходит время запроса

Лог говорит «что случилось», но не «где потеряно время». Запрос может провести 200 мс в базе данных, 50 мс в HTTP-вызове к соседнему сервису и 10 мс в бизнес-логике — лог этого не покажет. Трейс показывает.

Трейс — это дерево отрезков (spans): каждый span фиксирует начало и конец операции. Смотришь на трейс — видишь, где реально уходит время.

Стандарт — OpenTelemetry. Для FastAPI есть готовое авто-инструментирование:

from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

FastAPIInstrumentor.instrument_app(app)

Одна строка добавляет span на каждый HTTP-запрос. Для SQLAlchemy и HTTP-клиентов есть отдельные инструментирующие пакеты — они добавляют вложенные spans, так что трейс получается сквозным: от входящего запроса до SQL-запроса в базу.

Готовые трейсы уходят в коллектор (Jaeger, Grafana Tempo) — через настройку экспортёра, не через изменение кода приложения.

Метрики: что происходит в целом

Трейс отвечает на вопрос «что случилось с этим конкретным запросом». Метрики отвечают на другой вопрос: «как сервис ведёт себя в целом прямо сейчас?»

Метрика — это число во времени: сколько запросов в секунду, какой процент отвечает дольше 500 мс, сколько ошибок за последние пять минут. По метрикам настраивают алерты.

Стандарт — Prometheus: он периодически забирает метрики с эндпоинта /metrics. Для FastAPI есть готовый инструментатор:

from prometheus_fastapi_instrumentator import Instrumentator

Instrumentator().instrument(app).expose(app)

Это поднимает базовые метрики (число запросов, длительность по перцентилям, коды ответов) и отдаёт их на /metrics. Свои бизнес-метрики (число созданных заказов, размер очереди) добавляют через prometheus-client:

from prometheus_client import Counter

orders_created = Counter("orders_created_total", "Количество созданных заказов")

async def create_order(...):
    ...
    orders_created.inc()

Health-checks: сигналы для Kubernetes

Kubernetes нужно знать две разные вещи о сервисе, и путать их опасно:

  • liveness — жив ли процесс вообще. Если провален — Kubernetes перезапускает контейнер. Должен быть максимально простым и не обращаться к внешним системам.
  • readiness — готов ли процесс принимать трафик. Если провален — Kubernetes перестаёт слать запросы на этот экземпляр, но не перезапускает его. Здесь уместно проверить базу данных.
@router.get("/health/live")
async def live():
    return {"status": "ok"}


@router.get("/health/ready")
async def ready(session: SessionDep):
    await session.execute(text("SELECT 1"))
    return {"status": "ready"}

Типичная ошибка — сделать liveness зависимым от базы данных. Если база недоступна, liveness начнёт падать, Kubernetes будет перезапускать контейнер — а перезапуск не починит чужую базу. Правило простое: liveness — только про сам процесс, readiness — про готовность к работе.

Коротко

  • Логи — структурные JSON-события с полями, не print(). Ключевое поле — request_id, чтобы сшивать все строки одного запроса.
  • Трейсинг через OpenTelemetry: FastAPIInstrumentor.instrument_app(app) добавляет spans на каждый запрос. Трейс показывает, где теряется время внутри запроса.
  • Метрики через Prometheus: prometheus-fastapi-instrumentator поднимает /metrics с базовыми показателями; бизнес-метрики — через prometheus-client.
  • liveness — простая проверка, что процесс жив; не зависит от базы. readiness — проверяет готовность к трафику, включая зависимости.
  • Три инструмента отвечают на разные вопросы: метрики — «что в целом», трейсы — «что с этим запросом», логи — «что произошло в этот момент».

Что почитать дальше

  • Middleware и обработка ошибок — где проставлять request_id и как перехватывать исключения.
  • Тестирование FastAPI — как покрывать эндпоинты тестами, включая health-checks.