Когда сервис работает под нагрузкой, вопросы возникают конкретные: сколько запросов в секунду? сколько из них падает? сколько памяти занимает процесс? Логи отвечают на «что случилось», а метрики — на «как сейчас». Без метрик на эти вопросы приходится угадывать.
В Python экосистеме для этого используют prometheus-client и Prometheus — систему сбора и хранения временных рядов. FastAPI подключается одной строкой через prometheus-fastapi-instrumentator.
Что такое Prometheus и как он работает
Prometheus не ждёт, пока ему пришлют данные — он сам приходит и забирает. Каждые 15 секунд scraper делает GET /metrics к вашему сервису и получает текстовый список текущих значений. Это называется pull-модель.
Ваш сервис хранит метрики в памяти и отдаёт их по запросу. Библиотека prometheus-client берёт на себя и хранение, и форматирование ответа.
Подключение
Установите зависимости:
prometheus-client>=0.20
prometheus-fastapi-instrumentator>=7.0
Инициализация в main.py:
from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator
app = FastAPI()
Instrumentator(
should_group_status_codes=True,
should_ignore_untemplated=True,
excluded_handlers=["/health/live", "/health/ready", "/metrics"],
).instrument(app).expose(app, endpoint="/metrics", include_in_schema=False)
После старта GET /metrics отдаёт текстовый список метрик. Prometheus scraper забирает его каждые 15 секунд.
should_group_status_codes=True группирует 200/201/204 в класс 2xx — это снижает количество уникальных комбинаций меток и уменьшает нагрузку на Prometheus.
Стандартные метки сервиса
Prometheus по-умолчанию знает только адрес, с которого взял данные. Чтобы различать сервисы, среды (prod/staging) и версии, добавляют identity-метки через Info:
import os
from prometheus_client import Info
SERVICE_INFO = Info("service", "Service identity labels")
SERVICE_INFO.info({
"service": os.getenv("SERVICE_NAME", "order-service"),
"env": os.getenv("APP_ENV", "dev"),
"version": os.getenv("BUILD_VERSION", "unknown"),
})
Это экспортирует одну метрику service_info{service="order-service",env="prod",version="1.4.2"} 1. Grafana подтягивает эти значения через group_left и добавляет к любому графику. Так не нужно дублировать эти метки на каждом счётчике.
RED для HTTP — автоматически
RED — три вопроса о состоянии HTTP-сервиса:
- Rate — сколько запросов в секунду
- Errors — какая доля заканчивается ошибкой
- Duration — как долго обрабатывается запрос
Instrumentator собирает всё это без дополнительного кода и экспортирует:
http_requests_total{handler, method, status_code}
http_request_duration_seconds_bucket{handler, method, le}
http_request_size_bytes_bucket{handler, method, le}
Запросы в Prometheus для дашборда:
# Rate — запросы в секунду по эндпоинту
sum(rate(http_requests_total[5m])) by (handler, method)
# Errors — доля 5xx
sum(rate(http_requests_total{status_code="5xx"}[5m])) by (handler)
/
sum(rate(http_requests_total[5m])) by (handler)
# Duration p95
histogram_quantile(
0.95,
sum by (le, handler) (rate(http_request_duration_seconds_bucket[5m]))
)
Если нужны конкретные границы для SLO (например, 100ms/500ms/1s):
from prometheus_fastapi_instrumentator import Instrumentator, metrics
Instrumentator().instrument(app).add(
metrics.latency(buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0])
)
USE для ресурсов — тоже автоматически
USE — три вопроса о ресурсах:
- Utilization — насколько загружен ресурс
- Saturation — есть ли очередь
- Errors — есть ли ошибки на уровне ресурса
prometheus-client экспортирует метрики процесса и GC без настройки:
| Метрика | Что показывает |
|---|---|
process_resident_memory_bytes | Сколько памяти занимает процесс (RSS) |
process_virtual_memory_bytes | Виртуальная память |
process_cpu_seconds_total | Потраченное процессорное время |
python_gc_objects_collected_total{generation} | Сборка мусора по поколениям |
python_gc_collections_total{generation} | Количество GC-циклов |
Пример алертов на насыщение ресурсов:
groups:
- name: order-service
rules:
- alert: MemoryHigh
expr: >
process_resident_memory_bytes{service="order-service"}
> 400 * 1024 * 1024
for: 10m
labels:
severity: warning
- alert: CPUHigh
expr: >
rate(process_cpu_seconds_total{service="order-service"}[5m]) > 0.8
for: 5m
labels:
severity: warning
Пулы соединений (asyncpg, Redis) метрики не экспортируют сами — их нужно инструментировать вручную через Gauge (об этом ниже).
Бизнес-метрики
HTTP и ресурсы — это инфраструктура. Бизнес-метрики отвечают на другой вопрос: что происходит в домене? Сколько заказов создано? Какое распределение сумм? Сколько корзин активно прямо сейчас?
Для этого в prometheus-client есть три типа объектов:
- Counter — только растёт (количество событий, ошибок)
- Histogram — распределение значений (суммы, длительности)
- Gauge — текущее значение, может расти и падать (активные корзины, размер пула)
Выносите метрики в отдельный модуль:
# app/metrics/order_metrics.py
from prometheus_client import Counter, Histogram, Gauge
order_created_total = Counter(
"order_created_total",
"Total orders created",
["channel"],
)
order_amount_rubles = Histogram(
"order_amount_rubles",
"Order amount distribution",
["channel"],
buckets=[100, 500, 1000, 5000, 10000, 50000],
)
payment_processing_seconds = Histogram(
"payment_processing_seconds",
"Payment processing duration",
["payment_method", "status_class"],
buckets=[0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
)
cart_active_total = Gauge(
"cart_active_total",
"Currently active shopping carts",
)
Применение в обработчике:
from app.metrics.order_metrics import order_created_total, order_amount_rubles
class CreateOrderHandler:
def __init__(self, order_repo: OrderRepository) -> None:
self._order_repo = order_repo
async def handle(self, command: CreateOrderCommand) -> Order:
order = await self._order_repo.save(Order.create(command))
order_created_total.labels(channel=command.channel).inc()
order_amount_rubles.labels(channel=command.channel).observe(
float(order.amount)
)
return order
Histogram.observe() автоматически раскладывает значение по bucket-ам и накапливает _sum/_count — из них Prometheus считает квантили без дополнительного кода.
Для замера времени удобен context manager Histogram.time() — или perf_counter вручную, если нужна метка по результату:
import time
from app.metrics.order_metrics import payment_processing_seconds
class ProcessPaymentHandler:
async def handle(self, command: ProcessPaymentCommand) -> PaymentResult:
start = time.perf_counter()
try:
result = await self._payment_client.charge(command)
status = "success"
return result
except PaymentClientError:
status = "client_error"
raise
except Exception:
status = "server_error"
raise
finally:
duration = time.perf_counter() - start
payment_processing_seconds.labels(
payment_method=command.payment_method,
status_class=status,
).observe(duration)
Пример Gauge для пула asyncpg:
from prometheus_client import Gauge
db_pool_size = Gauge("db_pool_size", "asyncpg pool total connections")
db_pool_active = Gauge("db_pool_active", "asyncpg pool acquired connections")
async def update_pool_metrics(pool: asyncpg.Pool) -> None:
db_pool_size.set(pool.get_size())
db_pool_active.set(pool.get_size() - pool.get_idle_size())
Вызывать из фоновой задачи каждые N секунд — не из обработчика, иначе метрика обновляется только при трафике.
Как правильно называть метрики
Prometheus-конвенция: snake_case, с единицей измерения в имени.
order_created_total # Counter — суффикс _total
payment_processing_seconds # Histogram длительности — _seconds
order_amount_rubles # Histogram суммы — единица в имени
cart_active_total # Gauge — текущее количество
Частые ошибки:
orderCreatedCount # camelCase — не принято
paymentTime # без единицы — непонятно, секунды? миллисекунды?
order_amount # нет единицы
Counter всегда заканчивается на _total. Histogram несёт единицу (_seconds, _bytes, _rubles) — Prometheus автоматически добавит _bucket, _sum, _count с правильными суффиксами.
Низкая cardinality меток — важно
Метки в Prometheus — это не просто текст. Каждая уникальная комбинация меток создаёт отдельный временной ряд в базе данных. Миллион уникальных значений = миллион рядов = Prometheus падает из-за нехватки памяти.
Правило: метка — это категория с небольшим числом значений, а не уникальный идентификатор.
# Хорошо — 3-10 значений на метку
order_created_total.labels(
channel="web", # web / mobile / api
).inc()
payment_processing_seconds.labels(
payment_method="SBP", # CARD / SBP / CRYPTO
status_class="success", # success / client_error / server_error
).observe(duration)
# Плохо — миллион временных рядов, OOM Prometheus
order_created_total.labels(
user_id=str(command.user_id), # миллионы значений
order_id=str(order.id), # миллионы значений
).inc()
Если нужно наблюдать за конкретными сущностями — это задача для трассировки, не для метрик. Метрики дают агрегированную картину, трейсы — детали конкретного запроса.
/metrics на отдельном порту
Эндпоинт /metrics не должен быть виден извне. В FastAPI его изолируют через отдельное ASGI-приложение на другом порту:
import uvicorn
import os
from prometheus_client import make_asgi_app
metrics_app = make_asgi_app()
async def run_metrics_server() -> None:
config = uvicorn.Config(
metrics_app,
host="0.0.0.0",
port=int(os.getenv("METRICS_PORT", "9090")),
)
server = uvicorn.Server(config)
await server.serve()
Запуск параллельно с основным приложением:
import asyncio
async def main() -> None:
await asyncio.gather(
run_app(),
run_metrics_server(),
)
Порт 9090 закрывают на уровне Ingress-правил и открывают только для namespace мониторинга через NetworkPolicy.
Коротко
prometheus-clientхранит метрики в памяти, Prometheus забирает их по pull-модели каждые 15 секунд.prometheus-fastapi-instrumentatorавтоматически собирает RED-метрики (rate/errors/duration) по HTTP-эндпоинтам.- USE-метрики процесса (память, CPU, GC) экспортируются без настройки —
process_*иpython_gc_*. - Бизнес-события — Counter (растёт), Histogram (распределение), Gauge (текущее значение).
- Identity-метки (
service,env,version) выставляются один раз черезInfo, не дублируются на каждой метрике. - Имена метрик:
snake_case+ единица измерения (_seconds,_bytes,_total). - Метки — категории с малым числом значений.
user_idкак метка = взрыв временных рядов и падение Prometheus. /metrics— на отдельном порту черезmake_asgi_app(), закрыт от внешнего трафика.
Что почитать дальше
- Tracing в Python — OTel, ручные span'ы, per-entity наблюдаемость без взрыва cardinality.
- Logging в Python — structlog, JSON в проде, контекстные поля.
- SLO и алерты в Python — error budget, multi-window burn rate, runbook.
- Health checks в Python — liveness vs readiness, кастомные проверки.