Когда сервис в продакшне ведёт себя странно — тормозит, отдаёт ошибки, падает — первый вопрос: «что происходит?» Если ответить нечем, остаётся угадывать. Наблюдаемость — это три инструмента (логи, трейсы, метрики) плюс 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.