Введение
После того как ваше веб-приложение попадает в продакшн, самый важный вопрос — а как оно работает прямо сейчас? Логи дают ответ постфактум, но хочется видеть проблемы до того, как пользователи начнут жаловаться.
В этой статье я расскажу, как построил полноценную систему мониторинга для 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 году продолжаю развивать проект и делиться опытом с сообществом.
