Files
nmon/internal/api/rates.go
Vibe Coder f31fb3712a
Some checks failed
ci / lint (push) Successful in 4m19s
ci / openapi (push) Successful in 29s
ci / postgres (push) Successful in 4m55s
ci / test (push) Failing after 19m48s
Add settings forms, first-install wizard, webhook presets, help and poll tools
Make nmon easier to install and run without editing YAML by hand:

- Settings: point-and-click forms over config.yaml (GET/PUT
  /api/v1/settings/form); the YAML editor moves to /settings/yaml.
- First install: with no sign-in configured nmon starts in install
  mode, serves only the setup wizard (sign-in, basics, devices,
  alerting) and closes it for good once sign-in is saved. A
  self-signed certificate is generated when no TLS source is given.
  poller.interval becomes a config setting.
- Reverse proxies: opt-in --http-listen plain-HTTP listener beside
  HTTPS; sign-in stays required.
- Webhooks: Generic, Teams Adaptive Card, Slack and Discord presets,
  template preview and test send.
- Help: the operations, coverage and profile docs served at /help.
- Polling: per-device "Poll now", a dry-run test poll returning an
  SNMP trace, and time-limited in-memory SNMP debug recording, in the
  UI and as `nmon device poll`.
- Ansible: deploys into install mode when no sign-in is set, and no
  longer overwrites the config.yaml the wizard wrote.

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

131 lines
3.8 KiB
Go

package api
import (
"context"
"strings"
"time"
"github.com/v10networks/nmon/internal/store"
)
// liveWindow is the lookback of the "live" rates on device and search pages: two default poll intervals, so a
// device polled at the default always has two samples inside it (never under the five minutes it used to be).
func (s *server) liveWindow() string { return secs(rateWindowFor(s.pollEvery())) }
// ifRates holds the five-minute rates of one interface by the counter they came
// from; pick chooses between the 64-bit and 32-bit counters.
type ifRates struct {
hcIn, hcOut, in32, out32, inErr, outErr *float64
}
// rateKinds maps the k label of the batched query to the metric and whether it is a bits/s rate.
var rateKinds = []struct {
k, metric string
bits bool
}{
{"in_hc", "ifHCInOctets", true}, {"out_hc", "ifHCOutOctets", true},
{"in_32", "ifInOctets", true}, {"out_32", "ifOutOctets", true},
{"in_err", "ifInErrors", false}, {"out_err", "ifOutErrors", false},
}
// ratesExpr is one MetricsQL expression yielding every live rate of the interfaces
// matched by match (label matchers), each series tagged with k. When uptime is set
// it also yields the device's last sysUpTime under k="uptime".
func ratesExpr(match string, uptime bool, window string) string {
parts := make([]string, 0, len(rateKinds)+1)
for _, k := range rateKinds {
scale := ""
if k.bits {
scale = "*8"
}
parts = append(parts, `label_set(rate(`+k.metric+`{`+match+`}[`+window+`])`+scale+`, "k", `+quote(k.k)+`)`)
}
if uptime {
parts = append(parts, `label_set(last_over_time(sysUpTime{`+match+`}[`+window+`]), "k", "uptime")`)
}
return strings.Join(parts, " or ")
}
// liveRates runs the batched query. A store that is down, busy or slow yields
// no rates rather than failing the page: the fields are null, as for unknown.
func (s *server) liveRates(ctx context.Context, match string, uptime bool) (map[string]*ifRates, *float64) {
release, err := s.admit(ctx, false)
if err != nil {
return nil, nil
}
defer release()
qctx, cancel := context.WithTimeout(ctx, QueryTimeout)
defer cancel()
body, err := s.Querier.Query(qctx, "hot", ratesExpr(match, uptime, s.liveWindow()), s.Clock.Now())
if err != nil {
s.Logger.Warn("live rates unavailable", "err", err)
return nil, nil
}
rows, err := parseVM(body)
if err != nil {
s.Logger.Warn("live rates unreadable", "err", err)
return nil, nil
}
return indexRates(rows)
}
// indexRates sorts result rows into per-interface rates and the device uptime.
func indexRates(rows []vmSeries) (map[string]*ifRates, *float64) {
out := map[string]*ifRates{}
var uptime *float64
for _, r := range rows {
if r.Value == nil || !r.Value.OK {
continue
}
v := r.Value.V
if r.Metric["k"] == "uptime" {
uptime = &v
continue
}
id := r.Metric["iface"]
x := out[id]
if x == nil {
x = &ifRates{}
out[id] = x
}
switch r.Metric["k"] {
case "in_hc":
x.hcIn = &v
case "out_hc":
x.hcOut = &v
case "in_32":
x.in32 = &v
case "out_32":
x.out32 = &v
case "in_err":
x.inErr = &v
case "out_err":
x.outErr = &v
}
}
return out, uptime
}
// bps returns the in and out rates from the counters the interface really has;
// an unreliable interface has none.
func (x *ifRates) bps(i *store.Interface) (in, out *float64) {
if x == nil || i.Unreliable {
return nil, nil
}
if i.HasHCCounters {
return x.hcIn, x.hcOut
}
return x.in32, x.out32
}
// errs returns the error rates, none for an unreliable interface.
func (x *ifRates) errs(i *store.Interface) (in, out *float64) {
if x == nil || i.Unreliable {
return nil, nil
}
return x.inErr, x.outErr
}
// pollEvery is the default poll interval right now: the live cadence when the app gave one, else the fixed option.
func (s *server) pollEvery() time.Duration { return s.Cadence.Or(s.PollInterval) }