check / check (push) Failing after 2m47s
The program is now sww, built from cmd/sww, with two modes: sww rproxy and sww discriminator. servewaf becomes discriminator everywhere it names that daemon: the package (internal/discriminator), its types, functions, files, the discriminator_line log field, and its docs and tests. smallwebwaf is left only where sneak's directive allows it: the documents' product name, the git.eeqj.de/sneak/smallwebwaf URLs, and the Go module path sneak.berlin/go/smallwebwaf. Everywhere else a name containing it takes the sww form: the package (internal/sww), metric names (sww_*), the X-Sww-* headers, the /_sww/ admin endpoints, the OS user, and the state and rules directories. The SWWAF_ environment variables take the SWW_ form. No logic changes. make check passes. Model: deepseek-v4-pro
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 lists 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 the country lists 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 lists 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
|
|
}
|