Опирается на правила:
R-OBS-MTR-1…R-OBS-MTR-7иR-OBS-MTR-X1…R-OBS-MTR-X4из Observability — раздел 2. Metrics.
Важно знать
- prometheus-client — основная библиотека;
prometheus-fastapi-instrumentatorдобавляет RED-инструментацию одной строкой.- Стандартные labels
service/env/versionвыставляются один раз черезREGISTRY.set_target_info(...)или константными labels на метриках — не дублировать на каждом объекте.- RED для HTTP (rate/errors/duration) собирает
Instrumentatorавтоматически; подключать доapp.include_router.- USE для resources —
prometheus-clientэкспортирует process/GC метрики (process_resident_memory_bytes,python_gc_objects_collected_total) без настройки.- Бизнес-метрики через
Counter,Histogram,Gaugeизprometheus_client; именаsnake_caseс единицей.- Низкая cardinality в labels:
payment_method(CARD/SBP/CRYPTO) допустимо;user_id/order_id→ взрыв time series → OOM./metricsне публично — только internal scraper; в FastAPI — sub-app на отдельном порту либо сетевая политика.
Метрики — количественная картина сервиса под нагрузкой. Python-биндинг UCP опирается на prometheus-client как непосредственный инструментарий; HTTP-слой покрывает prometheus-fastapi-instrumentator. Три метода — RED, USE и бизнес-метрики — дают полную картину: что происходит, насколько загружены ресурсы, что важно для бизнеса.
Подключение
R-OBS-MTR-1: зависимости:
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,
should_respect_env_var=False,
excluded_handlers=["/health/live", "/health/ready", "/metrics"],
).instrument(app).expose(app, endpoint="/metrics", include_in_schema=False)
После старта GET /metrics отдаёт текстовый Prometheus-формат. Prometheus scraper тянет каждые 15 секунд.
should_group_status_codes=True группирует 200/201/204 в класс 2xx — это снижает cardinality метрики http_requests_total{status_code=...} и соответствует R-OBS-MTR-7.
Стандартные labels
R-OBS-MTR-2: три label на каждой метрике — выставляются через target_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"),
})
Info экспортирует gauge service_info{service="order-service",env="prod",version="1.4.2"} 1 — стандартный способ доставить identity-labels в Prometheus без дублирования на каждой метрике. Grafana подтягивает через * on (job) group_left(service, env, version).
Альтернатива — константные labels на каждом Counter/Histogram, но это дублирование и нарушение R-OBS-MTR-X2.
RED для HTTP — автоматически
R-OBS-MTR-3: Instrumentator генерирует:
http_requests_total{handler, method, status_code}
http_request_duration_seconds_bucket{handler, method, le}
http_request_size_bytes_bucket{handler, method, le}
PromQL-запросы для dashboard:
# 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]))
)
Для настройки bucket-ов под SLO (например, 100ms/500ms/1s):
from prometheus_fastapi_instrumentator.metrics import default
from prometheus_client import REGISTRY
# переопределить дефолтные buckets до Instrumentator.instrument()
# через кастомный instrumentation-callback:
from prometheus_fastapi_instrumentator import Instrumentator, metrics
Instrumentator().instrument(app, metric_namespace="", metric_subsystem="").add(
metrics.latency(buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0])
)
USE для resources — автоматически
R-OBS-MTR-4: prometheus-client по умолчанию экспортирует process- и GC-метрики Python:
| Метрика | Что показывает |
|---|---|
process_resident_memory_bytes | RSS — утилизация памяти |
process_virtual_memory_bytes | VSZ |
process_cpu_seconds_total | CPU time (rate = утилизация) |
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 (см. раздел ниже).
Бизнес-метрики
R-OBS-MTR-5: бизнес-события измеряются через объекты Counter, Histogram, Gauge.
Класс-фасад (модуль 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",
)
Применение в use-case handler:
from app.metrics.order_metrics import (
order_created_total,
order_amount_rubles,
payment_processing_seconds,
)
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 — из них PromQL считает quantile и mean без дополнительного кода.
Пример замера времени через context-manager Histogram.time():
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)
status_class — три значения (success/client_error/server_error) вместо HTTP-кода — пример низкой cardinality по R-OBS-MTR-7.
Пример 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 секунд или из middleware — не из handler'а (иначе метрика обновляется только при трафике).
Имена метрик — snake_case с единицей
R-OBS-MTR-6: соглашение Prometheus:
order_created_total # Counter — суффикс _total
payment_processing_seconds # Histogram длительности — _seconds
order_amount_rubles # Histogram суммы — _rubles
cart_active_total # Gauge — текущее количество
orderCreatedCount # camelCase — нарушение
paymentTime # без единицы — нарушение
order_amount # без единицы — нарушение
Counter по Prometheus-конвенции заканчивается на _total; Histogram несёт единицу измерения (_seconds, _bytes, _rubles). Это позволяет Prometheus автоматически формировать _bucket, _sum, _count с корректными суффиксами.
Низкая cardinality в labels
R-OBS-MTR-7: label value — это категория, не уникальный идентификатор.
# ХОРОШО — 3-10 значений на label
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)
# ПЛОХО — миллион time series, OOM Prometheus
order_created_total.labels(
user_id=str(command.user_id), # миллионы уникальных значений
order_id=str(order.id), # миллионы уникальных значений
).inc()
Prometheus хранит отдельный time series для каждой уникальной комбинации label values. Миллион user_id × несколько метрик = миллионы time series → OOM scraper-а и Prometheus TSDB.
Для per-request наблюдаемости на уровне отдельных сущностей — distributed tracing (Tempo/Jaeger). См. Tracing.
Отдельный порт для /metrics
R-OBS-CFG-1, R-OBS-MTR-X4: в FastAPI /metrics изолируется через отдельное ASGI-приложение:
import uvicorn
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 — R-OBS-MTR-X4.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
user_id / order_id / request_id как label value | R-OBS-MTR-X1 | бизнес-категории: channel, payment_method |
labels={"app": "foo"} вместо service | R-OBS-MTR-X2 | Info("service").info({"service": ...}) |
Метрики без make_asgi_app() или экспортируемого registry | R-OBS-MTR-X3 | явный REGISTRY или отдельный CollectorRegistry |
/metrics публично через основной Ingress | R-OBS-MTR-X4 | отдельный порт + NetworkPolicy |
label(user_id=str(...)) напрямую на Counter | R-OBS-MTR-X1 | tracing / structured logs для per-entity наблюдаемости |
paymentTime без единицы | R-OBS-MTR-6 | payment_processing_seconds |
orderCreatedCount (camelCase) | R-OBS-MTR-6 | order_created_total |
Counter без суффикса _total | R-OBS-MTR-6 | order_created_total |
Куда дальше
- Конфигурация — management-порт, профили,
/metricsизоляция. - Context propagation — contextvars, request_id в middleware, очистка.
- Health checks — liveness vs readiness, кастомные проверки.
- Logging — structlog JSON, bound contextvars, нет
print. - SLO и алерты — multi-window burn rate, error budget, runbook.
- Tracing — OTel автоинструментация, manual span через context-manager, high-cardinality observability.