Files
nmon/internal/api/vmjson.go
jleung f85c3ef781
Some checks failed
ci / lint (push) Has been cancelled
ci / test (ubuntu-24.04-arm) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / postgres (push) Has been cancelled
ci / openapi (push) Has been cancelled
M3: Go API per UI contract, embedded UI, openapi compat check, nmon-devtools, live gate + screenshots; M4 WP1 reachability and dependency suppression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 20:34:14 +00:00

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()
}