Registry and Analytics: run, subnet, direction and protocol filters, successes-by-target chart

Registry (/registry):
- filters by run (slice by the address's cycle in that run), subnet
  (drop-down of configured subnets), direction (egress/ingress) and
  protocol (icmp, tcp, ssh, https, tls); status in scope is computed over
  the narrowed checks
- chart "successful checks per target (egress) / site (ingress)" when both
  direction and protocol are chosen; a row opens the list of addresses
  (dialog, CSV)
- API: direction/protocol parameters and run in GET /admin/registry,
  GET /admin/registry/breakdown and /breakdown/list
- subnet filter passes ids as one JSON parameter (SQLite variable limit)

Analytics (/analytics):
- subnet filter recomputes the whole page over the addresses of the run
  inside the subnet; only their checks are read; cache per run and subnet
- direction and protocol focus the page; with both set the registry chart
  is shown
- subnet parameter in GET /admin/analytics/runs/{id} and lists (JSON, CSV)

Docs: plans and summaries in docs/changes, README, API, USAGE.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-06 14:23:48 +03:00
1 parent 068c10ea1c
commit ded196ec8d
40 files changed
+2545 -188

No files matched your search

+25
View File
@@ -22,6 +22,9 @@ type Input struct {
Subnets []db.Subnet
SiteNames map[int]string // site index -> site id
Rechecked int // addresses with more than one cycle in the run
// Subnet narrows the report to the addresses of the run inside it, nested
// subnets included; the zero prefix means the whole run.
Subnet netip.Prefix
// Each feeds every check of the run's result cycles to fn.
Each func(fn func(db.RunCheck)) error
}
@@ -29,6 +32,7 @@ type Input struct {
// Report is the data of the analytics page for one run.
type Report struct {
Run RunInfo `json:"run"`
Scope *Scope `json:"scope,omitempty"` // set when the report is narrowed to a subnet
Summary Summary `json:"summary"`
Reasons []Reason `json:"reasons"`
Quality Quality `json:"quality"`
@@ -50,6 +54,14 @@ type RunInfo struct {
Rechecked int `json:"rechecked"`
}
// Scope says which part of the run a narrowed report covers: Addresses of
// Summary are those inside Subnet, RunAddresses all of the run (cancelled
// ones excluded in both).
type Scope struct {
Subnet string `json:"subnet"`
RunAddresses int `json:"run_addresses"`
}
type Summary struct {
Addresses int `json:"addresses"`
Pass int `json:"pass"`
@@ -176,7 +188,17 @@ func Compute(in Input) (*Analysis, error) {
byReg := make(map[int64]*addr, len(in.Results))
var addrs []*addr
subnetOf := newSubnetMatcher(in.Subnets)
runAddrs := 0
for _, r := range in.Results {
if r.Verdict != db.ResultCancelled {
runAddrs++
}
if in.Subnet.IsValid() {
// checks of an address outside the subnet find no entry in byReg below
if ip, err := netip.ParseAddr(r.IPAddress); err != nil || !in.Subnet.Contains(ip) {
continue
}
}
a := &addr{res: r, subnet: subnetOf(r.IPAddress), failedTargets: map[string]bool{}}
a.ssh.errs = map[string]bool{}
byReg[r.RegistryID] = a
@@ -279,6 +301,9 @@ func Compute(in Input) (*Analysis, error) {
rep := Report{Matrix: map[string][]MatrixRow{}}
rep.Run = RunInfo{ID: in.Run.ID, Kind: in.Run.Kind, State: in.Run.State, StartedAt: in.Run.StartedAt,
FinalizedAt: in.Run.FinalizedAt, Rechecked: in.Rechecked}
if in.Subnet.IsValid() {
rep.Scope = &Scope{Subnet: in.Subnet.Masked().String(), RunAddresses: runAddrs}
}
if in.Run.FinalizedAt != nil {
rep.Run.DurationSec = int(in.Run.FinalizedAt.Sub(in.Run.StartedAt).Seconds())
}
+58
View File
@@ -1,6 +1,7 @@
package analytics
import (
"net/netip"
"reflect"
"testing"
"time"
@@ -30,12 +31,18 @@ func (f *fixture) check(reg int64, source, typ, target string, ok bool, validato
}
func (f *fixture) compute(t *testing.T, subnets []db.Subnet) *Analysis {
t.Helper()
return f.computeIn(t, subnets, netip.Prefix{})
}
func (f *fixture) computeIn(t *testing.T, subnets []db.Subnet, subnet netip.Prefix) *Analysis {
t.Helper()
end := t0.Add(10 * time.Minute)
an, err := Compute(Input{
Run: db.CheckRun{ID: 7, Kind: db.RunManual, State: db.RunFinalized, StartedAt: t0, FinalizedAt: &end},
Results: f.results,
Subnets: subnets,
Subnet: subnet,
SiteNames: map[int]string{1: "rxmsk", 2: "rxyc"},
Each: func(fn func(db.RunCheck)) error {
for _, c := range f.checks {
@@ -153,6 +160,57 @@ func TestComputeCountsFactsPerAddress(t *testing.T) {
}
}
// TestComputeNarrowedToSubnet: with Input.Subnet every block and every list is
// built from the addresses inside it (nested subnets included) and nothing else.
func TestComputeNarrowedToSubnet(t *testing.T) {
f := &fixture{}
f.addr(1, "10.0.0.1", db.ResultPass, 3)
f.addr(2, "10.0.0.2", db.ResultPartial, 3)
f.addr(3, "10.0.1.1", db.ResultPartial, 3)
f.addr(4, "10.0.1.200", db.ResultCancelled, 3)
for reg := int64(1); reg <= 4; reg++ {
f.check(reg, eg, "https", "https://a.test", reg != 2, "vkiplab-v1", "", false)
f.check(reg, s1, "icmp", "ip", true, "vkiplab-v1", "", false)
f.check(reg, s1, "ssh", "ip", reg != 3, "vkiplab-v1", "dial tcp: i/o timeout", false)
}
subnets := []db.Subnet{{CIDR: "10.0.0.0/24"}, {CIDR: "10.0.1.0/24"}}
all := f.compute(t, subnets).Report
if all.Scope != nil || all.Summary.Addresses != 3 || len(all.Subnets) != 2 {
t.Fatalf("without a subnet the whole run is reported: %+v %+v", all.Scope, all.Summary)
}
// 10.0.0.0/23 holds both configured subnets: the same numbers as the whole run.
if wide := f.computeIn(t, subnets, netip.MustParsePrefix("10.0.0.0/23")).Report; wide.Summary != all.Summary || len(wide.Subnets) != 2 {
t.Errorf("a wider subnet changes nothing: %+v", wide.Summary)
}
an := f.computeIn(t, subnets, netip.MustParsePrefix("10.0.1.0/24"))
r := an.Report
if r.Scope == nil || r.Scope.Subnet != "10.0.1.0/24" || r.Scope.RunAddresses != 3 {
t.Errorf("scope = %+v", r.Scope)
}
if r.Summary.Addresses != 1 || r.Summary.Partial != 1 || r.Summary.Pass != 0 || r.Summary.Cancelled != 1 || r.Summary.IngressSSHAny != 1 || r.Summary.EgressHTTPSAny != 0 {
t.Errorf("summary = %+v", r.Summary)
}
if len(r.Subnets) != 1 || r.Subnets[0].CIDR != "10.0.1.0/24" || r.Subnets[0].Addresses != 1 {
t.Errorf("subnets = %+v", r.Subnets)
}
if !reflect.DeepEqual(r.Targets.Failed["https"], []int{0}) || len(r.Sites.Rows) != 1 || r.Sites.Rows[0].Stats[1] != (SiteStat{Total: 1, OK: 0}) {
t.Errorf("targets %+v, sites %+v", r.Targets, r.Sites)
}
// the lists are cut from the same subset, so they agree with the tiles
for kind, want := range map[string]int{ListVerdictPartial: r.Summary.Partial, ListVerdictPass: 0, ListIngressSSHAny: r.Summary.IngressSSHAny, ListEgressHTTPSAny: 0} {
l, err := an.List(kind, "")
if err != nil {
t.Fatal(err)
}
if len(l.Rows) != want {
t.Errorf("%s: %d rows, want %d", kind, len(l.Rows), want)
}
}
}
func TestSubnetMatching(t *testing.T) {
in := []db.Subnet{{CIDR: "10.0.0.0/8"}, {CIDR: "10.1.0.0/16"}}
m := newSubnetMatcher(in)
+17 -4
View File
@@ -2,12 +2,14 @@ package analytics
import (
"context"
"net/netip"
"cloudipvalidator/internal/db"
)
// Load reads a finished run from the database and computes its analysis.
func Load(ctx context.Context, d *db.DB, runID int64) (*Analysis, error) {
// Load reads a finished run from the database and computes its analysis,
// narrowed to subnet unless that is the zero prefix.
func Load(ctx context.Context, d *db.DB, runID int64, subnet netip.Prefix) (*Analysis, error) {
run, err := d.GetRun(ctx, runID)
if err != nil {
return nil, err
@@ -32,8 +34,19 @@ func Load(ctx context.Context, d *db.DB, runID int64) (*Analysis, error) {
if err != nil {
return nil, err
}
// With a subnet only the checks of its addresses are read; Compute narrows
// the results by the same rule.
var ids []int64
if subnet.IsValid() {
ids = []int64{}
for _, r := range results {
if ip, err := netip.ParseAddr(r.IPAddress); err == nil && subnet.Contains(ip) {
ids = append(ids, r.RegistryID)
}
}
}
return Compute(Input{
Run: *run, Results: results, Subnets: subnets, SiteNames: names, Rechecked: rechecked,
Each: func(fn func(db.RunCheck)) error { return d.EachRunCheck(ctx, runID, fn) },
Run: *run, Results: results, Subnets: subnets, SiteNames: names, Rechecked: rechecked, Subnet: subnet,
Each: func(fn func(db.RunCheck)) error { return d.EachRunCheck(ctx, runID, ids, fn) },
})
}