Instance name on process log lines and every metric (closes #91)
check / check (push) Waiting to run
check / check (push) Waiting to run
Process log lines carry instance, as request lines do; the instance name is read before the other settings, so the line saying a setting is invalid carries it too. Every metric, Go's and the process's included, carries the label instance, set once on the registry. README.md says so, and that Prometheus keeps it as exported_instance unless the scrape sets honor_labels. An instance name that is not valid UTF-8 stops the start, as the metrics library panics on such a label. Tests that read metrics expect the label; one helper replaces the alert tests' loops that wait for them. Judgement call: the label is named instance, as in the log lines and alerts, although Prometheus gives each target a label of that name. Model: opus-5-5
This commit was merged in pull request #96.
This commit is contained in:
@@ -148,8 +148,8 @@ func TestMetricsCountTheTraffic(t *testing.T) {
|
||||
wantStatus(t, get(t, addr, "/_smallwebwaf/nothing"), http.StatusNotFound)
|
||||
out.requestLines(t, 2)
|
||||
|
||||
forward := `{action="forward",status_class="2xx"}`
|
||||
notFound := `{action="admin",status_class="4xx"}`
|
||||
forward := `{action="forward",instance="app",status_class="2xx"}`
|
||||
notFound := `{action="admin",instance="app",status_class="4xx"}`
|
||||
|
||||
// The request for the metrics is itself under way.
|
||||
metrics := scrape(t, addr)
|
||||
@@ -159,11 +159,13 @@ func TestMetricsCountTheTraffic(t *testing.T) {
|
||||
wantMetric(t, metrics, "smallwebwaf_response_bytes_total"+forward, 5)
|
||||
wantMetric(t, metrics, "smallwebwaf_response_bytes_total"+notFound,
|
||||
float64(len("Not Found\n")))
|
||||
wantMetric(t, metrics, "smallwebwaf_request_duration_seconds_count", 2)
|
||||
wantMetric(t, metrics, "smallwebwaf_upstream_duration_seconds_count", 1)
|
||||
wantMetric(t, metrics, "smallwebwaf_requests_in_flight", 1)
|
||||
metric(t, metrics, "go_goroutines")
|
||||
metric(t, metrics, "process_start_time_seconds")
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_request_duration_seconds_count{instance="app"}`, 2)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_upstream_duration_seconds_count{instance="app"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_requests_in_flight{instance="app"}`, 1)
|
||||
metric(t, metrics, `go_goroutines{instance="app"}`)
|
||||
metric(t, metrics, `process_start_time_seconds{instance="app"}`)
|
||||
|
||||
// A request the app holds is under way until it ends.
|
||||
httpClient := newClient(t)
|
||||
@@ -180,7 +182,7 @@ func TestMetricsCountTheTraffic(t *testing.T) {
|
||||
}()
|
||||
|
||||
<-arrived
|
||||
wantMetric(t, scrape(t, addr), "smallwebwaf_requests_in_flight", 2)
|
||||
wantMetric(t, scrape(t, addr), `smallwebwaf_requests_in_flight{instance="app"}`, 2)
|
||||
releaseApp()
|
||||
|
||||
err := <-ended
|
||||
@@ -189,7 +191,7 @@ func TestMetricsCountTheTraffic(t *testing.T) {
|
||||
}
|
||||
|
||||
out.requestLines(t, 5)
|
||||
wantMetric(t, scrape(t, addr), "smallwebwaf_requests_in_flight", 1)
|
||||
wantMetric(t, scrape(t, addr), `smallwebwaf_requests_in_flight{instance="app"}`, 1)
|
||||
}
|
||||
|
||||
func TestMetricsCountLimitsAndBans(t *testing.T) {
|
||||
@@ -219,15 +221,16 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
|
||||
|
||||
metrics := s.scrape(scraper)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_requests_total{action="denied",status_class="none"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_rate_limit_hits_total{window="minute"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_offences_total{kind="limit"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_bans_made_total{cause="limit"}`, 1)
|
||||
wantMetric(t, metrics, "smallwebwaf_active_bans", 1)
|
||||
wantMetric(t, metrics, "smallwebwaf_permanent_bans", 0)
|
||||
`smallwebwaf_requests_total{action="denied",instance="app",status_class="none"}`, 1)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_rate_limit_hits_total{instance="app",window="minute"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_offences_total{instance="app",kind="limit"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_bans_made_total{cause="limit",instance="app"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_active_bans{instance="app"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_permanent_bans{instance="app"}`, 0)
|
||||
|
||||
clk.advance(time.Hour)
|
||||
wantMetric(t, s.scrape(scraper), "smallwebwaf_active_bans", 0)
|
||||
wantMetric(t, s.scrape(scraper), `smallwebwaf_active_bans{instance="app"}`, 0)
|
||||
|
||||
// A limit broken again right after would ban for three hours, longer
|
||||
// than SWWAF_MAX_BAN_DURATION, so the ban is permanent.
|
||||
@@ -235,13 +238,14 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
|
||||
s.get(client, 0, requestlog.ActionRateLimited)
|
||||
|
||||
metrics = s.scrape(scraper)
|
||||
wantMetric(t, metrics, `smallwebwaf_rate_limit_hits_total{window="minute"}`, 2)
|
||||
wantMetric(t, metrics, `smallwebwaf_offences_total{kind="limit"}`, 2)
|
||||
wantMetric(t, metrics, `smallwebwaf_bans_made_total{cause="limit"}`, 2)
|
||||
wantMetric(t, metrics, "smallwebwaf_active_bans", 1)
|
||||
wantMetric(t, metrics, "smallwebwaf_permanent_bans", 1)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_rate_limit_hits_total{instance="app",window="minute"}`, 2)
|
||||
wantMetric(t, metrics, `smallwebwaf_offences_total{instance="app",kind="limit"}`, 2)
|
||||
wantMetric(t, metrics, `smallwebwaf_bans_made_total{cause="limit",instance="app"}`, 2)
|
||||
wantMetric(t, metrics, `smallwebwaf_active_bans{instance="app"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_permanent_bans{instance="app"}`, 1)
|
||||
// denied, client, and the scraper as of its earlier requests.
|
||||
wantMetric(t, metrics, "smallwebwaf_tracked_clients", 3)
|
||||
wantMetric(t, metrics, `smallwebwaf_tracked_clients{instance="app"}`, 3)
|
||||
}
|
||||
|
||||
func TestMetricsCountTheBansAnAdminMakes(t *testing.T) {
|
||||
@@ -255,7 +259,7 @@ func TestMetricsCountTheBansAnAdminMakes(t *testing.T) {
|
||||
rateLimitExemptNets: scraper,
|
||||
})
|
||||
|
||||
const admins = `smallwebwaf_bans_made_total{cause="admin"}`
|
||||
const admins = `smallwebwaf_bans_made_total{cause="admin",instance="app"}`
|
||||
|
||||
wantMetric(t, s.scrape(scraper), admins, 0)
|
||||
|
||||
@@ -319,28 +323,42 @@ func TestMetricsByCountryKeepTheBusiestAndCountTheRestAsOther(t *testing.T) {
|
||||
metrics := scrape(t, addr)
|
||||
lines++
|
||||
|
||||
wantMetric(t, metrics, `smallwebwaf_country_requests_total{country="KP"}`, 3)
|
||||
wantMetric(t, metrics, `smallwebwaf_country_requests_total{country="DE"}`, 2)
|
||||
wantMetric(t, metrics, `smallwebwaf_country_requests_total{country="other"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_country_list_refusals_total{country="KP"}`, 3)
|
||||
wantMetric(t, metrics, `smallwebwaf_country_request_bytes_total{country="KP"}`, 0)
|
||||
wantMetric(t, metrics, `smallwebwaf_country_request_bytes_total{country="DE"}`, 6)
|
||||
wantMetric(t, metrics, `smallwebwaf_country_response_bytes_total{country="KP"}`,
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_requests_total{country="KP",instance="app"}`, 3)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_requests_total{country="DE",instance="app"}`, 2)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_requests_total{country="other",instance="app"}`, 1)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_list_refusals_total{country="KP",instance="app"}`, 3)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_request_bytes_total{country="KP",instance="app"}`, 0)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_request_bytes_total{country="DE",instance="app"}`, 6)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_response_bytes_total{country="KP",instance="app"}`,
|
||||
float64(3*len("Forbidden\n")))
|
||||
wantMetric(t, metrics, `smallwebwaf_country_response_bytes_total{country="other"}`,
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_response_bytes_total{country="other",instance="app"}`,
|
||||
float64(len("hello")))
|
||||
wantNoSeries(t, metrics, `smallwebwaf_country_requests_total{country="FR"}`)
|
||||
wantNoSeries(t, metrics,
|
||||
`smallwebwaf_country_requests_total{country="FR",instance="app"}`)
|
||||
|
||||
// Once FR is busier than DE, it takes DE's place: its series counts
|
||||
// from then on, and DE's is gone.
|
||||
send(fromFR, 3, http.StatusOK)
|
||||
|
||||
metrics = scrape(t, addr)
|
||||
wantMetric(t, metrics, `smallwebwaf_country_requests_total{country="KP"}`, 3)
|
||||
wantMetric(t, metrics, `smallwebwaf_country_requests_total{country="FR"}`, 2)
|
||||
wantMetric(t, metrics, `smallwebwaf_country_requests_total{country="other"}`, 2)
|
||||
wantNoSeries(t, metrics, `smallwebwaf_country_requests_total{country="DE"}`)
|
||||
wantNoSeries(t, metrics, `smallwebwaf_country_request_bytes_total{country="DE"}`)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_requests_total{country="KP",instance="app"}`, 3)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_requests_total{country="FR",instance="app"}`, 2)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_country_requests_total{country="other",instance="app"}`, 2)
|
||||
wantNoSeries(t, metrics,
|
||||
`smallwebwaf_country_requests_total{country="DE",instance="app"}`)
|
||||
wantNoSeries(t, metrics,
|
||||
`smallwebwaf_country_request_bytes_total{country="DE",instance="app"}`)
|
||||
}
|
||||
|
||||
func TestMetricsCountGeoJSRequestsAndFailures(t *testing.T) {
|
||||
@@ -370,16 +388,16 @@ func TestMetricsCountGeoJSRequestsAndFailures(t *testing.T) {
|
||||
deadline := time.Now().Add(waitLimit)
|
||||
metrics := scrape(t, addr)
|
||||
|
||||
for metric(t, metrics, "smallwebwaf_geojs_failures_total") == 0 &&
|
||||
for metric(t, metrics, `smallwebwaf_geojs_failures_total{instance="app"}`) == 0 &&
|
||||
time.Now().Before(deadline) {
|
||||
time.Sleep(pollInterval)
|
||||
|
||||
metrics = scrape(t, addr)
|
||||
}
|
||||
|
||||
wantMetric(t, metrics, "smallwebwaf_geojs_requests_total", 1)
|
||||
wantMetric(t, metrics, "smallwebwaf_geojs_failures_total", 1)
|
||||
wantMetric(t, metrics, "smallwebwaf_geojs_unanswered_total", 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_geojs_requests_total{instance="app"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_geojs_failures_total{instance="app"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_geojs_unanswered_total{instance="app"}`, 1)
|
||||
}
|
||||
|
||||
// keptAnswer returns GeoJS's answer that the client at addr is in
|
||||
@@ -422,8 +440,9 @@ func (s *sender) scrape(from string) string {
|
||||
|
||||
// metric returns the value of series in metrics, which are in the
|
||||
// Prometheus text format. series is a name and its labels in the order of
|
||||
// their names, such as smallwebwaf_offences_total{kind="limit"}. It fails
|
||||
// the test if there is no such series.
|
||||
// their names, such as
|
||||
// smallwebwaf_offences_total{instance="app",kind="limit"}. It fails the
|
||||
// test if there is no such series.
|
||||
func metric(t *testing.T, metrics, series string) float64 {
|
||||
t.Helper()
|
||||
|
||||
@@ -471,7 +490,8 @@ func wantNoSeries(t *testing.T, metrics, series string) {
|
||||
func wantLimitHits(t *testing.T, addr, limit string, hits int) {
|
||||
t.Helper()
|
||||
|
||||
series := `smallwebwaf_size_and_time_limit_hits_total{limit="` + limit + `"}`
|
||||
series := `smallwebwaf_size_and_time_limit_hits_total{instance="app",limit="` +
|
||||
limit + `"}`
|
||||
metrics := scrape(t, addr)
|
||||
|
||||
if hits == 0 {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -123,10 +122,8 @@ func wantAnswer(t *testing.T, got answer, body []byte) {
|
||||
func wantRequestFields(t *testing.T, line logLine, host string, sent, received int) {
|
||||
t.Helper()
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
want := withTimings(line, requestlog.Line{
|
||||
Type: requestType, Time: line.Time, Instance: hostname,
|
||||
Type: requestType, Time: line.Time, Instance: "app",
|
||||
ClientIP: localhost, Method: http.MethodPatch, Scheme: plain, Host: host,
|
||||
Path: rawPath, Query: rawQuery, Protocol: protocol,
|
||||
Status: http.StatusTeapot, RequestBytes: int64(sent),
|
||||
@@ -318,7 +315,7 @@ func TestServerHasTheDefaultLimits(t *testing.T) {
|
||||
server := proxy.New(proxy.Params{
|
||||
Config: cfg,
|
||||
RequestLog: io.Discard,
|
||||
ProcessLog: requestlog.NewProcessLogger(io.Discard),
|
||||
ProcessLog: requestlog.NewProcessLogger(io.Discard, cfg.InstanceName),
|
||||
})
|
||||
|
||||
if server.Addr != ":8080" || server.MaxHeaderBytes != 28<<10 ||
|
||||
|
||||
@@ -88,7 +88,7 @@ type Server struct {
|
||||
// applies the timeouts and size limits from then on.
|
||||
func New(params Params) *Server {
|
||||
errorLog := slog.NewLogLogger(params.ProcessLog.Handler(), slog.LevelWarn)
|
||||
m := metrics.New(params.Config.MetricsTopN)
|
||||
m := metrics.New(params.Config.MetricsTopN, params.Config.InstanceName)
|
||||
h := &handler{
|
||||
config: params.Config,
|
||||
requestLog: params.RequestLog,
|
||||
|
||||
@@ -217,7 +217,9 @@ func startProxyWithGeoJS(
|
||||
|
||||
// startProxyWithClock is startProxyWithGeoJS with requests counted and
|
||||
// bans made by the time now tells, and returns the server as well. Unless
|
||||
// env sets SWWAF_RULES_DIR, it is an empty directory, of no rules.
|
||||
// env sets SWWAF_RULES_DIR, it is an empty directory, of no rules, and
|
||||
// unless it sets SWWAF_INSTANCE_NAME, that is app, the label instance of
|
||||
// every metric.
|
||||
func startProxyWithClock(
|
||||
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
||||
env map[string]string,
|
||||
@@ -238,7 +240,9 @@ func startProxyWithAlerts(
|
||||
) (string, *output, *proxy.Server, *alerts.Queue) {
|
||||
t.Helper()
|
||||
|
||||
settings := map[string]string{"SWWAF_UPSTREAM_URL": appURL, rulesDir: t.TempDir()}
|
||||
settings := map[string]string{
|
||||
"SWWAF_UPSTREAM_URL": appURL, rulesDir: t.TempDir(), instanceName: "app",
|
||||
}
|
||||
maps.Copy(settings, env)
|
||||
|
||||
cfg, err := config.FromEnvironment(func(name string) (string, bool) {
|
||||
@@ -251,7 +255,7 @@ func startProxyWithAlerts(
|
||||
}
|
||||
|
||||
out := &output{}
|
||||
processLog := requestlog.NewProcessLogger(out)
|
||||
processLog := requestlog.NewProcessLogger(out, cfg.InstanceName)
|
||||
|
||||
ruleFiles, err := rules.Load(rules.Params{
|
||||
Dir: cfg.RulesDir, Enabled: cfg.RulesEnabled, ProcessLog: processLog,
|
||||
|
||||
@@ -197,15 +197,15 @@ func TestMetricsCountRuleMatchesAndBansForAnAttack(t *testing.T) {
|
||||
|
||||
metrics := s.scrape(scraper)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_rule_matches_total{action="block",rule_id="blocked"}`, 1)
|
||||
`smallwebwaf_rule_matches_total{action="block",instance="app",rule_id="blocked"}`, 1)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_rule_matches_total{action="ban",rule_id="probe"}`, 1)
|
||||
wantMetric(t, metrics, "smallwebwaf_rules_loaded", 2)
|
||||
wantMetric(t, metrics,
|
||||
`smallwebwaf_requests_total{action="rule_blocked",status_class="4xx"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_bans_made_total{cause="attack"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_bans_made_total{cause="limit"}`, 0)
|
||||
wantMetric(t, metrics, "smallwebwaf_permanent_bans", 1)
|
||||
`smallwebwaf_rule_matches_total{action="ban",instance="app",rule_id="probe"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_rules_loaded{instance="app"}`, 2)
|
||||
wantMetric(t, metrics, `smallwebwaf_requests_total{action="rule_blocked",`+
|
||||
`instance="app",status_class="4xx"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_bans_made_total{cause="attack",instance="app"}`, 1)
|
||||
wantMetric(t, metrics, `smallwebwaf_bans_made_total{cause="limit",instance="app"}`, 0)
|
||||
wantMetric(t, metrics, `smallwebwaf_permanent_bans{instance="app"}`, 1)
|
||||
}
|
||||
|
||||
// writeRules writes content as a rule file into a new directory, and
|
||||
|
||||
Reference in New Issue
Block a user