Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
99 lines
2.9 KiB
Go
99 lines
2.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
|
|
"github.com/v10networks/nmon/internal/metrics"
|
|
"github.com/v10networks/nmon/internal/store"
|
|
)
|
|
|
|
// maxPushedNames bounds the pushed metric names listed on a device.
|
|
const maxPushedNames = 200
|
|
|
|
// PushedSeries summarises what a device's push senders write.
|
|
type PushedSeries struct {
|
|
Count int `json:"count" doc:"Distinct series pushed in the last hour."`
|
|
Names []string `json:"names" doc:"Their metric names, sorted, at most 200."`
|
|
}
|
|
|
|
// fillM5 adds what M5 attached to a device: its host or guests, the checks that watch it and the series pushed for it.
|
|
func (s *server) fillM5(ctx context.Context, d *store.Device, v *DeviceView) error {
|
|
v.Guests = []DeviceRef{}
|
|
v.Checks = []CheckView{}
|
|
v.PushedSeries = PushedSeries{Names: []string{}}
|
|
hosts, err := s.Store.ListParents(ctx, d.ID, store.RelHostedOn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(hosts) > 0 {
|
|
if p, err := s.Store.GetDeviceByID(ctx, hosts[0].ParentID); err == nil {
|
|
v.HostedOn = &DeviceRef{Slug: p.Slug, Name: p.Name}
|
|
}
|
|
}
|
|
guests, err := s.Store.ListChildren(ctx, d.ID, store.RelHostedOn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, g := range guests {
|
|
if c, err := s.Store.GetDeviceByID(ctx, g.DeviceID); err == nil && c.RetiredAt == nil {
|
|
v.Guests = append(v.Guests, DeviceRef{Slug: c.Slug, Name: c.Name})
|
|
}
|
|
}
|
|
sort.Slice(v.Guests, func(i, j int) bool { return v.Guests[i].Name < v.Guests[j].Name })
|
|
if err := s.fillChecks(ctx, d, v); err != nil {
|
|
return err
|
|
}
|
|
v.PushedSeries = s.pushedSeries(ctx, d.Slug)
|
|
return nil
|
|
}
|
|
|
|
func (s *server) fillChecks(ctx context.Context, d *store.Device, v *DeviceView) error {
|
|
rows, err := s.Store.ListChecks(ctx, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res := s.checkResults()
|
|
for i := range rows {
|
|
if rows[i].DeviceID != nil && *rows[i].DeviceID == d.ID {
|
|
v.Checks = append(v.Checks, s.checkView(ctx, &rows[i], res))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pushedSeries lists the series the device's push senders wrote in the last hour. A store that is down or slow
|
|
// leaves it empty, as for the live rates.
|
|
func (s *server) pushedSeries(ctx context.Context, slug string) PushedSeries {
|
|
out := PushedSeries{Names: []string{}}
|
|
release, err := s.admit(ctx, false)
|
|
if err != nil {
|
|
return out
|
|
}
|
|
defer release()
|
|
qctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
|
defer cancel()
|
|
expr := `count by (__name__) (last_over_time({device=` + quote(slug) + `,` + metrics.LabelPush + `="1"}[1h]))`
|
|
body, err := s.Querier.Query(qctx, "hot", expr, s.Clock.Now())
|
|
if err != nil {
|
|
s.Logger.Warn("pushed series unavailable", "device", slug, "err", err)
|
|
return out
|
|
}
|
|
rows, err := parseVM(body)
|
|
if err != nil {
|
|
return out
|
|
}
|
|
for _, r := range rows {
|
|
if r.Value == nil || !r.Value.OK || r.Metric["__name__"] == "" {
|
|
continue
|
|
}
|
|
out.Count += int(r.Value.V)
|
|
out.Names = append(out.Names, r.Metric["__name__"])
|
|
}
|
|
sort.Strings(out.Names)
|
|
if len(out.Names) > maxPushedNames {
|
|
out.Names = out.Names[:maxPushedNames]
|
|
}
|
|
return out
|
|
}
|