Log the rest of the request log's fields (closes #79)
check / check (push) Successful in 3m45s

Each request log line now has the fields "Request log" in SPEC.md lists
whose features are built: instance (SWWAF_INSTANCE_NAME), scheme,
request_id (a trusted proxy's X-Request-ID or a new one, sent on to the
app), forwarded_for, client_group, content_type, content_length, the
headers SWWAF_LOG_REQUEST_HEADERS names, has_authorization, has_cookie,
websocket, response_content_type, cache_control, location, counts,
duration_checks, duration_upstream_connect and
duration_upstream_first_byte. A field that does not apply is left out.
Authorization, Cookie and Set-Cookie values are never logged.

Deviation: counts has request totals only; byte totals come with the byte limits.
Deviation: SWWAF_INSTANCE_NAME is on request lines only, not yet on process lines or metrics.
Judgement call: the headers go under request_headers, a name SPEC.md does not give.

Model: opus-5-5
This commit is contained in:
2026-10-06 11:54:56 +00:00
parent cff385af41
commit f61d821215
15 changed files with 818 additions and 120 deletions
+39
View File
@@ -6,6 +6,7 @@ import (
"log/slog"
"maps"
"net/netip"
"os"
"slices"
"strings"
"testing"
@@ -47,8 +48,14 @@ const (
stateCounterInterval = "SWWAF_STATE_COUNTER_INTERVAL"
metricsToken = "SWWAF_METRICS_TOKEN" //nolint:gosec // the setting's name
metricsTopN = "SWWAF_METRICS_TOP_N"
instanceName = "SWWAF_INSTANCE_NAME"
logRequestHeaders = "SWWAF_LOG_REQUEST_HEADERS"
)
// defaultLogRequestHeaders is the default of SWWAF_LOG_REQUEST_HEADERS.
const defaultLogRequestHeaders = "accept,accept-language,accept-encoding," +
"content-type,origin,range"
// token is a token of 32 characters, the shortest allowed.
const token = "0123456789abcdef0123456789abcdef"
@@ -120,6 +127,18 @@ func TestDefaults(t *testing.T) {
wantNetblocks(t, cfg.DenyNets)
wantCountries(t, deniedCountries, cfg.DeniedCountries)
wantCountries(t, allowedCountries, cfg.ExclusivelyAllowedCountries)
hostname, err := os.Hostname()
if err != nil || hostname == "" || cfg.InstanceName != hostname {
t.Errorf("%s is %q, want the host's name %q (%v)", instanceName,
cfg.InstanceName, hostname, err)
}
wantHeaders := strings.Split(defaultLogRequestHeaders, ",")
if !slices.Equal(cfg.LogRequestHeaders, wantHeaders) {
t.Errorf("%s gave %v, want %v", logRequestHeaders, cfg.LogRequestHeaders,
wantHeaders)
}
}
func TestValuesAsSet(t *testing.T) {
@@ -198,6 +217,21 @@ func TestValuesAsSet(t *testing.T) {
wantCountries(t, allowedCountries, cfg.ExclusivelyAllowedCountries, "DE")
}
func TestInstanceNameAndLoggedHeadersAsSet(t *testing.T) {
t.Parallel()
cfg := fromEnvironment(t, environment{
instanceName: "fsn1app1/gitea",
logRequestHeaders: " Accept , X-Custom",
})
if cfg.InstanceName != "fsn1app1/gitea" ||
!slices.Equal(cfg.LogRequestHeaders, []string{"accept", "x-custom"}) {
t.Errorf("%s is %q and %s gives %v", instanceName, cfg.InstanceName,
logRequestHeaders, cfg.LogRequestHeaders)
}
}
func TestCodeOnBothCountryListsStopsTheStart(t *testing.T) {
t.Parallel()
@@ -361,6 +395,7 @@ func TestInvalidValueStopsTheStart(t *testing.T) {
{stateWriteDelay, off}, {stateWriteDelay, "0s"},
{stateCounterInterval, off}, {stateCounterInterval, "15"},
{metricsTopN, off}, {metricsTopN, "0"}, {metricsTopN, "-1"},
{logRequestHeaders, "accept,,origin"},
} {
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel()
@@ -428,6 +463,8 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
t.Fatalf("decode %s: %v", out.Bytes(), err)
}
hostname, _ := os.Hostname()
want := map[string]string{
listenAddr: ":8080",
upstreamURL: "http://127.0.0.1:8081",
@@ -460,6 +497,8 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
stateCounterInterval: "15m",
metricsToken: "",
metricsTopN: "50",
instanceName: hostname,
logRequestHeaders: defaultLogRequestHeaders,
}
if !maps.Equal(line.Settings, want) {
t.Errorf("logged settings\n%v\nwant\n%v", line.Settings, want)