Skip to content

Алерты

Подсистема алертов Ongrid — это единственный tick-цикл, который обходит каждую включённую строку правила, спрашивает соответствующий бэкенд (Prom для метрик + trace spanmetrics, Loki для логов), сопоставляется ли предикат, и записывает срабатывания в таблицу incidents.

Отдельного Alertmanager нет, нет отдельного rules-файла. Правила живут в MySQL, evaluator опрашивает их с обновлением кэша 30с, а уведомления расходятся через реестр каналов.

14 типов правил

Правила хранятся со столбцом kind. Компилятор диспетчирует на нём.

Компилятор находится в rules.go, а evaluator — в evaluators_phaseA.go + evaluators_phaseB.go.

Разделение 8+6 — это Phase-A (метрики) / Phase-B (логи + трейсы) HLD-004, приземлилось 2026-05-08.

Метрики (Phase A)

KindЧто делаетПоля spec
metric_rawPromQL-выражение И ЕСТЬ предикат. Срабатывает на каждую возвращённую запись вектора.expr
metric_anomalyZ-score или MAD поверх скользящего окна baseline.metric, method, baseline_window, baseline_step, deviation, for_seconds
metric_forecastpredict_linear(metric[fit_window], predict_seconds) <op> threshold.metric, fit_window, predict_seconds, operator, threshold
metric_burn_rateGoogle SRE multi-window multi-burn над SLO. ВСЕ окна должны триггериться.sli, slo, burns[].window, burns[].multiplier

Унаследованный kind prom_query переименован в metric_raw. Унаследованная форма metric_threshold теперь — UI-only запись, которая компилируется в metric_raw во время сохранения — отдельного evaluator для неё нет.

go
// internal/manager/biz/alert/rules.go:36
type MetricRawRule struct {
    ID         uint64
    RuleKey    string
    Name       string
    Severity   string
    ScopeType  string // host / global / monitoring_pipeline
    RunbookURL string
    Labels     map[string]string
    Expr       string // canonical predicate, e.g. `up == 0`
}

Логи + трейсы (Phase B)

KindЧто делаетБэкенд
log_matchcount_over_time(<stream> |~ <filter> [window]) <op> threshold против Loki. Срабатывание на label-set.Loki
log_volumeТакая же форма, как log_match, счёт текущего окна vs абсолютный порог.Loki
trace_latencyhistogram_quantile(q, sum by(le)(rate(traces_spanmetrics_latency_bucket[w]))) > threshold_ms.Prom (spanmetrics)
trace_error_rate100 * (sum rate(traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}) / sum rate(...)) > pct.Prom (spanmetrics)

Трейсовые типы запрашивают Prometheus, не Tempo. Генератор spanmetrics скрейпит Tempo и записывает серии traces_spanmetrics_* обратно в Prom — запрос к Prom держит alert evaluator на одном query-движке и переиспользует всю логику фильтрации операторов / порогов.

Типы scope

У каждого правила есть scope_type ∈ {host, global, monitoring_pipeline}. По умолчанию per-kind определяется в defaultScopeForKind в rules.go.

  • host — инцидент должен нести device_id. Evaluator парсит label device_id из меток результата Prom; validateFiring отвергает host-scoped срабатывания без него.
  • global — service-level алерты (trace_, log_), которые не закрепляются за единственным хостом.
  • monitoring_pipeline — мета-алерты о самом Ongrid (scrape_down, prom_ingest_fail, ...).

Evaluator tick

PipelineEvaluator.evaluate запускается каждый Interval (по умолчанию 5 мин, настраивается через PipelineEvaluatorOpts.Interval).

go
func (e *PipelineEvaluator) evaluate(ctx context.Context) {
    now := e.now()
    if e.edges != nil {
        e.refreshDeviceStalenessGauge(ctx, now)
    }
    if e.prom != nil {
        e.evaluatePromQuery(ctx, now)
        e.evaluateMetricAnomaly(ctx, now)
        e.evaluateMetricForecast(ctx, now)
        e.evaluateMetricBurnRate(ctx, now)
        e.evaluateTraceLatency(ctx, now)
        e.evaluateTraceErrorRate(ctx, now)
    }
    if e.logq != nil {
        e.evaluateLogMatch(ctx, now)
        e.evaluateLogVolume(ctx, now)
    }
}

nil-бэкенд молча пропускает соответствующие kind — упавший Loki не ломает алерты на метриках.

Dedup + recovery

Evaluator отслеживает firingSnapshot[ruleKey] = set<dedupeKey> между тиками. Ключ, присутствующий в прошлом тике, но отсутствующий в этом → фильтр сравнения PromQL отбросил серию → предикат снят → SystemResolveIncident срабатывает с "prom condition cleared". Так алармы восстанавливаются без отдельного evaluator «resolve».

Форма ключа dedupe: pipeline:<rule_key>:<sorted-label-set> — provenance-метки (__name__, ongrid_source) срезаются, чтобы один и тот же аларм, сообщённый и встроенным, и облачным коллектором, дедуплицировался в один инцидент, а не два (labelSetKey).

Fan-out по каналам

Когда инцидент срабатывает, путь Notifier.MaybeNotify обращается к ChannelResolver:

  1. Per-rule прикрепление — если rule.notify_channel_ids_json непустой, совпадают только эти channel id (и только включённые).
  2. Иначе каждая включённая строка notification_channels фильтруется по match_severity_min и match_scope_types.
  3. Если ничего не совпало, resolver откатывается на синтетический список каналов, посеянный из DefaultChannels, так что уведомления никогда не исчезают.

См. router.go.

Inhibition

Два встроенных правила inhibition (inhibit.go), покрывающие шумные дефолты:

  • edge_offline:edge_X подавляет любые host:X:* — когда edge недостижим, каждый host-scoped аларм на нём подавлен.
  • pipeline:prom_ingest_fail подавляет pipeline:scrape_down:* — когда сам Prometheus не может принять данные, каждый аларм «target down» — шум.

Будущая таблица inhibition_rules расширит это на admin-определённые группы.

Cooldown + дампенинг

NotifyOpts.Cooldown (по умолчанию 10 минут) ограничивает повторное уведомление на тот же dedupe_key. Фильтр дампенинга сидит внутри Usecase.MaybeNotify, так что resolver канала и inhibitor всё ещё запускаются на каждое срабатывание — пропускается только фактический Notifier.Send.

См. также