check / check (push) Waiting to run
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
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
|
|
}
|