Add is_tor plumbing from collector->aggregator->frontend/cli

This commit is contained in:
2026-03-23 22:17:39 +01:00
parent b89caa594c
commit cd7f15afaf
20 changed files with 1815 additions and 212 deletions

View File

@@ -113,8 +113,21 @@ func applyTerm(term string, fs *filterState) error {
return fmt.Errorf("prefix only supports =, not %q", op)
}
fs.Prefix = value
case "is_tor":
if op != "=" && op != "!=" {
return fmt.Errorf("is_tor only supports = and !=, not %q", op)
}
if value != "0" && value != "1" {
return fmt.Errorf("is_tor value must be 0 or 1, not %q", value)
}
// Normalise: is_tor=1 and is_tor!=0 both mean "TOR only"
if (op == "=" && value == "1") || (op == "!=" && value == "0") {
fs.IsTor = "1"
} else {
fs.IsTor = "0"
}
default:
return fmt.Errorf("unknown field %q; valid: status, website, uri, prefix", field)
return fmt.Errorf("unknown field %q; valid: status, website, uri, prefix, is_tor", field)
}
return nil
}
@@ -151,6 +164,9 @@ func FilterExprString(f filterState) string {
if f.Status != "" {
parts = append(parts, statusTermStr(f.Status))
}
if f.IsTor != "" {
parts = append(parts, "is_tor="+f.IsTor)
}
return strings.Join(parts, " AND ")
}

View File

@@ -53,6 +53,7 @@ type filterState struct {
Status string // expression: "200", "!=200", ">=400", etc.
WebsiteRe string // RE2 regex against website
URIRe string // RE2 regex against request URI
IsTor string // "", "1" (TOR only), "0" (non-TOR only)
}
// QueryParams holds all parsed URL parameters for one page request.
@@ -77,6 +78,7 @@ type PageData struct {
Windows []Tab
GroupBys []Tab
Targets []Tab // source/target picker; empty when only one target available
TorTabs []Tab // all / tor / no-tor toggle
RefreshSecs int
Error string
FilterExpr string // current filter serialised to mini-language for the input box
@@ -156,12 +158,13 @@ func (h *Handler) parseParams(r *http.Request) QueryParams {
Status: q.Get("f_status"),
WebsiteRe: q.Get("f_website_re"),
URIRe: q.Get("f_uri_re"),
IsTor: q.Get("f_is_tor"),
},
}
}
func buildFilter(f filterState) *pb.Filter {
if f.Website == "" && f.Prefix == "" && f.URI == "" && f.Status == "" && f.WebsiteRe == "" && f.URIRe == "" {
if f.Website == "" && f.Prefix == "" && f.URI == "" && f.Status == "" && f.WebsiteRe == "" && f.URIRe == "" && f.IsTor == "" {
return nil
}
out := &pb.Filter{}
@@ -186,6 +189,12 @@ func buildFilter(f filterState) *pb.Filter {
if f.URIRe != "" {
out.UriRegex = &f.URIRe
}
switch f.IsTor {
case "1":
out.Tor = pb.TorFilter_TOR_YES
case "0":
out.Tor = pb.TorFilter_TOR_NO
}
return out
}
@@ -214,6 +223,9 @@ func (p QueryParams) toValues() url.Values {
if p.Filter.URIRe != "" {
v.Set("f_uri_re", p.Filter.URIRe)
}
if p.Filter.IsTor != "" {
v.Set("f_is_tor", p.Filter.IsTor)
}
return v
}
@@ -314,6 +326,18 @@ func buildCrumbs(p QueryParams) []Crumb {
RemoveURL: p.buildURL(map[string]string{"f_uri_re": ""}),
})
}
switch p.Filter.IsTor {
case "1":
crumbs = append(crumbs, Crumb{
Text: "is_tor=1 (TOR only)",
RemoveURL: p.buildURL(map[string]string{"f_is_tor": ""}),
})
case "0":
crumbs = append(crumbs, Crumb{
Text: "is_tor=0 (no TOR)",
RemoveURL: p.buildURL(map[string]string{"f_is_tor": ""}),
})
}
return crumbs
}
@@ -341,6 +365,23 @@ func buildGroupByTabs(p QueryParams) []Tab {
return tabs
}
func buildTorTabs(p QueryParams) []Tab {
specs := []struct{ val, label string }{
{"", "all"},
{"1", "tor"},
{"0", "no tor"},
}
tabs := make([]Tab, len(specs))
for i, s := range specs {
tabs[i] = Tab{
Label: s.label,
URL: p.buildURL(map[string]string{"f_is_tor": s.val}),
Active: p.Filter.IsTor == s.val,
}
}
return tabs
}
// buildTargetTabs builds the source/target picker tabs from a ListTargets response.
// Returns nil (hide picker) when only one endpoint is reachable.
func (h *Handler) buildTargetTabs(p QueryParams, lt *pb.ListTargetsResponse) []Tab {
@@ -502,6 +543,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Breadcrumbs: buildCrumbs(params),
Windows: buildWindowTabs(params),
GroupBys: buildGroupByTabs(params),
TorTabs: buildTorTabs(params),
Targets: h.buildTargetTabs(params, lt),
RefreshSecs: h.refreshSecs,
FilterExpr: filterExprInput,
@@ -524,6 +566,7 @@ func (h *Handler) errorPage(params QueryParams, msg string) PageData {
Params: params,
Windows: buildWindowTabs(params),
GroupBys: buildGroupByTabs(params),
TorTabs: buildTorTabs(params),
Breadcrumbs: buildCrumbs(params),
RefreshSecs: h.refreshSecs,
Error: msg,

View File

@@ -35,6 +35,7 @@ a:hover { text-decoration: underline; }
.nodata { color: #999; margin: 2em 0; font-style: italic; }
footer { margin-top: 2em; padding-top: 0.6em; border-top: 1px solid #e0e0e0; font-size: 0.8em; color: #999; }
.tabs-targets { margin-top: -0.4em; }
.tabs-tor { margin-top: -0.4em; }
.tabs-label { font-size: 0.85em; color: #888; margin-right: 0.2em; align-self: center; }
.filter-form { display: flex; gap: 0.4em; align-items: center; margin-bottom: 0.7em; }
.filter-input { flex: 1; font-family: monospace; font-size: 13px; padding: 0.25em 0.5em; border: 1px solid #aaa; }

View File

@@ -20,12 +20,19 @@
{{- end}}
</div>{{end}}
<div class="tabs tabs-tor">
<span class="tabs-label">tor:</span>
{{- range .TorTabs}}
<a href="{{.URL}}"{{if .Active}} class="active"{{end}}>{{.Label}}</a>
{{- end}}
</div>
<form class="filter-form" method="get" action="/">
<input type="hidden" name="target" value="{{.Params.Target}}">
<input type="hidden" name="w" value="{{.Params.WindowS}}">
<input type="hidden" name="by" value="{{.Params.GroupByS}}">
<input type="hidden" name="n" value="{{.Params.N}}">
<input class="filter-input" type="text" name="q" value="{{.FilterExpr}}" placeholder="status>=400 AND website~=gouda.* AND uri~=^/api/">
<input class="filter-input" type="text" name="q" value="{{.FilterExpr}}" placeholder="status>=400 AND website~=gouda.* AND uri~=^/api/ AND is_tor=1">
<button type="submit">filter</button>
{{- if .FilterExpr}} <a class="clear" href="{{.ClearFilterURL}}">× clear</a>{{end}}
</form>