Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
107 lines
2.4 KiB
Go
107 lines
2.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// vmPoint is one sample of a Prometheus-style response. OK is false for NaN and
|
|
// infinities, which JSON cannot carry and a graph must show as a gap.
|
|
type vmPoint struct {
|
|
TS int64 // unix seconds
|
|
V float64
|
|
OK bool
|
|
}
|
|
|
|
func (p *vmPoint) UnmarshalJSON(b []byte) error {
|
|
var raw [2]json.RawMessage
|
|
if err := json.Unmarshal(b, &raw); err != nil {
|
|
return err
|
|
}
|
|
var ts float64
|
|
if err := json.Unmarshal(raw[0], &ts); err != nil {
|
|
return err
|
|
}
|
|
var str string
|
|
if err := json.Unmarshal(raw[1], &str); err != nil {
|
|
return err
|
|
}
|
|
v, err := strconv.ParseFloat(str, 64)
|
|
p.TS = int64(math.Round(ts))
|
|
p.V, p.OK = v, err == nil && !math.IsNaN(v) && !math.IsInf(v, 0)
|
|
return nil
|
|
}
|
|
|
|
// vmSeries is one result row: its labels and either one sample (instant query)
|
|
// or many (range query).
|
|
type vmSeries struct {
|
|
Metric map[string]string `json:"metric"`
|
|
Value *vmPoint `json:"value"`
|
|
Values []vmPoint `json:"values"`
|
|
}
|
|
|
|
var errVMStatus = errors.New("metric store reported an error")
|
|
|
|
// parseVM decodes the result rows of an instant or range response.
|
|
func parseVM(body []byte) ([]vmSeries, error) {
|
|
var r struct {
|
|
Status string `json:"status"`
|
|
Error string `json:"error"`
|
|
Data struct {
|
|
Result []vmSeries `json:"result"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &r); err != nil {
|
|
return nil, fmt.Errorf("decode metric store response: %w", err)
|
|
}
|
|
if r.Status != "success" {
|
|
return nil, fmt.Errorf("%w: %s", errVMStatus, r.Error)
|
|
}
|
|
return r.Data.Result, nil
|
|
}
|
|
|
|
// quote renders s as a PromQL string literal.
|
|
func quote(s string) string {
|
|
var b strings.Builder
|
|
b.WriteByte('"')
|
|
for _, r := range s {
|
|
switch r {
|
|
case '"', '\\':
|
|
b.WriteByte('\\')
|
|
b.WriteRune(r)
|
|
case '\n':
|
|
b.WriteString(`\n`)
|
|
default:
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
b.WriteByte('"')
|
|
return b.String()
|
|
}
|
|
|
|
// regexAlt joins literal values into an anchored-by-VM regex alternation.
|
|
func regexAlt(vals []string) string {
|
|
esc := make([]string, len(vals))
|
|
for i, v := range vals {
|
|
esc[i] = quoteMeta(v)
|
|
}
|
|
return strings.Join(esc, "|")
|
|
}
|
|
|
|
// quoteMeta escapes regex metacharacters; slugs and ULIDs rarely need it, names might.
|
|
func quoteMeta(s string) string {
|
|
const meta = `\.+*?()|[]{}^$`
|
|
var b strings.Builder
|
|
for _, r := range s {
|
|
if strings.ContainsRune(meta, r) {
|
|
b.WriteByte('\\')
|
|
}
|
|
b.WriteRune(r)
|
|
}
|
|
return b.String()
|
|
}
|