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

Что внутри:
Архитектура метрик (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
Ключевые техники:
Нормализация путей — критически важно! Без этого получите тысячи уникальных метрик для
/api/activities/1,/api/activities/2, etc.Labels — позволяют фильтровать и группировать метрики в PromQL
Отдельные счетчики для ошибок — упрощают написание алертов
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: визуализация
Теперь самое интересное — превращаем сырые метрики в красивые и информативные дашборды.

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
Критически важно мониторить зависимости от внешних сервисов — они могут стать узким местом.

Панель 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 поддерживает переменные для интерактивных дашбордов:
Создание переменной
Dashboard Settings → Variables → Add variable
Name:
endpointType: Query
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 > 1
Condition:
> 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])) > 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])) > 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 > 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 систему мониторинга. Но это только начало:
Следующие шаги:
Distributed Tracing — добавьте Jaeger/Tempo для трейсинга запросов
Logging — интегрируйте Loki для централизованных логов
Custom Dashboards — создайте дашборды для бизнеса (не только DevOps)
SLO/SLI — определите Service Level Objectives
Anomaly Detection — используйте машинное обучение для детекции аномалий
Cost Monitoring — добавьте метрики затрат (AWS CloudWatch, etc.)
Полезные ресурсы:
Заключение
Система мониторинга — это не "поставил и забыл". Это живой организм, который нужно развивать вместе с приложением. Но базовая архитектура, которую мы построили, масштабируется от стартапа до enterprise.
Ключевые выводы:
Три уровня метрик: HTTP (инфраструктура) → API (зависимости) → Business (продукт)
Middleware автоматизирует сбор базовых метрик
PromQL мощный — изучайте постепенно
Labels важны — но не переборщите с cardinality
Алерты критичны — мониторинг без алертов бесполезен
Документируйте — через полгода вы забудете, что значит метрика
foo_bar_total
Мониторинг — это культура, а не инструмент. Начните с простого, итерируйте, улучшайте. И ваше приложение будет работать стабильно, а вы будете спать спокойно ?
О проекте Peakline
Эта система мониторинга разработана для Peakline — веб-приложения для анализа Strava активностей. Peakline предоставляет спортсменам:
Детальный анализ сегментов с интерактивными картами
Исторические данные о погоде для каждой активности
Генерацию продвинутых FIT-файлов для виртуальных гонок
Автоматическое исправление ошибок в GPX треках
Планировщик маршрутов
Все эти фичи требуют надежного мониторинга для обеспечения качественного пользовательского опыта.
Вопросы? Пишите в комментариях!
P.S. Если статья была полезна — поделитесь с коллегами, кому может пригодиться.
Об авторе
Solo developer, создающий Peakline — инструменты для спортсменов. Сам спортсмен и энтузиаст, верю в automation, observability и качественный код. В 2025 году продолжаю развивать проект и делиться опытом с сообществом.