Опирается на правила: R-OBS-MTR-1R-OBS-MTR-7 и R-OBS-MTR-X1R-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 для resourcesprometheus-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_bytesRSS — утилизация памяти
process_virtual_memory_bytesVSZ
process_cpu_seconds_totalCPU 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 valueR-OBS-MTR-X1бизнес-категории: channel, payment_method
labels={"app": "foo"} вместо serviceR-OBS-MTR-X2Info("service").info({"service": ...})
Метрики без make_asgi_app() или экспортируемого registryR-OBS-MTR-X3явный REGISTRY или отдельный CollectorRegistry
/metrics публично через основной IngressR-OBS-MTR-X4отдельный порт + NetworkPolicy
label(user_id=str(...)) напрямую на CounterR-OBS-MTR-X1tracing / structured logs для per-entity наблюдаемости
paymentTime без единицыR-OBS-MTR-6payment_processing_seconds
orderCreatedCount (camelCase)R-OBS-MTR-6order_created_total
Counter без суффикса _totalR-OBS-MTR-6order_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.