package main import ( "context" "encoding/json" "fmt" "io" "math" "net/http" "net/url" "strconv" "time" ) // Mimir instant query (Prometheus HTTP API under /api/v1/query; single tenant, no X-Scope-OrgID). // Returns NaN when the result vector is empty. func (a *App) mimirScalar(ctx context.Context, expr string) (float64, error) { u := a.cfg.MimirURL + "/api/v1/query?query=" + url.QueryEscape(expr) req, _ := http.NewRequestWithContext(ctx, "GET", u, nil) resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req) if err != nil { return math.NaN(), err } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { return math.NaN(), fmt.Errorf("mimir %s: %s", resp.Status, truncate(string(b), 300)) } var r struct { Status string `json:"status"` Data struct { ResultType string `json:"resultType"` Result []struct { Value []any `json:"value"` } `json:"result"` } `json:"data"` } if err := json.Unmarshal(b, &r); err != nil { return math.NaN(), err } if r.Status != "success" || len(r.Data.Result) == 0 || len(r.Data.Result[0].Value) < 2 { return math.NaN(), nil } s, _ := r.Data.Result[0].Value[1].(string) return strconv.ParseFloat(s, 64) } // SLOState is one evaluation of experiment design 4.2 (probe 12 consecutive successes = min over 3m; p95 and 5xx ratio over 5m). // Values are nil when the series is absent (JSON has no NaN). type SLOState struct { At time.Time `json:"at"` ProbeMin3m *float64 `json:"probe_min_3m"` P95 *float64 `json:"p95_s"` ErrorRatio *float64 `json:"error_ratio"` ProbeOK bool `json:"probe_ok"` LatencyOK bool `json:"latency_ok"` ErrorsOK bool `json:"errors_ok"` Recovered bool `json:"recovered"` MissingData []string `json:"missing_data,omitempty"` QueryErrors []string `json:"query_errors,omitempty"` } const ( qProbeMin3m = `min_over_time(probe_success{job="blackbox",target="alb"}[3m])` qP95 = `histogram_quantile(0.95, sum by (le) (rate(demo_http_request_duration_seconds_bucket{route=~"orders|price"}[5m])))` qErrRatio = `sum(rate(demo_http_requests_total{route=~"orders|price",status=~"5.."}[5m])) / sum(rate(demo_http_requests_total{route=~"orders|price"}[5m]))` ) func (a *App) evaluateSLO(ctx context.Context) SLOState { st := SLOState{At: time.Now().UTC()} get := func(name, expr string) *float64 { v, err := a.mimirScalar(ctx, expr) if err != nil { st.QueryErrors = append(st.QueryErrors, name+": "+err.Error()) return nil } if math.IsNaN(v) { st.MissingData = append(st.MissingData, name) return nil } return &v } st.ProbeMin3m = get("probe", qProbeMin3m) st.ProbeOK = st.ProbeMin3m != nil && *st.ProbeMin3m >= 1 // the probe series must exist: no data is not recovery st.P95 = get("p95", qP95) st.LatencyOK = st.P95 == nil || *st.P95 < 0.5 // no traffic in the window (no k6) counts as within SLO st.ErrorRatio = get("errors", qErrRatio) st.ErrorsOK = st.ErrorRatio == nil || *st.ErrorRatio < 0.01 st.Recovered = st.ProbeOK && st.LatencyOK && st.ErrorsOK && len(st.QueryErrors) == 0 return st }