eval
This commit is contained in:
1 parent
f5015581a4
commit
e046bd86d1
18 files changed
+3020
-46
No files matched your search
@@ -0,0 +1,144 @@
|
||||
// laya-eval runs alert scenarios through laya and compares its shadow verdict
|
||||
// with what a person judged the right response to be, so the model can be
|
||||
// assessed before it is ever allowed to act.
|
||||
//
|
||||
// go run ./cmd/laya-eval # built-in scenarios
|
||||
// go run ./cmd/laya-eval -file alerts.json # a GET /api/alerts dump (no expectations)
|
||||
// go run ./cmd/laya-eval -send # also deliver each as a TEST alert
|
||||
//
|
||||
// -send uses the channels configured by OTEL_ALERT_* (see alerts.ConfigFromEnv).
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-otel/internal/alerts"
|
||||
)
|
||||
|
||||
//go:embed scenarios.json
|
||||
var builtin []byte
|
||||
|
||||
// Scenario is one alert and the response a person would pick for it.
|
||||
type Scenario struct {
|
||||
Name string `json:"name"`
|
||||
Expect *Expectation `json:"expect,omitempty"`
|
||||
Alert alerts.Alert `json:"alert"`
|
||||
}
|
||||
|
||||
type Expectation struct {
|
||||
Action string `json:"action"`
|
||||
Human bool `json:"human"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
url := flag.String("url", envOr("OTEL_ALERT_LAYA_URL", "https://laya.k6n.net"), "laya base URL")
|
||||
file := flag.String("file", "", "scenarios JSON, or a GET /api/alerts response (default: built-in scenarios)")
|
||||
send := flag.Bool("send", false, "deliver each scenario, with its verdict, as a TEST alert to the configured channels")
|
||||
flag.Parse()
|
||||
|
||||
raw := builtin
|
||||
if *file != "" {
|
||||
var err error
|
||||
if raw, err = os.ReadFile(*file); err != nil {
|
||||
fail(err)
|
||||
}
|
||||
}
|
||||
scenarios, err := parse(raw)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
var notifiers []alerts.Notifier
|
||||
if *send {
|
||||
cfg := alerts.ConfigFromEnv()
|
||||
if notifiers = cfg.Notifiers(); len(notifiers) == 0 {
|
||||
fail(fmt.Errorf("-send: no channel configured (OTEL_ALERT_TELEGRAM_TOKEN + OTEL_ALERT_TELEGRAM_CHAT_ID, OTEL_ALERT_WEBHOOK_URL or OTEL_ALERT_AOD_URL)"))
|
||||
}
|
||||
}
|
||||
|
||||
laya := &alerts.Laya{URL: *url}
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "SCENARIO\tLAYA ACTION\tHUMAN\tURGENCY\tEXPECTED\tMATCH")
|
||||
judged, actionOK, humanOK := 0, 0, 0
|
||||
for _, s := range scenarios {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
v, err := laya.Classify(ctx, s.Alert)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintf(tw, "%s\terror: %v\t\t\t\t\n", s.Name, err)
|
||||
continue
|
||||
}
|
||||
exp, match := "-", "-"
|
||||
if s.Expect != nil {
|
||||
judged++
|
||||
a, h := v.Action == s.Expect.Action, (v.Human >= 0.5) == s.Expect.Human
|
||||
if a {
|
||||
actionOK++
|
||||
}
|
||||
if h {
|
||||
humanOK++
|
||||
}
|
||||
exp = fmt.Sprintf("%s, human=%v", s.Expect.Action, s.Expect.Human)
|
||||
match = fmt.Sprintf("action %s, human %s", mark(a), mark(h))
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s %.0f%%\t%.0f%%\t%s %.0f%%\t%s\t%s\n", s.Name, v.Action, v.ActionConfidence*100, v.Human*100, v.Urgency, v.UrgencyConf*100, exp, match)
|
||||
|
||||
if *send {
|
||||
a := s.Alert
|
||||
a.Test, a.Laya = true, v
|
||||
for _, n := range notifiers {
|
||||
sctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
if err := n.Send(sctx, a); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: %s: %v\n", s.Name, n.Name(), err)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
tw.Flush()
|
||||
if judged > 0 {
|
||||
fmt.Printf("\nagreement over %d judged scenarios: action %d/%d, needs-human %d/%d\n", judged, actionOK, judged, humanOK, judged)
|
||||
}
|
||||
}
|
||||
|
||||
// parse accepts a scenario list or a raw GET /api/alerts response.
|
||||
func parse(raw []byte) ([]Scenario, error) {
|
||||
var list []Scenario
|
||||
if err := json.Unmarshal(raw, &list); err == nil {
|
||||
return list, nil
|
||||
}
|
||||
var st alerts.Status
|
||||
if err := json.Unmarshal(raw, &st); err != nil {
|
||||
return nil, fmt.Errorf("neither a scenario list nor an /api/alerts response: %w", err)
|
||||
}
|
||||
for _, a := range st.Alerts {
|
||||
list = append(list, Scenario{Name: a.ID + " " + a.Spike.Service, Alert: a})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func mark(ok bool) string {
|
||||
if ok {
|
||||
return "ok"
|
||||
}
|
||||
return "MISS"
|
||||
}
|
||||
|
||||
func envOr(k, def string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func fail(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user