Documentation
¶
Overview ¶
Package evaluator runs alert rules on a fixed interval, opens problems when their condition is breached, and resolves problems whose breach is no longer present. Built-in rules cover the typical APM signals (error rate, P99 latency, request-rate drops).
v0.9.609 — altyapı-ölümcül exception dedektörü.
Operatör isteği (prod trace'i, 2026-08-03): bir servis bağımlılığına ulaşamıyor, `java.net.UnknownHostException` alıyor, hedef bir Kubernetes servis adı. "Varsa çok kritik, hemen P1 üret."
Bu dedektör paylaşılan-bağımlılık dedektöründen ÜÇ noktada ayrılıyor ve üçü de aynı sebepten: bu bir OLAY değil DURUM.
patlama (shared_exception) ölümcül exception (bu dosya) ─────────────────────────── ──────────────────────────── çok servis ŞART (≥3) TEK servis yeter eşik: aynı anda N servis eşik: 1 oluşum kimlik zaman kovası taşır kimlik (tip, servis) — kova YOK kendiliğinden durabilir biri düzeltene kadar sürer
v0.9.550 — evaluator kalp atışı.
Operatör raporu: "worker modda çalışan evaluator gerçekten problem bulsun ve Problems sekmesinde göreyim, bazen sanki takıldığını hissediyorum."
"Hissediyorum" cümlesi teşhisin kendisiydi: evaluator'ın koştuğuna dair HİÇBİR kayıt yoktu. Son tik ne zaman atıldı, ne kadar sürdü, kaç kural işlendi, kaç problem açıldı — hiçbiri hiçbir yerde tutulmuyordu. Dolayısıyla "takıldı mı?" sorusu ne kanıtlanabilir ne çürütülebilirdi; operatörün elinde yalnızca Problems sayfasının sessizliği vardı, o da "sorun yok" ile "evaluator ölü" arasında ayrım yapmıyor. İkisi ekranda BİREBİR aynı görünüyor. En kötü gözlemlenebilirlik hatası budur: sistem sana yanlış bir şey söylemiyor, hiçbir şey söylemiyor ve sen bunu iyi haber sanıyorsun.
Neden Redis: evaluator YALNIZCA worker pod'unda koşar (main.go, mode.worker), /api/problems'i ise API pod'u sunar. Bellekteki bir sayaç API tarafından görülemez — dağıtık kurulumda asla, monolitikte tesadüfen. Yazma-okuma paylaşımlı bir yerden geçmek ZORUNDA. Redis zaten lider kilidi ve v0.8.354 stamp aynası için bağlı; bu yüzden yeni bağımlılık yok, aynı cache tutamacı (SetStampCache) kullanılıyor.
Neden ClickHouse değil: kalp atışı tik başına yazılır (60 sn) ve yalnız EN SON değeri anlamlıdır — geçmişi yok. Bu, TTL'li bir anahtarın tam tanımı; ReplacingMergeTree'ye tik başına satır yazıp FINAL ile okumak aynı bilgiyi çok daha pahalıya verirdi.
TTL bilerek kısa DEĞİL: bayat bir kalp atışı silinirse "hiç çalışmadı" ile "14 dakikadır takıldı" ayırt edilemez hale gelir — ki operatörün sorduğu tam olarak bu ayrım. Anahtar bu yüzden uzun yaşar ve TAZELİĞİ okuyan taraf yorumlar.
v0.9.572 — paylaşılan bağımlılık patlaması dedektörü.
Operatör raporu (prod, gece 03:04): on beşten fazla servis AYNI saniyede aynı `java.sql.SQLRecoverableException` ORA-18730 hatasını aldı. Coremetry bunları on beş ayrı exception grubu olarak gösterdi — her biri "şu servisin sorunu" gibi. Oysa bu paylaşılan bir bağımlılıkta (Oracle) TEK bir olaydı; ortak paydayı operatörün kafasında kurması gerekiyordu.
Bu dedektör onu Coremetry'nin kurmasını sağlıyor: aynı exception tipi, dar bir pencerede, çok sayıda ayrı serviste başlıyorsa tek bir Problem açılır. Tespit DETERMİNİSTİK — LLM yok. RCA paketinin [2] CORRELATE katmanı: model tespit etmez, tespit edileni anlatır.
v0.8.354 — HA audit 🟡#2: the ForSec sustain clock (breachSince) and the CooldownSec gate (lastResolved) lived ONLY in per-pod memory, so every leader failover and every rolling deploy (maxUnavailable:0 ⇒ leader moves on EVERY release) reset them: a breach 9 minutes into a 10-minute sustain window restarted from zero on the new leader (alert delayed another full ForSec), and a just-resolved problem could re-open straight past its cooldown (flap + duplicate page).
Design: write-through, read-fallback. The in-memory maps stay the hot path (unchanged semantics for a stable leader); every stamp WRITE mirrors to Redis, and an in-memory MISS (fresh leader) hydrates from Redis once before falling back to "no stamp". Redis errors, misses, and the Noop cache (SwitchableCache pre-reconnect, v0.8.344) all degrade to the pre-v0.8.354 in-memory-only behavior — stamp IO never blocks or fails a tick.
Lock discipline: breachMu guards ONLY the map access; all Redis IO happens outside the mutex (values are copied first).
v0.9.588 — sessiz-kaynak süpürmesinin süreklilik kapısı.
Operatör raporu: her rollout'ta ~6 problem "auto-resolved: source silent" damgasıyla kapanıyor ve hemen ardından yeniden açılıyor.
KÖK NEDEN — süpürme kendi kör noktasını okuyor. Kural şu:
updated_at 3 tikten eskiyse → kaynak susmuş demektir
Bu çıkarım, evaluator'ın O SÜRE BOYUNCA KOŞTUĞU varsayımına dayanıyor. Rollout'ta koşmuyor. Worker pod'u inip kalkarken hiçbir dedektör tik atamaz, dolayısıyla hiçbir problem tazelenemez ve yeni pod'un ilk tikinde HERKES bayat görünür. Süpürme de bunu "kaynak sustu" diye okur.
İki hal ekranda BİREBİR aynı görünüyor:
kaynak sustu → updated_at donuk evaluator koşmuyordu → updated_at donuk
Ayırt eden tek şey, delili TOPLAYANIN o sırada ayakta olup olmadığı — ve bunu yalnız evaluator bilebilir. Bu dosya o bilgiyi kaydediyor: süpürme, ancak KENDİ gözlemlediği bir sessizliğe dayanarak karar verir.
Zarar kozmetik değildi: kapanış problemin cooldown damgasını da siliyor (clearResolved), yani yeniden açılış TAZE bir problem olarak doğuyor — StartedAt sıfırlanıyor, yaş-bazlı eskalasyon baştan başlıyor ve /problems geçmişi her dağıtımda uydurma bir çözüldü→açıldı çifti kazanıyor.
Index ¶
Constants ¶
const HeartbeatKey = "coremetry:eval:heartbeat"
HeartbeatKey — tek anahtar; kalp atışının geçmişi yok, son değeri var. API pod'u bunu okur (internal/api/evaluator_health.go). Dışa açık: evaluator yazar, API okur, ikisi AYNI anahtarı bilmek zorunda ve string'i iki yerde ayrı yazmak sessizce ayrışabilirdi.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Evaluator ¶
type Evaluator struct {
// contains filtered or unexported fields
}
func New ¶
func New(store *chstore.Store, interval time.Duration, lock cache.Lock, notifier *notify.Notifier) *Evaluator
New takes a cache.Lock so multiple Coremetry replicas only run the evaluation loop once per tick, and a notifier so PROBLEM OPENED transitions fan out to email/slack/etc.
func (*Evaluator) SetLogs ¶ added in v0.5.242
SetLogs wires the log backend so the saved-search alert path (rules with LogQuery != "") can count matches via logstore. Called from main() once buildLogStore has resolved the backend — keeps the New() constructor lean + avoids reordering boot-time wiring around the evaluator.
func (*Evaluator) SetStampCache ¶ added in v0.8.354
SetStampCache wires the shared cache so sustain/cooldown stamps survive leader failover. Called from main() once, like SetLogs — keeps New()'s signature stable. nil (never wired) or a Noop inner keeps the evaluator on pure in-memory stamps.
func (*Evaluator) SetVersion ¶ added in v0.9.550
SetVersion — kalp atışına gömülecek binary kimliği. main()'den bir kez çağrılır (SetLogs / SetStampCache ile aynı desen); evaluator main'i import edemeyeceği için setter şart.
type Heartbeat ¶ added in v0.9.550
type Heartbeat struct {
// StartedAt / FinishedAt — unix ns. FinishedAt=0 iken tik HÂLÂ
// koşuyor demektir; bu ikisinin ayrı tutulmasının tek sebebi
// "uzun sürüyor" ile "bitti" arasındaki farkı okuyan tarafa
// gösterebilmek. Tek zaman damgası bu ayrımı yapamazdı.
StartedAt int64 `json:"startedAt"`
FinishedAt int64 `json:"finishedAt"`
DurationMS int64 `json:"durationMs"`
// Rules — bu tikte DEĞERLENDİRİLEN kural sayısı (devre dışı olanlar
// hariç). Sıfır ve hata yoksa: kural yok demektir, takılma değil.
Rules int `json:"rules"`
// Opened / Resolved — bu tikte açılan/kapanan problem sayısı.
// Sürekli 0 olması takılma DEĞİLDİR (sağlıklı sistemin normali);
// bu yüzden hiçbir yerde alarm koşulu olarak kullanılmaz, yalnız
// "evaluator iş yapıyor mu" sorusuna renk katar.
Opened int `json:"opened"`
Resolved int `json:"resolved"`
// Err — tikin BAŞARISIZ bittiği durumun mesajı (deadline dahil).
// Boş = tik temiz bitti.
Err string `json:"err,omitempty"`
// Leader — bu pod tiki gerçekten çalıştırdı mı. Lider olmayan pod
// kalp atışı YAZMAZ (aşağıya bkz.), ama alan yine de taşınır ki
// okuyan taraf kaydın anlamını tahmin etmek zorunda kalmasın.
Leader bool `json:"leader"`
// Version — kalp atışını yazan binary. Bir dağıtım sonrası eski
// pod'un kalp atışı hâlâ Redis'te durabilir; sürüm olmadan bunu
// "çalışıyor" sanmak mümkün.
Version string `json:"version,omitempty"`
// ContinuousSince (v0.9.588) — KESİNTİSİZ değerlendirmenin
// başladığı an, unix ns.
//
// Kalp atışında taşınmasının tek sebebi POD SINIRINI geçmek: bir
// rollout'ta yeni pod'un belleği boştur ama fleet'in gerçek
// kesintisi yalnız birkaç saniye olabilir. Bu alan olmasa yeni
// lider her seferinde sıfırdan sayardı ve sessiz-kaynak süpürmesi
// her dağıtımdan sonra gereksizce üç tik beklerdi.
//
// Ayrıca operatör için doğrudan okunabilir bir cevap: "bu evaluator
// ne zamandır kesintisiz koşuyor?"
ContinuousSince int64 `json:"continuousSince,omitempty"`
// IntervalMS — YAZANIN tik aralığı. Okuyan taraf "bu kalp atışı
// bayat mı" sorusunu buna göre cevaplar; aralığı okuyan tarafta
// sabit yazmak, evaluator'ın aralığı değiştiğinde sessizce yanlış
// eşiğe düşmek demekti. Tempoyu yazan bildirir.
IntervalMS int64 `json:"intervalMs"`
}
Heartbeat — bir evaluator tikinin sonucu.
Alan seçimi "operatör neye bakıp karar verir" sorusundan türedi: tik atıldı mı (FinishedAt), sağlıklı mı (DurationMS, Err), iş yaptı mı (Rules, Opened/Resolved), ve bu pod gerçekten lider mi (Leader).