Alertas
O subsistema de alerta do Ongrid é um loop de tick único que percorre cada linha de rule habilitada, pergunta ao backend apropriado (Prom para métricas + trace spanmetrics, Loki para logs) se o predicado bate, e registra disparos na tabela incidents.
Não há Alertmanager separado, nem arquivo de rules separado. As rules vivem no MySQL, o evaluator faz polling com refresh de cache de 30s, e as notificações fazem fan-out via o registry de canais.
As 14 categorias de rule
Rules são armazenadas com uma coluna kind. O compiler despacha em cima dela.
O compiler está em rules.go e os evaluators em evaluators_phaseA.go + evaluators_phaseB.go.
O split 8+6 é Phase-A (métricas) / Phase-B (logs + traces) do HLD-004, entregue em 2026-05-08.
Categorias de métrica (Phase A)
| Kind | O que faz | Campos do spec |
|---|---|---|
metric_raw | A expressão PromQL É o predicado. Dispara por entrada de vetor retornada. | expr |
metric_anomaly | Z-score ou MAD sobre uma janela rolling de baseline. | metric, method, baseline_window, baseline_step, deviation, for_seconds |
metric_forecast | predict_linear(metric[fit_window], predict_seconds) <op> threshold. | metric, fit_window, predict_seconds, operator, threshold |
metric_burn_rate | Multi-window multi-burn do SRE do Google sobre um SLO. TODAS as janelas precisam disparar. | sli, slo, burns[].window, burns[].multiplier |
O kind legado prom_query foi renomeado para metric_raw. A forma legada metric_threshold agora é uma entrada exclusiva da UI que compila para metric_raw na hora do save — não existe evaluator separado.
// 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`
}Categorias de log + trace (Phase B)
| Kind | O que faz | Backend |
|---|---|---|
log_match | count_over_time(<stream> |~ <filter> [window]) <op> threshold contra Loki. Dispara por label-set. | Loki |
log_volume | Mesmo formato de log_match, contagem da janela atual vs threshold absoluto. | Loki |
trace_latency | histogram_quantile(q, sum by(le)(rate(traces_spanmetrics_latency_bucket[w]))) > threshold_ms. | Prom (spanmetrics) |
trace_error_rate | 100 * (sum rate(traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}) / sum rate(...)) > pct. | Prom (spanmetrics) |
Categorias de trace consultam o Prometheus, não o Tempo. O gerador de spanmetrics raspa o Tempo e grava séries traces_spanmetrics_* de volta no Prom — consultar o Prom mantém o evaluator de alerta em uma única query engine e reaproveita toda a lógica de filtragem por operador / threshold.
Tipos de scope
Toda rule tem scope_type ∈ {host, global, monitoring_pipeline}. Padrão por kind definido em defaultScopeForKind em rules.go.
host— incidente deve carregar umdevice_id. O evaluator parseia a labeldevice_iddos resultados do Prom;validateFiringrejeita disparos host-scoped sem ela.global— alertas a nível de serviço (trace_, log_) que não fixam em um único host.monitoring_pipeline— meta-alertas sobre o próprio Ongrid (scrape_down,prom_ingest_fail, ...).
O tick do evaluator
PipelineEvaluator.evaluate roda a cada Interval (padrão 5 min, configurável via PipelineEvaluatorOpts.Interval).
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)
}
}Um backend nil pula silenciosamente as categorias correspondentes — Loki fora não quebra alertas de métrica.
Dedup + recovery
O evaluator rastreia firingSnapshot[ruleKey] = set<dedupeKey> através de ticks. Uma chave presente no tick anterior mas ausente neste tick → o filtro de comparação do PromQL descartou a série → predicado limpou → SystemResolveIncident dispara com "prom condition cleared". É assim que os alarmes se recuperam sem um evaluator separado de "resolve".
Formato da dedupe key: pipeline:<rule_key>:<sorted-label-set> — labels de proveniência (__name__, ongrid_source) são removidas de modo que o mesmo alarme reportado pelo collector embarcado e pelo cloud seja deduplicado em um único incidente, não dois (labelSetKey).
Fan-out por canal
Quando um incidente dispara, o caminho Notifier.MaybeNotify consulta o ChannelResolver:
- Pinning por rule — se
rule.notify_channel_ids_jsonnão estiver vazio, só esses channel ids casam (e somente os habilitados). - Caso contrário, toda linha habilitada em
notification_channelsé filtrada pormatch_severity_minematch_scope_types. - Se nada bate, o resolver cai para uma lista de canal sintética semeada de
DefaultChannelspara que notificações nunca desapareçam.
Veja router.go.
Inibição
Duas regras de inibição embutidas (inhibit.go), cobrindo os casos padrão barulhentos:
edge_offline:edge_Xinibe qualquerhost:X:*— quando um edge está inalcançável, todo alarme host-scoped nele é suprimido.pipeline:prom_ingest_failinibepipeline:scrape_down:*— quando o próprio Prometheus não consegue ingerir, todo alarme "target down" é ruído.
Uma futura tabela inhibition_rules estende isso a grupos definidos pelo admin.
Cooldown + dampening
NotifyOpts.Cooldown (padrão 10 minutos) limita re-notificação na mesma dedupe_key. O filtro de dampening fica dentro de Usecase.MaybeNotify para que o resolver de canal e o inibidor ainda rodem em cada disparo — só o Notifier.Send em si é pulado.
Veja também
- RCA — o que acontece quando um incidente dispara.
- Logs — Loki + os evaluators
log_match/log_volume. - Traces — Tempo + os evaluators
trace_latency/trace_error_rate. - Visão geral dos canais — como canais Slack / Telegram / Lark / DingTalk / WeCom + webhook são configurados.
- Schema da rule de alerta — o formato wire da linha de rule.