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
117 lines
3.3 KiB
Go
117 lines
3.3 KiB
Go
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
|
|
}
|