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
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -24,6 +25,17 @@ type analyticsPageData struct {
|
||||
RunID int64
|
||||
PrevURL string // older run, "" when there is none
|
||||
NextURL string // newer run
|
||||
// CompareURL is the comparison page with this run as the target.
|
||||
CompareURL string
|
||||
// The filter of the page: a subnet recomputes every block, a direction
|
||||
// and a protocol focus the page on them (and give the chart).
|
||||
Subnet string
|
||||
Direction string
|
||||
Protocol string
|
||||
Protocols []string
|
||||
SubnetOptions []subnetOption
|
||||
Filtered bool // a subnet, direction or protocol is chosen
|
||||
Breakdown *breakdownView // chart of the direction and protocol; nil without both
|
||||
// DataJSON is the page's data for analytics.js (run meta, labels, report),
|
||||
// HTML-safe JSON.
|
||||
DataJSON template.JS
|
||||
@@ -40,12 +52,25 @@ type analyticsMeta struct {
|
||||
Rechecked int `json:"rechecked"`
|
||||
ListURL string `json:"list_url"`
|
||||
CSVURL string `json:"csv_url"`
|
||||
Registry string `json:"registry_url"`
|
||||
Registry string `json:"registry_url"` // registry of the run with the page's direction and protocol; a link adds the subnet
|
||||
Subnet string `json:"subnet"`
|
||||
Direction string `json:"direction"`
|
||||
Protocol string `json:"protocol"`
|
||||
}
|
||||
|
||||
// analyticsURL is the analytics page of a run with the slice filter f (subnet,
|
||||
// direction, protocol) of the page that links to it.
|
||||
func analyticsURL(run int64, f registryQuery) string {
|
||||
v := f.filter()
|
||||
v.Set("run", strconv.FormatInt(run, 10))
|
||||
return "/analytics?" + v.Encode()
|
||||
}
|
||||
|
||||
// handleAnalyticsPage renders the analytics of one finished run: ?run=ID, by
|
||||
// default the newest finished run. The run selector lists every run, the open
|
||||
// one disabled; nothing of any other run is on the page.
|
||||
// one disabled; nothing of any other run is on the page. ?subnet= narrows every
|
||||
// block to the addresses of that subnet; ?direction=&protocol= focus the page
|
||||
// (analytics.js) and, both given, add the chart of the registry.
|
||||
func (s *Server) handleAnalyticsPage(w http.ResponseWriter, r *http.Request) {
|
||||
data := analyticsPageData{}
|
||||
data.ActiveNav = "analytics"
|
||||
@@ -56,7 +81,16 @@ func (s *Server) handleAnalyticsPage(w http.ResponseWriter, r *http.Request) {
|
||||
s.renderPage(w, r, "analytics_page", data)
|
||||
return
|
||||
}
|
||||
want, _ := strconv.ParseInt(r.URL.Query().Get("run"), 10, 64)
|
||||
f := parseSliceFilter(r.URL.Query())
|
||||
want := f.Run
|
||||
f.Run = 0 // the links below name the run themselves
|
||||
// the subnet choices come from the configured list; without it the filter
|
||||
// still works, just without the choices
|
||||
subnets, subnetsErr := s.CA.GetSubnets(r.Context())
|
||||
data.Subnet, data.Direction, data.Protocol = f.Subnet, f.Direction, f.Protocol
|
||||
data.Protocols = registryProtocols
|
||||
data.SubnetOptions = registrySubnetOptions(subnets.Subnets, f.Subnet)
|
||||
data.Filtered = f.Subnet != "" || f.Direction != "" || f.Protocol != ""
|
||||
var chosen *analyticsRun
|
||||
for i := range runs { // newest first
|
||||
if runs[i].State != "finalized" || runs[i].Addresses == 0 {
|
||||
@@ -94,15 +128,16 @@ func (s *Server) handleAnalyticsPage(w http.ResponseWriter, r *http.Request) {
|
||||
for i, x := range finished {
|
||||
if x.ID == chosen.ID {
|
||||
if i+1 < len(finished) {
|
||||
data.PrevURL = "/analytics?run=" + strconv.FormatInt(finished[i+1].ID, 10)
|
||||
data.PrevURL = analyticsURL(finished[i+1].ID, f)
|
||||
}
|
||||
if i > 0 {
|
||||
data.NextURL = "/analytics?run=" + strconv.FormatInt(finished[i-1].ID, 10)
|
||||
data.NextURL = analyticsURL(finished[i-1].ID, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
data.CompareURL = compareURL(chosen.ID, f)
|
||||
|
||||
report, err := s.CA.GetAnalyticsReport(r.Context(), chosen.ID)
|
||||
report, err := s.CA.GetAnalyticsReport(r.Context(), chosen.ID, f.Subnet)
|
||||
if err != nil {
|
||||
data.Banner = bannerFor(err)
|
||||
s.renderPage(w, r, "analytics_page", data)
|
||||
@@ -120,7 +155,12 @@ func (s *Server) handleAnalyticsPage(w http.ResponseWriter, r *http.Request) {
|
||||
Start: fmtShort(chosen.StartedAt), Duration: "—", Rechecked: info.Run.Rechecked,
|
||||
ListURL: "/analytics/lists/",
|
||||
CSVURL: "/analytics/csv/",
|
||||
Registry: "/registry?run=" + id,
|
||||
Registry: "/registry?run=" + id, Subnet: f.Subnet, Direction: f.Direction, Protocol: f.Protocol,
|
||||
}
|
||||
for _, p := range [][2]string{{"direction", f.Direction}, {"protocol", f.Protocol}} {
|
||||
if p[1] != "" {
|
||||
meta.Registry += "&" + p[0] + "=" + url.QueryEscape(p[1])
|
||||
}
|
||||
}
|
||||
if chosen.FinalizedAt != nil {
|
||||
meta.End = fmtShort(*chosen.FinalizedAt)
|
||||
@@ -137,14 +177,33 @@ func (s *Server) handleAnalyticsPage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
data.HasRun = true
|
||||
data.DataJSON = template.JS(payload)
|
||||
if f.Direction != "" && f.Protocol != "" {
|
||||
f.Run = chosen.ID
|
||||
data.Breakdown = s.registryBreakdown(r, f, f.filter())
|
||||
}
|
||||
data.Banner = bannerFor(subnetsErr)
|
||||
s.renderPage(w, r, "analytics_page", data)
|
||||
}
|
||||
|
||||
// compareURL is the comparison page with a run as the target; it carries the
|
||||
// slice filter along, as the other links of the page do.
|
||||
func compareURL(target int64, f registryQuery) string {
|
||||
v := f.filter()
|
||||
v.Set("target", strconv.FormatInt(target, 10))
|
||||
return "/analytics/compare?" + v.Encode()
|
||||
}
|
||||
|
||||
func parseRunParam(r *http.Request) (int64, bool) {
|
||||
id, err := strconv.ParseInt(r.URL.Query().Get("run"), 10, 64)
|
||||
return id, err == nil && id > 0
|
||||
}
|
||||
|
||||
// analyticsSubnetParam is the ?subnet= of a list request as it came: control-api
|
||||
// validates it, so a malformed one is an error and never a silently wider list.
|
||||
func analyticsSubnetParam(r *http.Request) string {
|
||||
return strings.TrimSpace(r.URL.Query().Get("subnet"))
|
||||
}
|
||||
|
||||
// handleAnalyticsList proxies one address table of a run as JSON.
|
||||
func (s *Server) handleAnalyticsList(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := parseRunParam(r)
|
||||
@@ -152,7 +211,7 @@ func (s *Server) handleAnalyticsList(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "run is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
out, err := s.CA.GetAnalyticsList(r.Context(), id, r.PathValue("kind"), r.URL.Query().Get("class"))
|
||||
out, err := s.CA.GetAnalyticsList(r.Context(), id, r.PathValue("kind"), r.URL.Query().Get("class"), analyticsSubnetParam(r))
|
||||
if err != nil {
|
||||
writeProxyError(w, err)
|
||||
return
|
||||
@@ -169,7 +228,7 @@ func (s *Server) handleAnalyticsCSV(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "run is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
body, disposition, err := s.CA.GetAnalyticsListCSV(r.Context(), id, r.PathValue("kind"), r.URL.Query().Get("class"))
|
||||
body, disposition, err := s.CA.GetAnalyticsListCSV(r.Context(), id, r.PathValue("kind"), r.URL.Query().Get("class"), analyticsSubnetParam(r))
|
||||
if err != nil {
|
||||
writeProxyError(w, err)
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user