Введение

После того как ваше веб-приложение попадает в продакшн, самый важный вопрос — а как оно работает прямо сейчас? Логи дают ответ постфактум, но хочется видеть проблемы до того, как пользователи начнут жаловаться.

В этой статье я расскажу, как построил полноценную систему мониторинга для Peakline — FastAPI приложения для анализа Strava данных, обрабатывающего тысячи запросов в день от спортсменов по всему миру.

Главный дашборд Grafana с метриками HTTP и производительности
Главный дашборд Grafana с метриками HTTP и производительности

Что внутри:

  • Архитектура метрик (HTTP, API, бизнес-метрики)

  • Настройка Prometheus + Grafana с нуля

  • 50+ production-ready метрик

  • Продвинутые PromQL запросы

  • Реактивные дашборды

  • Best practices и подводные камни

Архитектура: три уровня мониторинга

Современная система мониторинга — это не просто "поставил Grafana и смотрю графики". Это продуманная архитектура из нескольких слоев:

┌─────────────────────────────────────────────────┐
│   FastAPI Application                           │
│   ├── HTTP Middleware (автосбор метрик)         │
│   ├── Business Logic (бизнес-метрики)           │
│   └── /metrics endpoint (Prometheus format)     │
└──────────────────┬──────────────────────────────┘
                   │ scrape every 5s
┌──────────────────▼──────────────────────────────┐
│   Prometheus                                    │
│   ├── Time Series Database (TSDB)               │
│   ├── Storage retention: 200h                   │
│   └── PromQL Engine                             │
└──────────────────┬──────────────────────────────┘
                   │ query data
┌──────────────────▼──────────────────────────────┐
│   Grafana                                       │
│   ├── Dashboards                                │
│   ├── Alerting                                  │
│   └── Visualization                             │
└─────────────────────────────────────────────────┘

Почему именно эта связка?

Prometheus — de-facto стандарт в мире метрик. Pull-модель, мощный язык запросов PromQL, отличная интеграция с Kubernetes.

Grafana — лучший инструмент визуализации. Красивые дашборды, алерты, templating, rich UI.

FastAPI — асинхронный Python-фреймворк с нативной поддержкой метрик через prometheus_client.

Настройка базовой инфраструктуры

Docker Compose: быстрый старт за 5 минут

Первым делом поднимаем Prometheus и Grafana в Docker:

# docker-compose.yml
version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=200h'  # 8+ дней истории
      - '--web.enable-lifecycle'
    networks:
      - monitoring
    extra_hosts:
      - "host.docker.internal:host-gateway"  # Для доступа к хосту

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}  # Используйте .env!
      - GF_SERVER_ROOT_URL=/grafana  # Для nginx reverse proxy
    volumes:
      - grafana_data:/var/lib/grafana
      - ./monitoring/grafana/provisioning:/etc/grafana/provisioning
    depends_on:
      - prometheus
    networks:
      - monitoring

volumes:
  prometheus_data:
  grafana_data:

networks:
  monitoring:
    driver: bridge

Ключевые моменты:

  • storage.tsdb.retention.time=200h — храним метрики 8+ дней (для недельного анализа)

  • extra_hosts: host.docker.internal — позволяет Prometheus достучаться до приложения на хосте

  • Volumes для персистентности данных

Конфигурация Prometheus

# monitoring/prometheus.yml
global:
  scrape_interval: 15s      # Как часто собирать метрики
  evaluation_interval: 15s  # Как часто проверять алерты

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'webapp'
    static_configs:
      - targets: ['host.docker.internal:8000']  # Порт вашего приложения
    scrape_interval: 5s      # Более частый сбор для веб-приложения
    metrics_path: /metrics

Важно: scrape_interval: 5s для веб-приложения — это баланс между актуальностью данных и нагрузкой на систему. В production обычно 15-30s.

Провиженинг Grafana datasource

Чтобы не настраивать Prometheus в Grafana руками, используем provisioning:

# monitoring/grafana/provisioning/datasources/prometheus.yml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: true

Теперь при запуске Grafana автоматически подключится к Prometheus.

docker-compose up -d

Уровень 1: HTTP метрики

Самый базовый, но критически важный слой — мониторинг HTTP запросов. Middleware автоматически собирает метрики всех HTTP запросов.

Инициализация метрик

# webapp/main.py
from prometheus_client import Counter, Histogram, CollectorRegistry, generate_latest, CONTENT_TYPE_LATEST
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
import time

app = FastAPI(title="Peakline", version="2.0.0")

# Создаем отдельный registry для изоляции метрик
registry = CollectorRegistry()

# Counter: монотонно растущее значение (кол-во запросов)
http_requests_total = Counter(
    'http_requests_total',
    'Total number of HTTP requests',
    ['method', 'endpoint', 'status_code'],  # Labels для группировки
    registry=registry
)

# Histogram: распределение значений (время выполнения)
http_request_duration_seconds = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration in seconds',
    ['method', 'endpoint'],
    registry=registry
)

# Счетчики API вызовов
api_calls_total = Counter(
    'api_calls_total',
    'Total number of API calls by type',
    ['api_type'],
    registry=registry
)

# Отдельные счетчики для ошибок
http_errors_4xx_total = Counter(
    'http_errors_4xx_total',
    'Total number of 4xx HTTP errors',
    ['endpoint', 'status_code'],
    registry=registry
)

http_errors_5xx_total = Counter(
    'http_errors_5xx_total',
    'Total number of 5xx HTTP errors',
    ['endpoint', 'status_code'],
    registry=registry
)

Middleware для автоматического сбора

Магия происходит в middleware — он оборачивает каждый запрос:

@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
    start_time = time.time()
    
    # Выполняем запрос
    response = await call_next(request)
    
    duration = time.time() - start_time
    
    # Нормализация пути: /api/activities/12345 → /api/activities/{id}
    path = request.url.path
    if path.startswith('/api/'):
        parts = path.split('/')
        if len(parts) > 3 and parts[3].isdigit():
            parts[3] = '{id}'
            path = '/'.join(parts)
    
    # Записываем метрики
    http_requests_total.labels(
        method=request.method,
        endpoint=path,
        status_code=str(response.status_code)
    ).inc()
    
    http_request_duration_seconds.labels(
        method=request.method,
        endpoint=path
    ).observe(duration)
    
    # Трекинг API вызовов
    if path.startswith('/api/'):
        api_type = path.split('/')[2] if len(path.split('/')) > 2 else 'unknown'
        api_calls_total.labels(api_type=api_type).inc()
    
    # Отдельный подсчет ошибок
    status_code = response.status_code
    if 400 <= status_code < 500:
        http_errors_4xx_total.labels(endpoint=path, status_code=str(status_code)).inc()
    elif status_code >= 500:
        http_errors_5xx_total.labels(endpoint=path, status_code=str(status_code)).inc()
    
    return response

Ключевые техники:

  1. Нормализация путей — критически важно! Без этого получите тысячи уникальных метрик для /api/activities/1, /api/activities/2, etc.

  2. Labels — позволяют фильтровать и группировать метрики в PromQL

  3. Отдельные счетчики для ошибок — упрощают написание алертов

Endpoint для метрик

@app.get("/metrics")
async def metrics():
    """Prometheus metrics endpoint"""
    return PlainTextResponse(
        generate_latest(registry),
        media_type=CONTENT_TYPE_LATEST
    )

Теперь Prometheus может собирать метрики с http://localhost:2121/metrics.

Что мы получаем в Prometheus?

# Формат метрик в /metrics endpoint:
http_requests_total{method="GET",endpoint="/api/activities",status_code="200"} 1543
http_requests_total{method="POST",endpoint="/api/activities",status_code="201"} 89
http_request_duration_seconds_bucket{method="GET",endpoint="/api/activities",le="0.1"} 1234

Уровень 2: Внешние API метрики

Веб-приложение часто интегрируется с внешними API (Strava, Stripe, AWS, etc.). Важно отслеживать не только свои запросы, но и зависимости.

Метрики для внешних API

# Strava API metrics
strava_api_calls_total = Counter(
    'strava_api_calls_total',
    'Total number of Strava API calls by endpoint type',
    ['endpoint_type'],
    registry=registry
)

strava_api_errors_total = Counter(
    'strava_api_errors_total',
    'Total number of Strava API errors by endpoint type',
    ['endpoint_type'],
    registry=registry
)

strava_api_latency_seconds = Histogram(
    'strava_api_latency_seconds',
    'Strava API call latency in seconds',
    ['endpoint_type'],
    registry=registry
)

Helper для трекинга API вызовов

Вместо дублирования кода в каждом месте вызова API, создаем универсальную обертку:

async def track_strava_api_call(endpoint_type: str, api_call_func, *args, **kwargs):
    """
    Универсальная обертка для трекинга API вызовов
    
    Usage:
        result = await track_strava_api_call(
            'athlete_activities',
            client.get_athlete_activities,
            athlete_id=123
        )
    """
    start_time = time.time()
    
    try:
        # Инкрементируем счетчик вызовов
        strava_api_calls_total.labels(endpoint_type=endpoint_type).inc()
        
        # Выполняем API вызов
        result = await api_call_func(*args, **kwargs)
        
        # Записываем latency
        duration = time.time() - start_time
        strava_api_latency_seconds.labels(endpoint_type=endpoint_type).observe(duration)
        
        # Проверяем на ошибки API (статус >= 400)
        if isinstance(result, Exception) or (hasattr(result, 'status') and result.status >= 400):
            strava_api_errors_total.labels(endpoint_type=endpoint_type).inc()
        
        return result
        
    except Exception as e:
        # Записываем latency и ошибку
        duration = time.time() - start_time
        strava_api_latency_seconds.labels(endpoint_type=endpoint_type).observe(duration)
        strava_api_errors_total.labels(endpoint_type=endpoint_type).inc()
        raise e

Использование в коде

@app.get("/api/activities")
async def get_activities(athlete_id: int):
    # Вместо прямого вызова API:
    # activities = await strava_client.get_athlete_activities(athlete_id)
    
    # Используем обертку с трекингом:
    activities = await track_strava_api_call(
        'athlete_activities',
        strava_client.get_athlete_activities,
        athlete_id=athlete_id
    )
    
    return activities

Теперь мы видим:

  • Сколько вызовов к каждому endpoint Strava API

  • Сколько из них вернули ошибки

  • Какая latency у каждого типа вызовов

Уровень 3: Бизнес-метрики

Это самая ценная часть мониторинга — метрики, которые отражают реальное использование приложения.

Виды бизнес-метрик

# === Аутентификация ===
user_logins_total = Counter(
    'user_logins_total',
    'Total number of user logins',
    registry=registry
)

user_registrations_total = Counter(
    'user_registrations_total',
    'Total number of new user registrations',
    registry=registry
)

user_deletions_total = Counter(
    'user_deletions_total',
    'Total number of user deletions',
    registry=registry
)

# === Файловые операции ===
fit_downloads_total = Counter(
    'fit_downloads_total',
    'Total number of FIT file downloads',
    registry=registry
)

gpx_downloads_total = Counter(
    'gpx_downloads_total',
    'Total number of GPX file downloads',
    registry=registry
)

gpx_uploads_total = Counter(
    'gpx_uploads_total',
    'Total number of GPX file uploads',
    registry=registry
)

# === Пользовательские действия ===
settings_updates_total = Counter(
    'settings_updates_total',
    'Total number of user settings updates',
    registry=registry
)

idea_creations_total = Counter(
    'idea_creations_total',
    'Total number of feature requests',
    registry=registry
)

idea_votes_total = Counter(
    'idea_votes_total',
    'Total number of votes for ideas',
    registry=registry
)

# === Отчеты ===
manual_reports_total = Counter(
    'manual_reports_total',
    'Total number of manually created reports',
    registry=registry
)

auto_reports_total = Counter(
    'auto_reports_total',
    'Total number of automatically created reports',
    registry=registry
)

failed_reports_total = Counter(
    'failed_reports_total',
    'Total number of failed report creation attempts',
    registry=registry
)

Инкрементирование в коде

@app.post("/api/auth/login")
async def login(credentials: LoginCredentials):
    user = await authenticate_user(credentials)
    
    if user:
        # Инкрементируем счетчик успешных логинов
        user_logins_total.inc()
        return {"token": generate_token(user)}
    
    return {"error": "Invalid credentials"}

@app.post("/api/activities/report")
async def create_report(activity_id: int, is_auto: bool = False):
    try:
        report = await generate_activity_report(activity_id)
        
        # Разные счетчики для ручных и автоматических отчетов
        if is_auto:
            auto_reports_total.inc()
        else:
            manual_reports_total.inc()
        
        return report
    except Exception as e:
        failed_reports_total.inc()
        raise e

Уровень 4: Производительность и кэширование

Метрики кэша

Кэш — критически важная часть производительности. Нужно отслеживать hit rate:

cache_hits_total = Counter(
    'cache_hits_total',
    'Total number of cache hits',
    ['cache_type'],
    registry=registry
)

cache_misses_total = Counter(
    'cache_misses_total',
    'Total number of cache misses',
    ['cache_type'],
    registry=registry
)

# В коде кэширования:
async def get_from_cache(key: str, cache_type: str = 'generic'):
    value = await cache.get(key)
    
    if value is not None:
        cache_hits_total.labels(cache_type=cache_type).inc()
        return value
    else:
        cache_misses_total.labels(cache_type=cache_type).inc()
        return None

Метрики фоновых задач

Если у вас есть background tasks (Celery, APScheduler), отслеживайте их:

background_task_duration_seconds = Histogram(
    'background_task_duration_seconds',
    'Background task execution time',
    ['task_type'],
    registry=registry
)

async def run_background_task(task_type: str, task_func, *args, **kwargs):
    start_time = time.time()
    
    try:
        result = await task_func(*args, **kwargs)
        return result
    finally:
        duration = time.time() - start_time
        background_task_duration_seconds.labels(task_type=task_type).observe(duration)

PromQL: язык запросов метрик

Prometheus использует собственный язык запросов — PromQL. Это не SQL, но очень мощно.

Базовые запросы

# 1. Просто получить метрику (instant vector)
http_requests_total

# 2. Фильтрация по labels
http_requests_total{method="GET"}
http_requests_total{status_code="200"}
http_requests_total{method="GET", endpoint="/api/activities"}

# 3. Регулярные выражения в labels
http_requests_total{status_code=~"5.."}  # Все 5xx ошибки
http_requests_total{endpoint=~"/api/.*"}  # Все API эндпоинты

# 4. Временной интервал (range vector)
http_requests_total[5m]  # Данные за последние 5 минут

Rate и irate: скорость изменения

Counter постоянно растет, но нам нужна скорость изменения — RPS (requests per second):

# Rate - средняя скорость за интервал
rate(http_requests_total[5m])

# irate - мгновенная скорость (между последними двумя точками)
irate(http_requests_total[5m])

Когда что использовать:

  • rate() — для алертов и графиков тренда (сглаживает всплески)

  • irate() — для детального анализа (показывает пики)

Агрегация с sum, avg, max

# Общий RPS приложения
sum(rate(http_requests_total[5m]))

# RPS по методам
sum(rate(http_requests_total[5m])) by (method)

# RPS по endpoint'ам, отсортированный
sort_desc(sum(rate(http_requests_total[5m])) by (endpoint))

# Средняя latency
avg(rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m]))

Histogram и percentiles

Для Histogram метрик (latency, duration) используем histogram_quantile:

# P50 (медиана) latency
histogram_quantile(0.5, rate(http_request_duration_seconds_bucket[5m]))

# P95 latency
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# P99 latency (99% запросов быстрее этого)
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

# P95 по каждому endpoint'у
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) by (endpoint)

Сложные запросы

1. Success Rate (процент успешных запросов)

(
  sum(rate(http_requests_total{status_code=~"2.."}[5m]))
  /
  sum(rate(http_requests_total[5m]))
) * 100

2. Error Rate (процент ошибок)

(
  sum(rate(http_requests_total{status_code=~"4..|5.."}[5m]))
  /
  sum(rate(http_requests_total[5m]))
) * 100

3. Cache Hit Rate

(
  sum(rate(cache_hits_total[5m]))
  /
  (sum(rate(cache_hits_total[5m])) + sum(rate(cache_misses_total[5m])))
) * 100

4. Top-5 самых медленных endpoints

topk(5,
  histogram_quantile(0.95, 
    rate(http_request_duration_seconds_bucket[5m])
  ) by (endpoint)
)

5. API Health Score (0-100)

(
  (
    sum(rate(strava_api_calls_total[5m])) 
    - 
    sum(rate(strava_api_errors_total[5m]))
  ) 
  / 
  sum(rate(strava_api_calls_total[5m]))
) * 100

Grafana Dashboards: визуализация

Теперь самое интересное — превращаем сырые метрики в красивые и информативные дашборды.

Дашборд "HTTP и Производительность" с графиками запросов, latency и ошибок
Дашборд "HTTP и Производительность" с графиками запросов, latency и ошибок

Dashboard 1: HTTP & Performance

Панель 1: Request Rate

sum(rate(http_requests_total[5m]))
  • Тип: Time series

  • Цвет: Синий градиент

  • Unit: requests/sec

  • Легенда: Total RPS

Панель 2: Success Rate

(
  sum(rate(http_requests_total{status_code=~"2.."}[5m]))
  /
  sum(rate(http_requests_total[5m]))
) * 100
  • Тип: Stat

  • Цвет: Зеленый если > 95%, желтый если > 90%, красный если < 90%

  • Unit: percent (0-100)

  • Значение: Текущее (last)

Панель 3: Response Time (P50, P95, P99)

# P50
histogram_quantile(0.5, rate(http_request_duration_seconds_bucket[5m]))

# P95
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# P99
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
  • Тип: Time series

  • Unit: seconds (s)

  • Легенда: P50, P95, P99

Панель 4: Errors by Type

sum(rate(http_requests_total{status_code=~"4.."}[5m])) by (status_code)
sum(rate(http_requests_total{status_code=~"5.."}[5m])) by (status_code)
  • Тип: Bar chart

  • Colors: Желтый (4xx), Красный (5xx)

Панель 5: Request Rate by Endpoint

sort_desc(sum(rate(http_requests_total[5m])) by (endpoint))
  • Тип: Bar chart

  • Limit: Top 10

Dashboard 2: Business Metrics

Этот дашборд показывает реальное использование продукта — что пользователи делают и как часто.

Дашборд "Аутентификация и Пользователи" с метриками логинов, регистраций и активности
Дашборд "Аутентификация и Пользователи" с метриками логинов, регистраций и активности

Панель 1: User Activity (24h)

# Логины
increase(user_logins_total[24h])

# Регистрации
increase(user_registrations_total[24h])

# Удаления
increase(user_deletions_total[24h])
  • Тип: Stat

  • Layout: Horizontal

Панель 2: Downloads by Type

sum(rate({__name__=~".*_downloads_total"}[5m])) by (__name__)
  • Тип: Pie chart

  • Легенда справа

Панель 3: Feature Usage Timeline

rate(gpx_fixer_usage_total[5m])
rate(search_usage_total[5m])
rate(manual_reports_total[5m])
  • Тип: Time series

  • Stacking: Normal

Dashboard 3: External API

Критически важно мониторить зависимости от внешних сервисов — они могут стать узким местом.

Дашборд "API Strava" с детальной статистикой по эндпоинтам и latency
Дашборд "API Strava" с детальной статистикой по эндпоинтам и latency

Панель 1: API Health Score

(
  sum(rate(strava_api_calls_total[5m])) - sum(rate(strava_api_errors_total[5m]))
) / sum(rate(strava_api_calls_total[5m])) * 100
  • Тип: Gauge

  • Min: 0, Max: 100

  • Thresholds: 95 (зеленый), 90 (желтый), 0 (красный)

Панель 2: API Latency by Endpoint

histogram_quantile(0.95, rate(strava_api_latency_seconds_bucket[5m])) by (endpoint_type)
  • Тип: Bar chart

  • Sort: Descending

Панель 3: Error Rate by Endpoint

sum(rate(strava_api_errors_total[5m])) by (endpoint_type)
  • Тип: Bar chart

  • Color: Красный

Variables: динамические дашборды

Grafana поддерживает переменные для интерактивных дашбордов:

Создание переменной

  1. Dashboard Settings → Variables → Add variable

  2. Name: endpoint

  3. Type: Query

  4. Query:

label_values(http_requests_total, endpoint)

Использование в панелях

# Фильтр по выбранному endpoint
sum(rate(http_requests_total{endpoint="$endpoint"}[5m]))

# Multi-select
sum(rate(http_requests_total{endpoint=~"$endpoint"}[5m])) by (endpoint)

Полезные переменные

# Временной интервал
Variable: interval
Type: Interval
Values: 1m,5m,10m,30m,1h

# Метод HTTP
Variable: method
Query: label_values(http_requests_total, method)

# Статус код
Variable: status_code
Query: label_values(http_requests_total, status_code)

Alerting: реактивность системы

Мониторинг без алертов — как автомобиль без тормозов. Настраиваем умные алерты.

Grafana Alerting

Alert 1: High Error Rate

(
  sum(rate(http_requests_total{status_code=~"5.."}[5m]))
  /
  sum(rate(http_requests_total[5m]))
) * 100 &gt; 1
  • Condition: &gt; 1 (больше 1% ошибок)

  • For: 5m (в течение 5 минут)

  • Severity: Critical

  • Notification: Slack, Email, Telegram

Alert 2: High Latency

histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) &gt; 2
  • Condition: P95 > 2 секунд

  • For: 10m

  • Severity: Warning

Alert 3: External API Down

sum(rate(strava_api_errors_total[5m])) / sum(rate(strava_api_calls_total[5m])) &gt; 0.5
  • Condition: Больше 50% ошибок API

  • For: 2m

  • Severity: Critical

Alert 4: No Data

absent_over_time(http_requests_total[10m])
  • Condition: Нет метрик 10 минут

  • Severity: Critical

  • Означает: приложение упало или Prometheus не может собрать метрики

Notification Channels

# grafana/provisioning/notifiers/slack.yml
notifiers:
  - name: Slack
    type: slack
    uid: slack-notifications
    settings:
      url: https://hooks.slack.com/services/YOUR/WEBHOOK/URL
      recipient: '#monitoring'
      mentionChannel: 'here'

Best Practices: боевой опыт

1. Labels: не переборщите

Плохо:

# Слишком детализированные labels = cardinality explosion
http_requests_total.labels(
    method=request.method,
    endpoint=request.url.path,  # Каждый уникальный URL!
    user_id=str(user.id),       # Тысячи пользователей!
    timestamp=str(time.time())  # Бесконечные значения!
).inc()

Хорошо:

# Нормализованные endpoint'ы + ограниченный набор labels
http_requests_total.labels(
    method=request.method,
    endpoint=normalize_path(request.url.path),  # /api/users/{id}
    status_code=str(response.status_code)
).inc()

Правило: High-cardinality данные (user_id, timestamps, unique IDs) НЕ должны быть labels.

2. Naming Convention

Следуйте Prometheus naming conventions:

# Хорошие имена:
http_requests_total          # __
strava_api_latency_seconds   # Единица измерения в имени
cache_hits_total             # Понятно, что это Counter

# Плохие имена:
RequestCount                 # Не CamelCase
api-latency                  # Не используйте дефисы
request_time                 # Не указана единица измерения

3. Rate() интервал

Интервал rate() должен быть минимум в 4 раза больше scrape_interval:

# Если scrape_interval = 15s
rate(http_requests_total[1m])   # 4x = 60s ✅
rate(http_requests_total[30s])  # 2x = плохая точность ❌

4. Histogram buckets

Правильные buckets критичны для точных percentiles:

# По умолчанию (плохо для latency):
Histogram('latency_seconds', 'Latency')  # [.005, .01, .025, .05, .1, ...]

# Кастомные buckets для web latency:
Histogram(
    'http_request_duration_seconds',
    'Request latency',
    buckets=[.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10]
)

Принцип: Buckets должны покрывать типичный диапазон значений.

5. Стоимость метрик

Каждая метрика стоит памяти. Считаем:

Memory = Series count × (~3KB per series)

Series = Metric × Label combinations

Пример:

# 1 метрика × 5 methods × 20 endpoints × 15 status codes = 1,500 series
http_requests_total{method, endpoint, status_code}

# 1,500 × 3KB = ~4.5MB для одной метрики!

Совет: Регулярно проверяйте cardinality:

# Топ метрик по cardinality
topk(10, count by (__name__)({__name__=~".+"}))

6. Тестирование в dev

Не запускайте метрики только в production:

# .env
PROMETHEUS_ENABLED=true  # В dev тоже включаем

# В коде
if os.getenv('PROMETHEUS_ENABLED', 'false') == 'true':
    setup_prometheus_metrics()

Запускайте нагрузочные тесты с включенными метриками:

# Локальный Prometheus + Grafana
docker-compose up -d

# Нагрузочный тест
locust -f load_test.py --host=http://localhost:2121

Смотрите метрики в реальном времени → находите bottlenecks.

7. Документируйте метрики

Создайте README с описанием всех метрик:

# Metrics Documentation

## HTTP Metrics

### `http_requests_total`
- **Type:** Counter
- **Labels:** method, endpoint, status_code
- **Description:** Total HTTP requests
- **Dashboard:** Main → HTTP Performance
- **Alert:** High error rate if 5xx &gt; 1%

### `http_request_duration_seconds`
- **Type:** Histogram
- **Labels:** method, endpoint
- **Buckets:** [.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10]
- **Description:** Request latency distribution
- **Dashboard:** Main → Response Time P95

Production Checklist

Перед запуском в production проверьте:

  • [ ] Retention policy настроен (storage.tsdb.retention.time)

  • [ ] Disk space мониторится (Prometheus может занять много места)

  • [ ] Backups настроены для Grafana dashboards

  • [ ] Алерты протестированы (создайте искусственную ошибку)

  • [ ] Notification channels работают (отправьте тестовый алерт)

  • [ ] Access control настроен (не оставляйте Grafana с admin/admin!)

  • [ ] HTTPS настроен для Grafana (через nginx reverse proxy)

  • [ ] Cardinality проверен (topk(10, count by (__name__)({__name__=~".+"})))

  • [ ] Документация создана (какая метрика за что отвечает)

  • [ ] On-call process определен (кто получает алерты и что делать)

Реальный кейс: находим проблему

Представим: пользователи жалуются на медленную работу. Вот как мониторинг помог найти и решить проблему за считанные минуты.

Шаг 1: Открываем Grafana → HTTP Performance Dashboard

histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

Видим: P95 latency выросла с 0.2s до 3s.

Шаг 2: Смотрим latency по endpoint'ам

topk(5, histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) by (endpoint))

Находим: /api/activities — 5 секунд!

Шаг 3: Проверяем внешние API

histogram_quantile(0.95, rate(external_api_latency_seconds_bucket[5m])) by (endpoint_type)

External API athlete_activities — 4.8 секунд. Вот проблема!

Шаг 4: Смотрим error rate

rate(external_api_errors_total{endpoint_type="athlete_activities"}[5m])

Ошибок нет, просто медленно. Значит, проблема не на нашей стороне — внешний сервис тормозит.

Решение:

  • Добавляем агрессивное кэширование для внешнего API (TTL 5 минут)

  • Настраиваем алерт на latency > 2s

  • Добавляем timeout на запросы

Шаг 5: После деплоя проверяем

# Cache hit rate
(cache_hits_total / (cache_hits_total + cache_misses_total)) * 100

Hit rate 85% → latency упала до 0.3s. Победа! ?

Что дальше?

Вы построили production-ready систему мониторинга. Но это только начало:

Следующие шаги:

  1. Distributed Tracing — добавьте Jaeger/Tempo для трейсинга запросов

  2. Logging — интегрируйте Loki для централизованных логов

  3. Custom Dashboards — создайте дашборды для бизнеса (не только DevOps)

  4. SLO/SLI — определите Service Level Objectives

  5. Anomaly Detection — используйте машинное обучение для детекции аномалий

  6. Cost Monitoring — добавьте метрики затрат (AWS CloudWatch, etc.)

Полезные ресурсы:

Заключение

Система мониторинга — это не "поставил и забыл". Это живой организм, который нужно развивать вместе с приложением. Но базовая архитектура, которую мы построили, масштабируется от стартапа до enterprise.

Ключевые выводы:

  1. Три уровня метрик: HTTP (инфраструктура) → API (зависимости) → Business (продукт)

  2. Middleware автоматизирует сбор базовых метрик

  3. PromQL мощный — изучайте постепенно

  4. Labels важны — но не переборщите с cardinality

  5. Алерты критичны — мониторинг без алертов бесполезен

  6. Документируйте — через полгода вы забудете, что значит метрика foo_bar_total

Мониторинг — это культура, а не инструмент. Начните с простого, итерируйте, улучшайте. И ваше приложение будет работать стабильно, а вы будете спать спокойно ?


О проекте Peakline

Эта система мониторинга разработана для Peakline — веб-приложения для анализа Strava активностей. Peakline предоставляет спортсменам:

  • Детальный анализ сегментов с интерактивными картами

  • Исторические данные о погоде для каждой активности

  • Генерацию продвинутых FIT-файлов для виртуальных гонок

  • Автоматическое исправление ошибок в GPX треках

  • Планировщик маршрутов

Все эти фичи требуют надежного мониторинга для обеспечения качественного пользовательского опыта.


Вопросы? Пишите в комментариях!

P.S. Если статья была полезна — поделитесь с коллегами, кому может пригодиться.

Об авторе

Solo developer, создающий Peakline — инструменты для спортсменов. Сам спортсмен и энтузиаст, верю в automation, observability и качественный код. В 2025 году продолжаю развивать проект и делиться опытом с сообществом.

Комментарии (0)