Files
nmon/internal/api/graph_counters_test.go
Vibe Coder 26fdcdc124
Some checks failed
ci / lint (push) Successful in 1m25s
ci / openapi (push) Successful in 29s
ci / postgres (push) Successful in 3m32s
ci / test (push) Failing after 17m53s
Match LibreNMS port graphs: totals, max, aggregate 95th, compare, utilisation
Operators coming from LibreNMS expect the same figures under a port
graph. Add them:

- Totals: bytes transferred in and out over the selected range
  (GraphResponse.totals), integrated from the average rate across the
  stitched raw and hourly tiers without double counting; partial
  coverage is flagged.
- Summaries read Now / Avg / Max / 95th / Total per direction, plus an
  aggregate (in+out) 95th (p95.aggregate_bps, hot store only).
- Utilisation as a percentage of port speed, and a "% of speed" axis.
- "Compare previous period" overlay on the graph view and expanded row.
- Errors and discards graphs (GET /api/v1/graph/counters), hot-store
  range only since they have no rollups. Packet and broadcast/multicast
  graphs stay disabled: those counters are not polled (DEVIATIONS 56).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
2026-10-25 21:02:32 +00:00

50 lines
1.9 KiB
Go

package api_test
import (
"net/url"
"strings"
"testing"
"time"
"github.com/v10networks/nmon/internal/api"
)
func countersURL(id, kind string, from, to time.Time) string {
return "/api/v1/graph/counters?iface=" + id + "&kind=" + kind + "&from=" + url.QueryEscape(iso(from)) + "&to=" + url.QueryEscape(iso(to))
}
func TestGraphCountersQueryTheErrorCountersAsRates(t *testing.T) {
e, i := newGraphEnv(t, &graphVM{hotVal: rising})
var g api.CounterGraphResponse
rec := e.getJSON(t, countersURL(i.ID, "errors", t0.Add(-time.Hour), t0), &g)
assertNoLeaks(t, "counters", rec.Body.Bytes())
if g.Kind != "errors" || g.Unit != "errors/s" || g.StepSeconds != 60 || g.Clamped || g.Iface.ID != i.ID {
t.Fatalf("response: %+v", g)
}
if len(g.Series.In) != 61 || len(g.Series.Out) != 61 || g.Series.In[60].TS != t0.Unix() {
t.Fatalf("series: %d in, %d out", len(g.Series.In), len(g.Series.Out))
}
qs := e.q.queries()
if len(qs) != 2 || qs[0].store != "hot" || qs[0].expr != `rate(ifInErrors{iface="`+i.ID+`"}[600s])` ||
qs[1].expr != `rate(ifOutErrors{iface="`+i.ID+`"}[600s])` {
t.Errorf("queries: %+v", qs)
}
}
func TestGraphCountersDiscardsAndHotOnlyClamp(t *testing.T) {
e, i := newGraphEnv(t, &graphVM{hotVal: rising})
var g api.CounterGraphResponse
e.getJSON(t, countersURL(i.ID, "discards", t0.Add(-90*24*time.Hour), t0), &g)
if g.Kind != "discards" || !g.Clamped || !g.From.After(t0.Add(-90*24*time.Hour)) {
t.Fatalf("a range older than hot retention must be clamped: %+v", g)
}
for _, q := range e.q.queries() {
if q.store != "hot" || !strings.Contains(q.expr, "Discards") {
t.Errorf("query: %+v", q)
}
}
isProblem(t, e.do("GET", countersURL(i.ID, "packets", t0.Add(-time.Hour), t0), ""), 422)
isProblem(t, e.do("GET", countersURL(i.ID, "errors", t0, t0.Add(-time.Hour)), ""), 422)
isProblem(t, e.do("GET", countersURL("01ARZ3NDEKTSV4RRFFQ69G5FAV", "errors", t0.Add(-time.Hour), t0), ""), 404)
}