Serve Prometheus metrics behind SWWAF_METRICS_TOKEN (closes #23)
check / check (push) Successful in 4m35s

GET /_smallwebwaf/metrics answers in the Prometheus text format for a
request carrying SWWAF_METRICS_TOKEN, 401 without it and 404 while it is
unset. Every request under /_smallwebwaf/ but the health check now goes
through the checks and is answered where it would be forwarded, 404 for
any path but the metrics, so none reaches the app. In the client's
history a 401 counts as refused, the metrics and the 404s as neither.
SWWAF_METRICS_TOP_N bounds the series by country, the rest counted as
other.

Deviation: go.mod and go.sum written by hand, as go runs only through
make.
Deviation: no metrics yet for state files read again after an edit or
edits set aside; that work is not merged.

Model: opus-5-5
This commit is contained in:
2026-10-06 09:18:38 +00:00
parent 68f687cb0c
commit 99702de60b
25 changed files with 1548 additions and 83 deletions
+17
View File
@@ -19,6 +19,7 @@ import (
"time"
"github.com/hashicorp/golang-lru/v2/simplelru"
"sneak.berlin/go/smallwebwaf/internal/metrics"
)
// URL is GeoJS's country endpoint. Asked about several addresses at once,
@@ -64,6 +65,9 @@ type Params struct {
Now func() time.Time
// ProcessLog receives GeoJS's failures.
ProcessLog *slog.Logger
// Metrics count the requests to GeoJS, those that failed, and the
// clients that go without an answer.
Metrics *metrics.Metrics
}
// GeoJS looks up clients' countries through GeoJS. At most one request
@@ -73,6 +77,7 @@ type GeoJS struct {
url string
now func() time.Time
processLog *slog.Logger
metrics *metrics.Metrics
// httpClient follows no redirect, so that visitors' addresses go to
// GeoJS alone: a redirect is a failure.
httpClient *http.Client
@@ -121,6 +126,7 @@ func New(params Params) *GeoJS {
url: params.URL,
now: params.Now,
processLog: params.ProcessLog,
metrics: params.Metrics,
httpClient: &http.Client{
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
@@ -160,6 +166,9 @@ func (g *GeoJS) Country(ctx context.Context, client netip.Prefix) string {
defer g.mu.Unlock()
country, found := g.kept(client)
if !found {
g.metrics.GeoJSUnanswered.Inc()
}
w, waiting := g.waiting[client]
if !found && waiting {
@@ -234,6 +243,8 @@ func (g *GeoJS) answerOrWait(
g.ask(ctx)
if w == nil {
g.metrics.GeoJSUnanswered.Inc()
return "", nil // too many clients wait already
}
@@ -243,6 +254,8 @@ func (g *GeoJS) answerOrWait(
}
if w.late {
g.metrics.GeoJSUnanswered.Inc()
return "", nil
}
@@ -355,6 +368,8 @@ func (g *GeoJS) keep(
}
if err != nil {
g.metrics.GeoJSFailures.Inc()
g.retryDelay = min(max(retryDelayFactor*g.retryDelay, firstRetryDelay),
maxRetryDelay)
g.retryAt = now.Add(g.retryDelay)
@@ -399,6 +414,8 @@ func (g *GeoJS) request(
req.URL.RawQuery = "ip=" + strings.Join(addrs, ",")
g.metrics.GeoJSRequests.Inc()
res, err := g.httpClient.Do(req)
if err != nil {
// Do's error names the URL, and so the visitors' addresses, which