Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
164 lines
4.5 KiB
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
|
|
}
|