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:
1 parent
068c10ea1c
commit
ded196ec8d
40 files changed
+2545
-188
No files matched your search
@@ -0,0 +1,149 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Groups of a RegistryBreakdown: egress checks are grouped by target, ingress
|
||||
// ones by prober site.
|
||||
const (
|
||||
BreakdownTarget = "target"
|
||||
BreakdownSite = "site"
|
||||
)
|
||||
|
||||
// BreakdownRow is one group of a RegistryBreakdown: Key is the checks.target
|
||||
// (egress) or the checks.source (ingress, "inbound-site-N"), Total the checks
|
||||
// recorded for it and OK the successful ones.
|
||||
type BreakdownRow struct {
|
||||
Key string
|
||||
Total int
|
||||
OK int
|
||||
}
|
||||
|
||||
// Breakdown counts the checks of one direction and protocol per target or site.
|
||||
type Breakdown struct {
|
||||
Group string // BreakdownTarget or BreakdownSite
|
||||
Addresses int // addresses of the filter, the same as ListRegistryPage's total
|
||||
Rows []BreakdownRow
|
||||
}
|
||||
|
||||
// breakdownChecks is `FROM … JOIN checks c … WHERE …` over the checks of the
|
||||
// addresses f selects (registryFilterSQL), in the slice of f: the address's cycle
|
||||
// in the run or its newest one, narrowed by Level and Family, which must be
|
||||
// set. It also returns the column the checks are grouped by. CROSS JOIN keeps
|
||||
// the checks as the inner table, so the addresses drive the lookups over the
|
||||
// checks indexes whatever the table statistics say.
|
||||
func (d *DB) breakdownChecks(ctx context.Context, f RegistryFilter) (q, group string, args []any, err error) {
|
||||
sl := registrySlice{RunID: f.RunID, Level: f.Level, Family: f.Family}
|
||||
if err := sl.validate(); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
if sl.Level == "" || sl.Family == "" {
|
||||
return "", "", nil, fmt.Errorf("direction and protocol are required: %w", ErrValidation)
|
||||
}
|
||||
from, cond, args, err := d.registryFilterSQL(ctx, f, sl)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
group = "c.target"
|
||||
if sl.Level == LevelIngress {
|
||||
group = "c.source"
|
||||
}
|
||||
sc, scArgs := sl.scopeCond()
|
||||
q = from + `CROSS JOIN checks c ON c.registry_id = r.id AND c.cycle_id = `
|
||||
if sl.RunID > 0 {
|
||||
q += `rr.cycle_id WHERE ` + cond + ` AND c.run_id = ? AND ` + sc
|
||||
args = append(args, sl.RunID)
|
||||
} else {
|
||||
q += `(SELECT MAX(c2.cycle_id) FROM checks c2 WHERE c2.registry_id = r.id) WHERE ` + cond + ` AND ` + sc
|
||||
}
|
||||
return q, group, append(args, scArgs...), nil
|
||||
}
|
||||
|
||||
// RegistryBreakdown counts the recorded checks of f.Level and f.Family (both
|
||||
// required, else ErrValidation) per target (egress) or site (ingress), over
|
||||
// the same slice and the same addresses — all under the filter, not one page —
|
||||
// as ListRegistryPage. Rows are sorted by successful checks, most first, then by
|
||||
// key. It counts checks, not addresses: an address can have several checks per
|
||||
// site (tcp-22 and tcp-443).
|
||||
func (d *DB) RegistryBreakdown(ctx context.Context, f RegistryFilter) (*Breakdown, error) {
|
||||
from, group, args, err := d.breakdownChecks(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := &Breakdown{Group: BreakdownTarget}
|
||||
if f.Level == LevelIngress {
|
||||
b.Group = BreakdownSite
|
||||
}
|
||||
rows, err := d.QueryContext(ctx, `SELECT `+group+`, COUNT(*), COALESCE(SUM(c.success), 0) `+from+` GROUP BY `+group, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var r BreakdownRow
|
||||
if err := rows.Scan(&r.Key, &r.Total, &r.OK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Rows = append(b.Rows, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(b.Rows, func(i, j int) bool {
|
||||
if b.Rows[i].OK != b.Rows[j].OK {
|
||||
return b.Rows[i].OK > b.Rows[j].OK
|
||||
}
|
||||
return b.Rows[i].Key < b.Rows[j].Key
|
||||
})
|
||||
|
||||
// The addresses of the filter, as ListRegistryPage counts them.
|
||||
sl := registrySlice{RunID: f.RunID, Level: f.Level, Family: f.Family}
|
||||
afrom, cond, aargs, err := d.registryFilterSQL(ctx, f, sl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.QueryRowContext(ctx, `SELECT COUNT(*)`+afrom+`WHERE `+cond, aargs...).Scan(&b.Addresses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// RegistryBreakdownList returns the checks behind one row of RegistryBreakdown:
|
||||
// those whose target (egress) or source (ingress) is key, with the address
|
||||
// (IPAddress), type, validator, latency, detail and time, failures first, then
|
||||
// by registry order. A key without checks is ErrNotFound.
|
||||
func (d *DB) RegistryBreakdownList(ctx context.Context, f RegistryFilter, key string) ([]Check, error) {
|
||||
from, group, args, err := d.breakdownChecks(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := d.QueryContext(ctx, `
|
||||
SELECT r.ip_address, c.validator_id, c.source, c.check_type, c.target, c.success, c.latency_ms, c.detail, c.checked_at `+
|
||||
from+` AND `+group+` = ? ORDER BY c.success, r.first_seen_at, r.id, c.check_type, c.source, c.target`, append(args, key)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Check
|
||||
for rows.Next() {
|
||||
var c Check
|
||||
var checkedAt string
|
||||
if err := rows.Scan(&c.IPAddress, &c.ValidatorID, &c.Source, &c.CheckType, &c.Target, &c.Success, &c.LatencyMS, &c.Detail, &checkedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.CheckedAt, err = dbToTime(checkedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("breakdown key %q: %w", key, ErrNotFound)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in new issue
Block a user