Serve Prometheus metrics behind SWWAF_METRICS_TOKEN (closes #23)
check / check (push) Successful in 4m12s
check / check (push) Successful in 4m12s
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. SWWAF_METRICS_TOP_N bounds the series by country, the rest counted as other. Judgement call: a request answered at smallwebwaf's own endpoints is neither forwarded nor refused in the client's history. Deviation: go.mod and go.sum written by hand from the module proxy and sum.golang.org, 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:
@@ -0,0 +1,116 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
)
|
||||
|
||||
// other is the label under which the countries outside the busiest are
|
||||
// counted.
|
||||
const other = "other"
|
||||
|
||||
// countries are the metrics by the client's country, for requests whose
|
||||
// client's country is known. The topN busiest countries, by their requests
|
||||
// since the start, have series of their own, and the others are counted
|
||||
// under other, so that there are never more than topN + 1 series. A
|
||||
// country that drops out of the busiest loses its series, and its next
|
||||
// requests are counted under other; one that becomes one of them gets a
|
||||
// series that counts from then on. Each series therefore only ever goes
|
||||
// up.
|
||||
type countries struct {
|
||||
topN int
|
||||
|
||||
requests *prometheus.CounterVec
|
||||
requestBytes *prometheus.CounterVec
|
||||
responseBytes *prometheus.CounterVec
|
||||
// refused are the requests the country lists refused.
|
||||
refused *prometheus.CounterVec
|
||||
|
||||
mu sync.Mutex
|
||||
// seen is each country's requests since the start, by which the
|
||||
// countries are ranked. GeoJS gives two-letter codes, so it holds at
|
||||
// most a few hundred.
|
||||
seen map[string]int64
|
||||
// top are the countries with series of their own.
|
||||
top map[string]bool
|
||||
}
|
||||
|
||||
// newCountries returns the metrics by country, with series of their own
|
||||
// for the topN busiest countries.
|
||||
func newCountries(topN int) *countries {
|
||||
byCountry := []string{"country"}
|
||||
|
||||
return &countries{
|
||||
topN: topN,
|
||||
requests: counterVec("smallwebwaf_country_requests_total",
|
||||
"Requests, by the client's country.", byCountry),
|
||||
requestBytes: counterVec("smallwebwaf_country_request_bytes_total",
|
||||
"Request body bytes, by the client's country.", byCountry),
|
||||
responseBytes: counterVec("smallwebwaf_country_response_bytes_total",
|
||||
"Response body bytes, by the client's country.", byCountry),
|
||||
refused: counterVec("smallwebwaf_country_list_refusals_total",
|
||||
"Requests the country lists refused, by the client's country.",
|
||||
byCountry),
|
||||
seen: map[string]int64{},
|
||||
top: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
// add counts a request from its log line, whose country is known.
|
||||
func (c *countries) add(line *requestlog.Line) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.seen[line.Country]++
|
||||
|
||||
label := c.label(line.Country)
|
||||
c.requests.WithLabelValues(label).Inc()
|
||||
c.requestBytes.WithLabelValues(label).Add(float64(line.RequestBytes))
|
||||
c.responseBytes.WithLabelValues(label).Add(float64(line.ResponseBytes))
|
||||
|
||||
if line.Action == requestlog.ActionCountryDenied {
|
||||
c.refused.WithLabelValues(label).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
// label returns the label a request from country is counted under: the
|
||||
// country while it is one of the busiest, other while it is not. A
|
||||
// country busier than the least busy of them takes its place, and that
|
||||
// country's series are dropped.
|
||||
func (c *countries) label(country string) string {
|
||||
if c.top[country] {
|
||||
return country
|
||||
}
|
||||
|
||||
if len(c.top) < c.topN {
|
||||
c.top[country] = true
|
||||
|
||||
return country
|
||||
}
|
||||
|
||||
least := ""
|
||||
|
||||
for top := range c.top {
|
||||
if least == "" || c.seen[top] < c.seen[least] {
|
||||
least = top
|
||||
}
|
||||
}
|
||||
|
||||
if c.seen[country] <= c.seen[least] {
|
||||
return other
|
||||
}
|
||||
|
||||
delete(c.top, least)
|
||||
|
||||
for _, vec := range []*prometheus.CounterVec{
|
||||
c.requests, c.requestBytes, c.responseBytes, c.refused,
|
||||
} {
|
||||
vec.DeleteLabelValues(least)
|
||||
}
|
||||
|
||||
c.top[country] = true
|
||||
|
||||
return country
|
||||
}
|
||||
Reference in New Issue
Block a user