Пишем хорошие PrometheusRules: метки, ссылки на Grafana и humanizeTimestamp

Published: 2026-02-16

PrometheusRule, которое срабатывает вовремя и даёт дежурному достаточно контекста для действий, стоит десяти дашбордов. Плохие алерты либо шумят (срабатывают на каждый кратковременный сбой), либо молчат (неверные селекторы меток — алерт не доходит до Alertmanager). Вот паттерн, к которому мы пришли после нескольких десятков продакшн-алертов.


Анатомия правила алерта

yamlapiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: my-service-alerts
  namespace: observability
  labels:
    release: kube-prom-stack    # должно совпадать с селектором оператора Prometheus
spec:
  groups:
    - name: my-service
      interval: 30s
      rules:
        - alert: MyServiceDown
          expr: up{job="my-service"} < 1
          for: 5m
          labels:
            service: my-service
            severity: disaster
            groupp: admin
            url: 'https://grafana.example.com/d/my-service/overview'
            event: 'Service Down |{{$labels.job}}|{{$labels.instance}}'
          annotations:
            description: >-
              Service {{$labels.instance}} has been down for more than 5 min,
              timestamp: {{ with query "time()+10800" }}
              {{ . | first | value | humanizeTimestamp }}{{ end }}

Метка release: kube-prom-stack — именно так оператор kube-prometheus-stack обнаруживает PrometheusRule. Без неё правило молча игнорируется — никакой ошибки, никакого предупреждения. Проверить загрузку:

bash# Убедиться, что метка есть
kubectl get prometheusrule -n observability my-service-alerts -o yaml | grep -A5 "labels:"

# Проверить загрузку в Prometheus
kubectl port-forward -n observability svc/kube-prom-stack-prometheus 9090:9090
# открыть localhost:9090/rules в браузере

Почему interval: 30s на группе

Интервал вычисления правил в Prometheus по умолчанию — 1m. Для продакшн-сервисов, где нужны быстрые алерты, задайте группе интервал 30s — он переопределит глобальный только для этой группы, не затрагивая остальные.

Если уменьшить глобальный интервал до 30s, это затронет все правила, включая потенциально дорогие recording rules. Интервал на уровне группы — точечный подход.

Для некритичных алертов (использование диска, тренды глубины очереди) достаточно 2m — снижает нагрузку.


Метки для маршрутизации

Наш Alertmanager маршрутизирует по трём измерениям:

Метка Назначение
severity disaster / high / warning / average
groupp какая команда получит алерт (admin, dev, infra)
service для группировки и дедупликации

Двойная p в groupp — намеренно, чтобы избежать коллизии с зарезервированной меткой Prometheus group.

Конфигурация маршрутизации Alertmanager

yamlroute:
  group_by: ['alertname', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        severity: disaster
      receiver: pagerduty-critical
      continue: false
    - match:
        groupp: admin
      receiver: telegram-admin
    - match:
        groupp: dev
      receiver: telegram-dev

group_by: ['alertname', 'service'] гарантирует: если три экземпляра my-service упадут одновременно, придёт одно уведомление, а не три.


Метка url: ссылка на дашборд Grafana

Добавьте URL дашборда Grafana в метку — Alertmanager включит его в шаблон уведомления:

yamllabels:
  url: 'https://grafana.example.com/d/my-dashboard/my-service'

В шаблоне receiver Alertmanager:

{{ range .Alerts }}
*{{ .Labels.event }}*
{{ .Annotations.description }}
🔗 {{ .Labels.url }}
{{ end }}

Дежурный получает в уведомлении прямую ссылку на нужный дашборд — не придётся искать его в интерфейсе Grafana в три часа ночи.

Для ссылок с предустановленным временным диапазоном «вокруг момента срабатывания»:

yamlurl: 'https://grafana.example.com/d/my-dashboard/my-service?from=now-1h&to=now'

Метка event: понятный заголовок

yamlevent: 'Kafka Down |{{$labels.job}}|{{$labels.instance}}'

Эта метка становится заголовком уведомления. Контекст через разделители | позволяет с первого взгляда понять, какой экземпляр затронут.

Ещё примеры:

yamlevent: 'High Error Rate |{{$labels.service}}| {{$value | humanize}}%'
event: 'Disk Full |{{$labels.node}}| {{$value | humanize}}% used'
event: 'PVC Unbound |{{$labels.persistentvolumeclaim}}|{{$labels.namespace}}'

humanizeTimestamp для временных меток

Аннотации алерта вычисляются в момент срабатывания и могут включать PromQL-выражения. Всегда добавляйте читаемую временную метку:

yamlannotations:
  description: >-
    {{ .Labels.instance }} has been down for 5 min,
    timestamp: {{ with query "time()+10800" }}
    {{ . | first | value | humanizeTimestamp }}{{ end }}

time() возвращает Unix epoch. +10800 — поправка на UTC+3 (UTC+0 → +0, UTC+2 → +7200). humanizeTimestamp рендерит 2026-07-30 14:23:00 +0000 UTC.

Другие полезные функции humanize:

  • humanize — преобразует 1048576 в 1.05M
  • humanize1024 — преобразует 1048576 в 1.00Mi
  • humanizeDuration — преобразует 3665 в 1h 1m 5s

for: — не стрелять на флапах

Всегда задавайте for:. Без него один-единственный сбой сбора метрик мгновенно вызовет алерт.

Сценарий for:
Сервис полностью недоступен 1m или 5m
Всплеск ошибок 5m
Нехватка памяти/CPU 10m
Предупреждение по диску 15m
Истечение сертификата 1h

Group interval vs for

Это разные параметры:

  • interval: 30s (на группе) — как часто Prometheus вычисляет выражение правила
  • for: 5m — как долго выражение должно оставаться истинным, прежде чем алерт перейдёт из pending в firing

При for: 5m и интервале 30s Prometheus проверяет условие каждые 30 секунд и срабатывает после 10 положительных вычислений подряд. Если на седьмом условие перестало выполняться — алерт возвращается в pending, счётчик обнуляется.

Посмотреть pending-алерты:

bashcurl -s http://localhost:9090/api/v1/alerts | jq '.data.alerts[] | select(.state=="pending")'

Recording rules для дорогих выражений

yamlgroups:
  - name: my-service-recordings
    interval: 30s
    rules:
      - record: job:request_error_rate:5m
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)
          /
          sum(rate(http_requests_total[5m])) by (job)

  - name: my-service-alerts
    interval: 30s
    rules:
      - alert: HighErrorRate
        expr: job:request_error_rate:5m > 0.05
        for: 5m

Придерживайтесь соглашения об именовании job:metric_name:duration. Выражение алерта обращается к заранее вычисленной метрике, а не повторяет дорогой range query.


Тестирование правил локально

bashpromtool check rules my-prometheusrule.yaml

cat > test.yaml << 'EOF'
rule_files:
  - my-prometheusrule.yaml
tests:
  - interval: 1m
    input_series:
      - series: 'up{job="my-service",instance="host:9090"}'
        values: '1 1 1 0 0 0 0 0 0'
    alert_rule_test:
      - eval_time: 8m
        alertname: MyServiceDown
        exp_alerts:
          - exp_labels:
              severity: disaster
              instance: host:9090
EOF
promtool test rules test.yaml

Типичные ошибки

  1. Нет метки release — PrometheusRule молча не загружается
  2. Слишком маленький for: — поток алертов при каждом rolling update
  3. Нет метки service — Alertmanager группирует несвязанные алерты вместе
  4. Одинаковые имена алертов — второе правило молча перезаписывает первое
  5. Ошибки PromQL в аннотациях — алерт срабатывает, но в description выводится <error>; проверяйте через promtool check rules