Practical SRE & DevOps guides. Linux debugging. Lifestyle

Prometheus + Grafana + Alertmanager


1. Архітектура стеку: хто що робить

[ Targets ]  ←── scrape ──  [ Prometheus ]  ──→  [ Grafana ]
                                   │
                              [ Alertmanager ]  ──→  Slack / PagerDuty / Email
КомпонентРольПротокол
PrometheusЗбір, зберігання, обчисленняHTTP pull (scrape)
GrafanaВізуалізація, дашбордиHTTP API до Prometheus
AlertmanagerРоутинг, дедублікація, мовчання алертівHTTP від Prometheus
ExporterКонвертує метрики у Prometheus-форматHTTP /metrics endpoint

Ключовий принцип — pull model: Prometheus сам ходить до таргетів кожні N секунд (за замовчуванням 15 секунд). Це протилежність push-моделі (StatsD, InfluxDB Telegraf). Pull дає:


2. Що таке метрика в Prometheus: ключ-значення зсередини

2.1 Формат exposition (текстовий)

Кожен /metrics endpoint повертає plain text:

# HELP http_requests_total Total HTTP requests received
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200",handler="/api/users"} 1847
http_requests_total{method="POST",status="201",handler="/api/users"} 312
http_requests_total{method="GET",status="404",handler="/api/unknown"} 23

Розбір по рядках:

# HELP http_requests_total Total HTTP requests received
^      ^                   ^
│      │                   └── Опис метрики (опціонально, але обов'язково для хорошого коду)
│      └── Ім'я метрики
└── Директива HELP

# TYPE http_requests_total counter
^      ^                   ^
│      │                   └── Тип метрики: counter | gauge | histogram | summary | untyped
│      └── Ім'я метрики
└── Директива TYPE

http_requests_total{method="GET",status="200",handler="/api/users"} 1847
^                   ^                                                ^
│                   │                                                └── Поточне значення (float64)
│                   └── Labels: ключ-значення пари — саме вони утворюють унікальний time series
└── Metric name

2.2 Як Prometheus зберігає дані: TSDB

Prometheus використовує власну Time Series Database (TSDB), оптимізовану для append-only запису.

Ідентифікація time series — це пара:

metric_name + sorted set of labels = унікальний series

Приклад: наступні три рядки — це три різних time series:

http_requests_total{method="GET",  status="200"} 1847
http_requests_total{method="POST", status="200"} 312
http_requests_total{method="GET",  status="404"} 23

Що зберігається на диску:

Prometheus зберігає дані у блоках (blocks), кожен блок охоплює 2 години:

/prometheus/data/
├── 01GK4XY.../          ← block (2h chunk)
│   ├── chunks/          ← стиснуті дані (XOR delta encoding)
│   ├── index            ← inverted index label→series
│   └── meta.json
├── 01GK6AB.../
└── wal/                 ← Write-Ahead Log (останні ~2 години в пам'яті)

WAL (Write-Ahead Log): свіжі дані спочатку пишуться в WAL (пам’ять + диск), потім кожні 2 години компактуються у block. Це захищає від втрати даних при падінні.

Cardinality — головна причина OOM у Prometheus:

# Кількість унікальних time series = кросс-добуток унікальних label values
# Наприклад:
# method: GET, POST, PUT, DELETE = 4 значення
# status: 200, 201, 404, 500 = 4 значення  
# handler: 50 різних ендпоінтів
# Разом: 4 × 4 × 50 = 800 time series для одної метрики

Погана практика — динамічні labels з високою кардинальністю:

# НІКОЛИ так не робіть:
http_requests_total{user_id="12345"} 1   # user_id може мати мільйони значень!
http_requests_total{request_id="abc-123-xyz"} 1  # унікальний на кожен запит — катастрофа

3. Типи метрик: Counter, Gauge, Histogram, Summary

3.1 Counter — лічильник, що тільки зростає

Визначення: монотонно зростаюче значення. Скидається тільки при рестарті процесу.

Конвенція: суфікс _total у назві.

# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 182947
http_requests_total{method="GET",status="500"} 47

Що відбувається при рестарті: значення скидається до 0. Саме тому ніколи не використовуйте сирий counter у графіках — ви побачите вертикальне падіння при кожному деплої.

Правильне використання — завжди через rate() або increase():

# Кількість запитів на секунду (за останні 5 хвилин)
rate(http_requests_total[5m])

# Кількість запитів за останні 5 хвилин (абсолютна кількість)
increase(http_requests_total[5m])

Де застосовується:


3.2 Gauge — поточне значення

Визначення: значення, яке може зростати і зменшуватись довільно. Відображає поточний стан.

# HELP node_memory_MemAvailable_bytes Available memory in bytes
# TYPE node_memory_MemAvailable_bytes gauge
node_memory_MemAvailable_bytes 2.147e+09

# HELP go_goroutines Number of goroutines
# TYPE go_goroutines gauge
go_goroutines 42

# HELP node_load1 1-minute load average
# TYPE node_load1 gauge
node_load1 0.85

Ключова різниця від Counter: gauge можна використовувати напряму без rate().

# Пряме використання — правильно для gauge:
node_memory_MemAvailable_bytes
node_load1
go_goroutines

# Обчислення % використання пам'яті:
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100

# Тренд gauge за час (похідна):
deriv(node_load1[10m])   # чи зростає навантаження?
predict_linear(node_disk_free_bytes[1h], 4*3600)  # прогноз місця на диску через 4 години

Де застосовується:


3.3 Histogram — розподіл значень

Визначення: автоматично групує спостереження у попередньо визначені bucket-и (відра). Дозволяє обчислювати перцентилі на стороні Prometheus/Grafana.

Histogram генерує 3 метрики автоматично:

# Для метрики http_request_duration_seconds histogram:

# 1. _bucket — кількість запитів, що вклалися у ≤ N секунд
http_request_duration_seconds_bucket{le="0.005"} 24
http_request_duration_seconds_bucket{le="0.01"}  36
http_request_duration_seconds_bucket{le="0.025"} 89
http_request_duration_seconds_bucket{le="0.05"}  120
http_request_duration_seconds_bucket{le="0.1"}   142
http_request_duration_seconds_bucket{le="0.25"}  155
http_request_duration_seconds_bucket{le="0.5"}   158
http_request_duration_seconds_bucket{le="1"}     159
http_request_duration_seconds_bucket{le="2.5"}   160
http_request_duration_seconds_bucket{le="+Inf"}  160  ← завжди є, = total count

# 2. _count — загальна кількість спостережень
http_request_duration_seconds_count 160

# 3. _sum — сума всіх спостережень
http_request_duration_seconds_sum 12.34

Як читати bucket-и: кожен bucket є кумулятивним (le = “less or equal”). le="0.1" означає “скільки запитів тривали ≤ 100ms”.

Обчислення перцентилів у PromQL:

# p50 (медіана) латентності за останні 5 хвилин
histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))

# p95 — 95% запитів вклалися у це значення
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# p99 — "довгий хвіст", критично для SLA
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

# Середнє значення (з суми та кількості):
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])

Чому важливо правильно задати bucket-и:

Bucket-и задаються при оголошенні метрики у коді. Якщо ваші запити тривають від 1ms до 10s, але bucket-и задані для 0..1s — перцентилі будуть неточними. Загальне правило: bucket-и мають покривати 95%+ очікуваних значень з достатньою деталізацією.

# Приклад у Python (prometheus_client):
from prometheus_client import Histogram

REQUEST_LATENCY = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration',
    buckets=[.005, .01, .025, .05, .1, .25, .5, 1.0, 2.5, 5.0, 10.0]
)

Де застосовується: латентність запитів, розмір payload, час виконання черги.


3.4 Summary — клієнтські перцентилі

Визначення: схожий на Histogram, але перцентилі обчислюються на стороні клієнта (в самому застосунку), а не в Prometheus.

# HELP go_gc_duration_seconds A summary of GC invocation durations
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 4.9351e-05
go_gc_duration_seconds{quantile="0.25"} 7.5552e-05
go_gc_duration_seconds{quantile="0.5"} 0.000116
go_gc_duration_seconds{quantile="0.75"} 0.000181
go_gc_duration_seconds{quantile="0.99"} 0.001445
go_gc_duration_seconds_sum 0.35
go_gc_duration_seconds_count 2693

Порівняння Histogram vs Summary:

АспектHistogramSummary
Де рахуються квантиліСервер (Prometheus)Клієнт (застосунок)
Агрегація across instances✅ Можливо❌ Неможливо
Точність квантилівЗалежить від bucket-івВисока
Навантаження на клієнтМінімальнеПомірне
Рекомендація 2024ВикористовуватиТільки якщо точність критична

Критична різниця — агрегація:

# Histogram — можна агрегувати по всіх інстансах:
histogram_quantile(0.95,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
)

# Summary — НЕ можна агрегувати квантилі по інстансах:
# avg(go_gc_duration_seconds{quantile="0.99"}) — математично НЕКОРЕКТНО!
# Середнє перцентилів ≠ перцентиль всієї вибірки

4. PromQL: rate, increase, irate та інші функції

4.1 rate() — швидкість зміни counter

rate(http_requests_total[5m])

Що робить: обчислює середню швидкість зміни counter за вказаний часовий інтервал.

Алгоритм під капотом:

  1. Знаходить усі scrape-точки у вікні [5m]
  2. Обробляє reset-и counter (якщо значення впало — значить був рестарт)
  3. Обчислює: (last_value - first_value) / duration_in_seconds
  4. Результат: значення в одиницях на секунду
# rate(http_requests_total[5m]) = 2.3
# Означає: в середньому 2.3 запити/секунду за останні 5 хвилин

# Для отримання запитів/хвилину:
rate(http_requests_total[5m]) * 60

# Відсоток помилок:
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) * 100

Правило вибору інтервалу: інтервал [Xm] має бути мінімум у 4 рази більший за scrape interval.

Занадто малий інтервал → нестабільні результати (мало точок для обчислення).


4.2 increase() — абсолютне збільшення за інтервал

increase(http_requests_total[1h])

Що робить: скільки разів counter збільшився за вказаний час.

Математика: increase(m[d]) = rate(m[d]) * duration_in_seconds

# Кількість запитів за останню годину:
increase(http_requests_total[1h])

# Кількість 5xx помилок за останні 24 години:
increase(http_requests_total{status=~"5.."}[24h])

# Кількість failed jobs за останній день:
increase(batch_jobs_failed_total[24h])

Важливо: increase() також обробляє reset-и counter, тобто якщо процес перезапустився — це не псує результат.


4.3 irate() — миттєва швидкість (остання пара точок)

irate(http_requests_total[5m])

Що робить: обчислює rate тільки між двома останніми scrape-точками у вікні.

Різниця від rate():

Часові точки:  t=0   t=15s   t=30s   t=45s   t=60s   t=75s
Значення:      100   115     130     160     161     163

rate([5m]) = середнє за всі точки = плавний результат
irate([5m]) = (163 - 161) / 15 = 0.13/s  (тільки останні 2 точки)
ФункціяПеревагиНедоліки
rate()Плавна, стабільна, добре для алертівМоже “замилювати” піки
irate()Реагує на різкі пікиНестабільна, шумна; погана для алертів

Коли використовувати irate(): тільки для графіків реального часу, де потрібно бачити піки. Ніколи у правилах алертів.


4.4 Інші важливі функції

# --- Агрегаційні оператори ---

# Сума по всіх інстансах:
sum(rate(http_requests_total[5m]))

# Сума, зберігаючи тільки label "job":
sum by (job) (rate(http_requests_total[5m]))

# Сума, прибираючи тільки label "instance":
sum without (instance) (rate(http_requests_total[5m]))

# Максимум по всіх інстансах:
max by (instance) (node_load1)

# Топ-5 найзавантаженіших хостів:
topk(5, node_load1)

# --- Функції часу ---

# Скільки секунд тому відбувся останній scrape (для виявлення "мертвих" таргетів):
time() - node_boot_time_seconds  # uptime в секундах

# Прогноз вільного місця (лінійна екстраполяція):
predict_linear(node_filesystem_free_bytes{mountpoint="/"}[6h], 24*3600)
# "Якщо тренд останніх 6h продовжиться — скільки байт буде через 24h"

# --- Зміна у часі ---

# Різниця між поточним і N секунд тому (для gauge):
delta(node_load1[10m])

# Похідна (trend):
deriv(node_load1[10m])

# --- Відбір по значенню ---

# Тільки інстанси з load > 2.0:
node_load1 > 2.0

# Тільки інстанси де є помилки (rate > 0):
rate(http_requests_total{status=~"5.."}[5m]) > 0

# Умовний вираз — якщо error rate > 0.01, повернути значення, інакше nothing:
rate(http_requests_total{status=~"5.."}[5m]) > 0.01

5. Label matching та Vector matching

# Бінарні операції між метриками вимагають збігу labels

# Відсоток вільної пам'яті (обидві метрики мають label "instance"):
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100

# Якщо labels не збігаються — потрібен ignoring або on:
# Помилки на запит (метрики мають різні sets labels):
rate(http_errors_total[5m]) / ignoring(error_type) rate(http_requests_total[5m])

# group_left — many-to-one join (додати label з іншої метрики):
rate(http_requests_total[5m])
* on(instance) group_left(datacenter)
node_meta_info  # метрика з додатковим label "datacenter"

6. Grafana: побудова дашбордів

6.1 Типи панелей і коли які використовувати

Тип панеліВикористання
Time seriesБудь-які метрики у часі (CPU, latency, RPS)
StatОдне поточне значення (uptime, поточний RPS)
GaugeЗначення у відсотках з кольоровими зонами
Bar chartПорівняння між сутностями (топ хостів)
HeatmapРозподіл histogram по часу
TableДетальні дані (список алертів, топ помилок)
LogsLoki logs

6.2 Приклад: HTTP RPS дашборд

Панель: Requests per second by status

# Query A — успішні запити:
sum by (status) (rate(http_requests_total{job="my-api"}[5m]))

# Legend: {{status}}
# Visualization: Time series, Fill opacity: 20

Панель: Error rate %

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

# Unit: Percent (0-100)
# Thresholds: 0=green, 1=yellow, 5=red

Панель: p50/p95/p99 latency

# Query A — p50:
histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket{job="my-api"}[5m])))

# Query B — p95:
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{job="my-api"}[5m])))

# Query C — p99:
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{job="my-api"}[5m])))

# Legend: p{{quantile_label}}   (або задати вручну: p50, p95, p99)
# Unit: seconds (s)

6.3 Template variables для динамічних дашбордів

# У Settings → Variables:
# Variable 1:
Name: instance
Type: Query
Query: label_values(node_cpu_seconds_total, instance)
# Тепер у запитах використовуємо: {instance="$instance"}

# Variable 2:
Name: interval
Type: Interval
Values: 1m,5m,10m,30m,1h
Default: 5m
# Використовуємо: rate(metric[$interval])
# З variables — динамічний запит:
rate(http_requests_total{instance="$instance", job="$job"}[$interval])

6.4 Heatmap для Histogram

Heatmap — найкращий спосіб візуалізувати розподіл латентності у часі:

# Query для Heatmap панелі:
sum(rate(http_request_duration_seconds_bucket{job="my-api"}[$interval])) by (le)

# Format: Heatmap
# Bucket bound: Upper (тому що le = less-or-equal, тобто верхня межа)

7. Alertmanager: маршрутизація і дедублікація

7.1 Правила алертів у Prometheus

# /etc/prometheus/rules/api.yml
groups:
  - name: api.alerts
    interval: 1m  # як часто перевіряти (default: global evaluation_interval)
    rules:

      # Алерт на high error rate
      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m]))
          /
          sum(rate(http_requests_total[5m]))
          * 100 > 5          
        for: 5m
        # ^ "for" — алерт спрацьовує тільки якщо умова TRUE протягом 5 хвилин безперервно.
        # Без "for": будь-який спайк → негайний алерт (багато false positives)
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "High error rate on {{ $labels.job }}"
          description: |
            Error rate is {{ $value | printf "%.2f" }}% 
            for job {{ $labels.job }} on instance {{ $labels.instance }}.
            Current threshold: 5%            

      # Алерт на latency
      - alert: HighLatencyP99
        expr: |
          histogram_quantile(0.99,
            sum by (le, job) (rate(http_request_duration_seconds_bucket[5m]))
          ) > 1.0          
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "p99 latency > 1s for {{ $labels.job }}"
          description: "p99 = {{ $value | printf \"%.3f\" }}s"

      # Алерт на відсутність метрик (dead exporter)
      - alert: ExporterDown
        expr: up == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Exporter down: {{ $labels.instance }}"

Цикл стану алерту:

INACTIVE → PENDING → FIRING → RESOLVED
           ^          ^
           │          └── умова true > for duration
           └── умова стала true

7.2 Конфігурація Alertmanager

# /etc/alertmanager/alertmanager.yml
global:
  smtp_smarthost: 'smtp.gmail.com:587'
  smtp_from: 'alerts@company.com'
  slack_api_url: 'https://hooks.slack.com/services/T.../B.../xxx'

# Шаблони повідомлень
templates:
  - '/etc/alertmanager/templates/*.tmpl'

# Дерево маршрутизації
route:
  receiver: 'default-receiver'  # fallback
  group_by: ['alertname', 'job']
  # ^ групувати алерти з однаковим alertname+job в одне повідомлення
  group_wait: 30s
  # ^ чекати 30s перед відправкою першої нотифікації (щоб зібрати групу)
  group_interval: 5m
  # ^ мінімальний інтервал між повідомленнями для тієї ж групи
  repeat_interval: 4h
  # ^ якщо алерт не resolved — повторювати кожні 4h

  routes:
    # Critical → Slack #alerts-critical + PagerDuty
    - match:
        severity: critical
      receiver: 'critical-receiver'
      continue: false  # не передавати далі по дереву

    # Warning → Slack #alerts-warning тільки в робочий час
    - match:
        severity: warning
      receiver: 'warning-receiver'
      active_time_intervals:
        - business_hours

    # Конкретна команда
    - match_re:
        team: ^(backend|api)$
      receiver: 'backend-team'

time_intervals:
  - name: business_hours
    time_intervals:
      - weekdays: ['monday:friday']
        times:
          - start_time: '09:00'
            end_time: '18:00'
        location: 'Europe/Kyiv'

receivers:
  - name: 'default-receiver'
    slack_configs:
      - channel: '#alerts-general'
        send_resolved: true
        title: '{{ template "slack.title" . }}'
        text: '{{ template "slack.text" . }}'

  - name: 'critical-receiver'
    slack_configs:
      - channel: '#alerts-critical'
        send_resolved: true
    pagerduty_configs:
      - service_key: '<YOUR_PD_KEY>'

  - name: 'backend-team'
    slack_configs:
      - channel: '#backend-alerts'

inhibit_rules:
  # Якщо instance Down — не посилати інші алерти для цього instance
  - source_match:
      alertname: InstanceDown
    target_match_re:
      alertname: .+
    equal: ['instance']

7.3 Silence — заглушення алертів

# CLI через amtool:
amtool silence add \
  --alertmanager.url=http://alertmanager:9093 \
  --duration=2h \
  --comment="Planned maintenance window" \
  alertname=HighErrorRate \
  job=my-api

# Переглянути активні silences:
amtool silence query --alertmanager.url=http://alertmanager:9093

8. Prometheus конфігурація: scrape та service discovery

# /etc/prometheus/prometheus.yml
global:
  scrape_interval: 15s         # як часто збирати метрики
  evaluation_interval: 15s     # як часто обчислювати alerting rules
  scrape_timeout: 10s          # timeout для одного scrape

# Правила та алерти
rule_files:
  - "/etc/prometheus/rules/*.yml"

# Alertmanager
alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

scrape_configs:
  # --- Prometheus сам себе ---
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  # --- Node Exporter ---
  - job_name: 'node'
    static_configs:
      - targets:
          - 'server1.example.com:9100'
          - 'server2.example.com:9100'
    relabel_configs:
      # Замінити instance label на короткий hostname:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__address__]
        regex: '([^:]+):.*'
        target_label: instance
        replacement: '$1'

  # --- HAProxy exporter ---
  - job_name: 'haproxy'
    static_configs:
      - targets: ['haproxy-host:8405']
    labels:
      datacenter: 'nj'

  # --- File-based service discovery (динамічне оновлення без рестарту) ---
  - job_name: 'dynamic-targets'
    file_sd_configs:
      - files:
          - '/etc/prometheus/targets/*.json'
        refresh_interval: 30s

  # --- Blackbox exporter (зовнішній моніторинг HTTP) ---
  - job_name: 'blackbox-http'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - 'https://api.example.com/health'
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115

File-based SD — приклад файлу:

[
  {
    "targets": ["server10.dc1.example.com:9100", "server11.dc1.example.com:9100"],
    "labels": {
      "datacenter": "dc1",
      "role": "proxy"
    }
  }
]

9. Recording Rules — pre-computed метрики

Recording rules дозволяють попередньо обчислити важкі запити і зберегти результат як нову метрику.

# /etc/prometheus/rules/recording.yml
groups:
  - name: api_recording_rules
    interval: 1m
    rules:
      # Pre-compute error rate по job:
      - record: job:http_request_errors:rate5m
        expr: |
          sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
          /
          sum by (job) (rate(http_requests_total[5m]))          

      # Pre-compute p99 latency по job:
      - record: job:http_request_duration_p99:rate5m
        expr: |
          histogram_quantile(0.99,
            sum by (job, le) (rate(http_request_duration_seconds_bucket[5m]))
          )          

Конвенція найменування: level:metric:operations

Коли використовувати recording rules:


10. Best Practices: практичний чеклист

10.1 Найменування метрик

# Правила:
# 1. snake_case
# 2. Суфікс одиниці виміру:
#    _seconds, _bytes, _total, _ratio, _celsius, _info
# 3. Counter → _total суфікс
# 4. Не включати label name у metric name

# Добре:
http_request_duration_seconds
node_memory_free_bytes
http_requests_total

# Погано:
httpRequestDuration      # camelCase
http_request_time        # немає одиниці
http_get_requests_total  # метод (GET) краще як label

10.2 Labels: що варто і що не варто

# Добре — низька кардинальність, постійні значення:
{method="GET", status="200", datacenter="us-east-1"}

# Погано — висока кардинальність:
{user_id="12345"}          # мільйони користувачів
{url="/api/users/12345"}   # унікальний URL
{request_id="abc-xyz"}     # унікальний на запит
{timestamp="2024-01-01"}   # унікальне значення

10.3 Кардинальність: моніторинг і контроль

# Скільки активних time series у Prometheus:
prometheus_tsdb_head_series

# Топ метрик за кардинальністю:
topk(10, count by (__name__) ({__name__=~".+"}))

# Кількість series на метрику:
count by (__name__) ({__name__="http_requests_total"})
# Обмеження кардинальності на рівні scrape:
scrape_configs:
  - job_name: 'my-app'
    sample_limit: 5000  # якщо > 5000 samples — весь scrape відкидається
    label_limit: 30     # максимум labels на серію

10.4 Retention та Storage

# prometheus.yml запуск:
--storage.tsdb.retention.time=30d    # зберігати 30 днів
--storage.tsdb.retention.size=50GB   # або до 50GB (спрацьовує перший)

# Оцінка розміру (приблизна формула):
# needed_disk = retention_seconds * ingested_samples_per_second * bytes_per_sample
# bytes_per_sample ≈ 1-2 байти (після стиснення XOR delta encoding)
# 
# Приклад: 30d × 10,000 samples/s × 2 bytes = ~51GB

10.5 Rate/Increase: типові помилки

# ПОМИЛКА: rate на gauge
rate(node_memory_MemAvailable_bytes[5m])  # ❌ безглуздо — gauge не counter

# ПОМИЛКА: irate в alerting rule
alert: HighRPS
expr: irate(http_requests_total[5m]) > 1000  # ❌ нестабільний результат

# ПОМИЛКА: занадто малий інтервал
rate(http_requests_total[30s])  # ❌ якщо scrape_interval=15s — тільки 2 точки!

# ПРАВИЛЬНО:
rate(http_requests_total[5m])   # ✅ плавно, 20 точок при scrape 15s

10.6 Алерти: проти anti-patterns

# ПОГАНО: алерт без "for" → шквал false positives
- alert: HighCPU
  expr: node_load1 > 2.0
  # ← немає "for"! Будь-який секундний спайк = алерт

# ДОБРЕ: з обґрунтованим "for"
- alert: HighCPU
  expr: node_load1 > 2.0
  for: 10m  # проблема має бути стабільною

# ПОГАНО: алерт на сирий counter
- alert: ErrorsSpike
  expr: http_requests_total{status="500"} > 100  # ❌

# ДОБРЕ: алерт на rate
- alert: ErrorsSpike
  expr: rate(http_requests_total{status="500"}[5m]) > 0.1  # ✅

10.7 Grafana: UX та організація

Dashboard структура (рекомендована):
├── Row: Overview (Stat/Gauge панелі — одне число)
│   ├── Current RPS
│   ├── Error rate %
│   └── p99 Latency
├── Row: Traffic (Time series)
│   ├── RPS by status code
│   └── Bandwidth in/out
├── Row: Latency (Time series + Heatmap)
│   ├── p50/p95/p99 over time
│   └── Latency distribution heatmap
├── Row: Errors (Time series + Table)
│   ├── Error rate over time
│   └── Top errors (Table)
└── Row: Infrastructure
    ├── CPU / Memory
    └── Network I/O

11. Повний робочий приклад: моніторинг HAProxy

Реальний приклад для HAProxy native Prometheus exporter (порт 8405)

Метрики HAProxy

# Поточні з'єднання (gauge):
haproxy_frontend_current_sessions{proxy="http-in"}

# Запити (counter):
haproxy_frontend_http_requests_total{proxy="http-in"}

# HTTP відповіді по кодах (counter):
haproxy_frontend_http_responses_total{proxy="http-in", code="1xx"}
haproxy_frontend_http_responses_total{proxy="http-in", code="5xx"}

# Байти in/out (counter):
haproxy_frontend_bytes_in_total
haproxy_frontend_bytes_out_total

# Backend servers up/down (gauge, 0 або 1):
haproxy_server_status{proxy="backend", server="app1"}

PromQL запити для дашборду

# RPS по бекенду:
sum by (proxy) (rate(haproxy_frontend_http_requests_total[5m]))

# Error rate 5xx:
sum(rate(haproxy_frontend_http_responses_total{code="5xx"}[5m]))
/
sum(rate(haproxy_frontend_http_requests_total[5m]))
* 100

# Кількість живих серверів у кожному бекенді:
sum by (proxy) (haproxy_server_status == 1)

# Пропускна здатність (Мбіт/с):
sum(rate(haproxy_frontend_bytes_in_total[5m])) * 8 / 1e6

# Активні з'єднання vs максимум:
haproxy_frontend_current_sessions / haproxy_frontend_limit_sessions * 100

Alerting rules для HAProxy

groups:
  - name: haproxy
    rules:
      # Сервер виключився з ротації
      - alert: HAProxyServerDown
        expr: haproxy_server_status == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "HAProxy server {{ $labels.server }} is DOWN in {{ $labels.proxy }}"

      # Більше 50% серверів бекенду недоступні
      - alert: HAProxyBackendHalfDown
        expr: |
          sum by (proxy) (haproxy_server_status == 0)
          /
          sum by (proxy) (haproxy_server_status)
          > 0.5          
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "More than 50% servers down in backend {{ $labels.proxy }}"

      # Висока кількість 5xx помилок
      - alert: HAProxyHighErrorRate
        expr: |
          sum by (proxy) (rate(haproxy_frontend_http_responses_total{code="5xx"}[5m]))
          /
          sum by (proxy) (rate(haproxy_frontend_http_requests_total[5m]))
          * 100 > 5          
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "HAProxy 5xx error rate > 5% on {{ $labels.proxy }}"
          description: "Current rate: {{ $value | printf \"%.2f\" }}%"

      # Насичення з'єднань
      - alert: HAProxySessionSaturation
        expr: |
          haproxy_frontend_current_sessions
          / haproxy_frontend_limit_sessions
          * 100 > 80          
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "HAProxy session limit at {{ $value | printf \"%.0f\" }}%"

12. Діагностика та troubleshooting

# --- Prometheus UI (порт 9090) ---
# /graph         — PromQL playground
# /targets       — стан всіх таргетів (UP/DOWN)
# /rules         — стан recording/alerting rules
# /alerts        — активні алерти та їх стан
# /tsdb-status   — кардинальність, топ series за розміром
# /config        — поточна конфігурація

# --- Перевірка конфігурації перед застосуванням ---
promtool check config /etc/prometheus/prometheus.yml
promtool check rules /etc/prometheus/rules/*.yml

# --- Валідація alertmanager конфігу ---
amtool check-config /etc/alertmanager/alertmanager.yml

# --- Тест алерту (симуляція firing) ---
amtool alert add \
  --alertmanager.url=http://alertmanager:9093 \
  alertname=TestAlert \
  severity=warning \
  summary="Test alert from CLI"

# --- Розмір TSDB на диску ---
du -sh /var/lib/prometheus/

# --- API для отримання series ---
curl 'http://localhost:9090/api/v1/series?match[]=http_requests_total&start=2024-01-01T00:00:00Z&end=2024-01-01T01:00:00Z'

# --- Скільки samples ingested/s ---
curl -s 'http://localhost:9090/api/v1/query?query=rate(prometheus_tsdb_head_samples_appended_total[1m])' \
  | python3 -m json.tool

Підсумок

КонцепціяКлючовий висновок
Pull modelPrometheus сам ходить до таргетів — централізований контроль
Time seriesУнікальний ряд = metric name + набір labels
CounterТільки зростає → завжди використовувати rate()/increase()
GaugeПоточний стан → використовувати напряму
HistogramBuckets + server-side quantiles → агрегується між інстансами
SummaryClient-side quantiles → НЕ агрегується між інстансами
rate()Плавна середня швидкість → для алертів і дашбордів
irate()Миттєва швидкість між 2 точками → тільки для live графіків
CardinalityГоловна небезпека OOM → уникати dynamic labels
for: у alertахЗахист від false positives → мінімум 2-5 хвилин
Recording rulesPre-compute важких запитів → пришвидшують Grafana
inhibit_rulesПригнічення похідних алертів → менше шуму

#Prometheus #Grafana #Alertmanager #Monitoring #DevOps #Observability