알림
Ongrid 의 알림 서브시스템은 모든 활성 규칙 행을 워크하고 적절한 백엔드 (메트릭 + 트레이스 spanmetrics 용 Prom, 로그용 Loki) 에 predicate 가 매칭되는지 묻고 incidents 테이블에 발화를 기록하는 단일 틱 루프 입니다.
별도의 Alertmanager 없음, 별도의 rules 파일 없음. 규칙은 MySQL 에 살고, evaluator 는 30 초 캐시 새로고침으로 폴링하며, 알림은 채널 레지스트리를 통해 팬아웃합니다.
14 규칙 kind
규칙은 kind 컬럼으로 저장. 컴파일러가 그것으로 디스패치.
컴파일러는 rules.go, evaluator 는 evaluators_phaseA.go + evaluators_phaseB.go 에 있습니다.
8+6 분할은 HLD-004 의 Phase-A (메트릭) / Phase-B (로그 + 트레이스), 2026-05-08 안착.
메트릭 kind (Phase A)
| Kind | 동작 | Spec 필드 |
|---|---|---|
metric_raw | PromQL 표현식이 predicate. 반환된 벡터 엔트리당 발화. | expr |
metric_anomaly | 롤링 기준선 윈도우 위의 Z-score 또는 MAD. | 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 | SLO 위의 Google SRE 다중 윈도우 다중 burn. 모든 윈도우가 트리거해야 함. | sli, slo, burns[].window, burns[].multiplier |
레거시 prom_query kind 는 metric_raw 로 이름 변경. 레거시 metric_threshold 폼은 이제 저장 시 metric_raw 로 컴파일되는 UI 전용 엔트리 — 별도 evaluator 없음.
// 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`
}로그 + 트레이스 kind (Phase B)
| Kind | 동작 | Backend |
|---|---|---|
log_match | Loki 에 대해 count_over_time(<stream> |~ <filter> [window]) <op> threshold. 레이블 셋별 발화. | Loki |
log_volume | log_match 와 같은 형태, 현재 윈도우 카운트 vs 절대 임계값. | 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) |
트레이스 kind 는 Prometheus 를 쿼리 하지 Tempo 가 아닙니다. Spanmetrics generator 가 Tempo 를 scrape 하고 traces_spanmetrics_* 시리즈를 다시 Prom 에 씁니다 — Prom 쿼리는 알림 evaluator 를 하나의 쿼리 엔진에 두고 모든 연산자 필터링 / 임계값 로직을 재사용합니다.
Scope 타입
모든 규칙은 scope_type ∈ {host, global, monitoring_pipeline} 를 가짐. Kind 별 기본은 rules.go 의 defaultScopeForKind 에 정의.
host— incident 가device_id를 운반해야 함. evaluator 가 Prom 결과 레이블에서device_id레이블을 파싱;validateFiring이 없는 호스트 범위 발화를 거부.global— 단일 호스트에 고정되지 않은 서비스 레벨 알림 (trace_, log_).monitoring_pipeline— Ongrid 자체에 대한 메타 알림 (scrape_down,prom_ingest_fail, ...).
Evaluator 틱
PipelineEvaluator.evaluate 가 Interval 마다 (기본 5 분, 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)
}
}nil 백엔드는 해당 kind 를 조용히 건너뜀 — Loki 다운이 메트릭 알림을 깨지 않음.
Dedup + 회복
evaluator 가 틱 전반에 걸쳐 firingSnapshot[ruleKey] = set<dedupeKey> 를 추적. 지난 틱에 있었고 이번 틱에 없는 키 → PromQL 의 비교 필터가 시리즈를 떨어뜨림 → predicate 해제 → "prom condition cleared" 와 함께 SystemResolveIncident 발화. 이것이 별도 "resolve" evaluator 없이 알람이 회복되는 방법.
Dedupe 키 형태: pipeline:<rule_key>:<sorted-label-set> — provenance 레이블 (__name__, ongrid_source) 은 제거되어 같은 알람이 임베디드와 클라우드 컬렉터 모두에서 보고되어도 두 incident 가 아닌 하나로 dedupe (labelSetKey).
채널 팬아웃
Incident 발화 시 Notifier.MaybeNotify 경로가 ChannelResolver 를 참고:
- 규칙별 고정 —
rule.notify_channel_ids_json이 비어 있지 않으면 그 채널 id 만 매칭 (그리고 활성화된 것만). - 그 외에는 모든 활성
notification_channels행이match_severity_min과match_scope_types로 필터링. - 아무것도 매칭되지 않으면 resolver 는
DefaultChannels에서 시드된 합성 채널 리스트로 폴백하여 알림이 사라지지 않게 함.
router.go 참고.
억제
두 내장 억제 규칙 (inhibit.go), 시끄러운 기본 케이스 커버:
edge_offline:edge_X가 모든host:X:*를 억제 — edge 가 도달 불가 시 그 위의 모든 호스트 범위 알람이 억제.pipeline:prom_ingest_fail이pipeline:scrape_down:*를 억제 — Prometheus 자체가 인제스트할 수 없을 때 모든 "target down" 알람은 노이즈.
미래의 inhibition_rules 테이블이 이를 관리자 정의 그룹으로 확장.
쿨다운 + 댐핑
NotifyOpts.Cooldown (기본 10 분) 이 같은 dedupe_key 에 대한 재알림을 제한. 댐핑 필터는 Usecase.MaybeNotify 내부에 위치하여 채널 resolver 와 inhibitor 는 매 발화에서 여전히 실행 — 실제 Notifier.Send 만 건너뜀.