check / check (push) Failing after 1m14s
Every setting of discriminator and rproxy is read through viper, in place of the os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and AllowEmptyEnv on, so a setting that is set but empty reads as set. A setting may also be given as a file, the _FILE form, which rproxy's settings now have too; rproxy still writes nothing to disk. The settings are renamed. Discriminator's take the prefix SWW_WAF_, rproxy's take SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_. Model: deepseek-v4-pro (first version); opus-5-5 (rework)
156 lines
4.4 KiB
Go
156 lines
4.4 KiB
Go
package metrics
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
)
|
|
|
|
// other is the label value under which the countries or AS numbers
|
|
// outside the busiest are counted.
|
|
const other = "other"
|
|
|
|
// busiest are the metrics by one thing the lookup finds of the client,
|
|
// its country or its AS number, for requests whose client's is known.
|
|
// The topN busiest countries or AS numbers, 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. One 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 busiest struct {
|
|
topN int
|
|
|
|
requests *prometheus.CounterVec
|
|
requestBytes *prometheus.CounterVec
|
|
responseBytes *prometheus.CounterVec
|
|
// refused are, by country, the requests the country list refused; nil
|
|
// by AS number.
|
|
refused *prometheus.CounterVec
|
|
|
|
mu sync.Mutex
|
|
// seen is each country's or AS number's requests since the start, by
|
|
// which they are ranked.
|
|
seen map[string]int64
|
|
// top are those 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) *busiest {
|
|
countries := newBusiest(topN, "country", "the client's country")
|
|
countries.refused = counterVec("sww_country_list_refusals_total",
|
|
"Requests SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES refused, by the client's country.",
|
|
[]string{"country"})
|
|
|
|
return countries
|
|
}
|
|
|
|
// newASNs returns the metrics by AS number, with series of their own for
|
|
// the topN busiest AS numbers.
|
|
func newASNs(topN int) *busiest {
|
|
return newBusiest(topN, "asn", "the client's AS number")
|
|
}
|
|
|
|
// newBusiest returns the metrics by label, which is described as
|
|
// description, with series of their own for the topN busiest values.
|
|
func newBusiest(topN int, label, description string) *busiest {
|
|
by := []string{label}
|
|
|
|
return &busiest{
|
|
topN: topN,
|
|
requests: counterVec("sww_"+label+"_requests_total",
|
|
"Requests, by "+description+".", by),
|
|
requestBytes: counterVec("sww_"+label+"_request_bytes_total",
|
|
"Request body bytes, by "+description+".", by),
|
|
responseBytes: counterVec("sww_"+label+"_response_bytes_total",
|
|
"Response body bytes, by "+description+".", by),
|
|
seen: map[string]int64{},
|
|
top: map[string]bool{},
|
|
}
|
|
}
|
|
|
|
// Describe and Collect make the metrics a prometheus.Collector, so that
|
|
// they are registered together.
|
|
func (b *busiest) Describe(ch chan<- *prometheus.Desc) {
|
|
for _, vec := range b.vecs() {
|
|
vec.Describe(ch)
|
|
}
|
|
}
|
|
|
|
// Collect is the other half of prometheus.Collector, with Describe.
|
|
func (b *busiest) Collect(ch chan<- prometheus.Metric) {
|
|
for _, vec := range b.vecs() {
|
|
vec.Collect(ch)
|
|
}
|
|
}
|
|
|
|
// vecs returns the metrics: by AS number, those of requests and bytes; by
|
|
// country, the refusals by the country list as well.
|
|
func (b *busiest) vecs() []*prometheus.CounterVec {
|
|
vecs := []*prometheus.CounterVec{b.requests, b.requestBytes, b.responseBytes}
|
|
if b.refused != nil {
|
|
vecs = append(vecs, b.refused)
|
|
}
|
|
|
|
return vecs
|
|
}
|
|
|
|
// add counts a request from its log line, whose client's country or AS
|
|
// number, value, is known.
|
|
func (b *busiest) add(value string, line *requestlog.Line) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
b.seen[value]++
|
|
|
|
label := b.label(value)
|
|
b.requests.WithLabelValues(label).Inc()
|
|
b.requestBytes.WithLabelValues(label).Add(float64(line.RequestBytes))
|
|
b.responseBytes.WithLabelValues(label).Add(float64(line.ResponseBytes))
|
|
|
|
if b.refused != nil && line.Action == requestlog.ActionCountryDenied {
|
|
b.refused.WithLabelValues(label).Inc()
|
|
}
|
|
}
|
|
|
|
// label returns the label a request from value is counted under: value
|
|
// while it is one of the busiest, other while it is not. A value busier
|
|
// than the least busy of them takes its place, and that one's series are
|
|
// dropped.
|
|
func (b *busiest) label(value string) string {
|
|
if b.top[value] {
|
|
return value
|
|
}
|
|
|
|
if len(b.top) < b.topN {
|
|
b.top[value] = true
|
|
|
|
return value
|
|
}
|
|
|
|
least := ""
|
|
|
|
for top := range b.top {
|
|
if least == "" || b.seen[top] < b.seen[least] {
|
|
least = top
|
|
}
|
|
}
|
|
|
|
if b.seen[value] <= b.seen[least] {
|
|
return other
|
|
}
|
|
|
|
delete(b.top, least)
|
|
|
|
for _, vec := range b.vecs() {
|
|
vec.DeleteLabelValues(least)
|
|
}
|
|
|
|
b.top[value] = true
|
|
|
|
return value
|
|
}
|