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 }