Files
nmon/cmd/nmon-devtools/gatedump.go

164 lines
4.5 KiB
Go

package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// gateDumpCmd prints what the M4 gate asserts on, one line per item, so the shell
// script can count and match lines instead of parsing JSON.
//
// gate-dump sink --file F what the webhook sink received, in arrival order
// gate-dump alerts --api URL every alert, newest first
// gate-dump devices --api URL every device's reachability
func gateDumpCmd(ctx context.Context, args []string) error {
if len(args) == 0 {
return fmt.Errorf("gate-dump sink|alerts|devices")
}
fs := devFlagSet("gate-dump " + args[0])
file := fs.String("file", "", "sink file (sink)")
api := fs.String("api", "", "nmon API base URL (alerts, devices)")
mapFile := fs.String("map", "", "slug map written by topology-add; slugs are printed as topology names")
if err := fs.Parse(args[1:]); err != nil {
return flagErr(err)
}
names, err := readSlugMap(*mapFile)
if err != nil {
return err
}
switch args[0] {
case "sink":
return dumpSink(*file, names)
case "alerts":
return dumpAlerts(ctx, *api, names)
case "devices":
return dumpDevices(ctx, *api, names)
}
return fmt.Errorf("gate-dump: unknown %q", args[0])
}
func orDash(s []string) string {
if len(s) == 0 {
return "-"
}
return strings.Join(s, ",")
}
// dumpSink prints "rule state device parents=a,b children=N incident=ID" per request received.
func dumpSink(file string, names slugNames) error {
f, err := os.Open(file)
if err != nil {
return err
}
defer func() { _ = f.Close() }() // read only
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for sc.Scan() {
var l sinkLine
var n struct {
Rule, State string
IncidentID string `json:"incident_id"`
Device struct{ Slug string }
Parents []string
SuppressedChildren int `json:"suppressed_children"`
}
if json.Unmarshal(sc.Bytes(), &l) != nil || json.Unmarshal(l.Body, &n) != nil {
fmt.Println("unparsable - - parents=- children=0 incident=-")
continue
}
fmt.Printf("%s %s %s parents=%s children=%d incident=%s\n", n.Rule, n.State, names.one(n.Device.Slug), orDash(names.of(n.Parents)),
n.SuppressedChildren, n.IncidentID)
}
return sc.Err()
}
func getJSON(ctx context.Context, api, path string, out any) error {
hc := &http.Client{Timeout: 30 * time.Second}
return httpJSON(ctx, hc, http.MethodGet, api+path, nil, out)
}
// dumpAlerts prints "id rule state device suppressed_by=a,b incident=ID acked=yes|no" per alert.
func dumpAlerts(ctx context.Context, api string, names slugNames) error {
var page struct {
Items []struct {
ID, Rule, State string
Device struct{ Slug string }
SuppressedBy []string `json:"suppressed_by"`
IncidentID string `json:"incident_id"`
Ack *struct{} `json:"ack"`
} `json:"items"`
}
if err := getJSON(ctx, api, "/api/v1/alerts?per_page=1000", &page); err != nil {
return err
}
for _, a := range page.Items {
acked := "no"
if a.Ack != nil {
acked = "yes"
}
fmt.Printf("%s %s %s %s suppressed_by=%s incident=%s acked=%s\n", a.ID, a.Rule, a.State, names.one(a.Device.Slug),
orDash(names.of(a.SuppressedBy)), orDash([]string{a.IncidentID}), acked)
}
return nil
}
// dumpDevices prints "slug reachability poll_state warnings=a,b" per device.
func dumpDevices(ctx context.Context, api string, names slugNames) error {
var page struct {
Items []struct {
Slug, Reachability string
PollState string `json:"poll_state"`
Warnings []string `json:"warnings"`
} `json:"items"`
}
if err := getJSON(ctx, api, "/api/v1/devices?per_page=1000", &page); err != nil {
return err
}
for _, d := range page.Items {
fmt.Printf("%s %s %s warnings=%s\n", url.PathEscape(names.one(d.Slug)), d.Reachability, d.PollState, orDash(d.Warnings))
}
return nil
}
// slugNames maps nmon slugs back to the names in the topology file.
type slugNames map[string]string
func readSlugMap(path string) (slugNames, error) {
m := slugNames{}
if path == "" {
return m, nil
}
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
for _, l := range strings.Split(string(b), "\n") {
if f := strings.Fields(l); len(f) == 2 {
m[f[0]] = f[1]
}
}
return m, nil
}
func (m slugNames) one(slug string) string {
if n, ok := m[slug]; ok {
return n
}
return slug
}
func (m slugNames) of(slugs []string) []string {
out := make([]string, len(slugs))
for i, s := range slugs {
out[i] = m.one(s)
}
return out
}