Log the rest of the request log's fields (closes #79)
check / check (push) Successful in 3m24s
check / check (push) Successful in 3m24s
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 and the timings. Authorization, Cookie and Set-Cookie values are never logged. An entry of SWWAF_LOG_REQUEST_HEADERS that is not a header name, or is Host or Transfer-Encoding, stops the start. Deviation: counts has request totals only. Deviation: SWWAF_INSTANCE_NAME is on request lines only. Model: opus-5-5
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||
)
|
||||
|
||||
// The action a request line names: what smallwebwaf did with the
|
||||
@@ -45,32 +47,71 @@ const OffenceLimit = "limit"
|
||||
// timeLayout is RFC 3339 with milliseconds.
|
||||
const timeLayout = "2006-01-02T15:04:05.000Z07:00"
|
||||
|
||||
// Line is one request's line in the request log. The field names are
|
||||
// those of the "Request log" section of SPEC.md.
|
||||
// Line is one request's line in the request log. The field names, and
|
||||
// their order, are those of the "Request log" section of SPEC.md. A field
|
||||
// that may not apply to a request is left out of its line when it does
|
||||
// not.
|
||||
//
|
||||
//nolint:tagliatelle // SPEC.md's request log names its fields in snake_case
|
||||
type Line struct {
|
||||
Type string `json:"type"`
|
||||
Time string `json:"time"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
PeerIP string `json:"peer_ip"`
|
||||
Country string `json:"country"`
|
||||
Method string `json:"method"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Query string `json:"query"`
|
||||
Protocol string `json:"protocol"`
|
||||
Status int `json:"status"`
|
||||
UpstreamStatus int `json:"upstream_status,omitempty"`
|
||||
RequestBytes int64 `json:"request_bytes"`
|
||||
ResponseBytes int64 `json:"response_bytes"`
|
||||
Referer string `json:"referer"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Action string `json:"action"`
|
||||
Type string `json:"type"`
|
||||
|
||||
// The standard web log fields. Scheme is how the client reached
|
||||
// smallwebwaf, or the trusted proxy in front of it.
|
||||
Time string `json:"time"`
|
||||
Instance string `json:"instance"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
Method string `json:"method"`
|
||||
Scheme string `json:"scheme"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Query string `json:"query"`
|
||||
Protocol string `json:"protocol"`
|
||||
Status int `json:"status"`
|
||||
RequestBytes int64 `json:"request_bytes"`
|
||||
ResponseBytes int64 `json:"response_bytes"`
|
||||
Referer string `json:"referer"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
|
||||
// Request detail. RequestID is the X-Request-ID a trusted proxy sent,
|
||||
// or a new one, and is sent on to the app. ForwardedFor is the
|
||||
// X-Forwarded-For header as received. ClientGroup is the netblock the
|
||||
// client is counted as.
|
||||
RequestID string `json:"request_id"`
|
||||
PeerIP string `json:"peer_ip"`
|
||||
ForwardedFor string `json:"forwarded_for,omitempty"`
|
||||
ClientGroup string `json:"client_group"`
|
||||
Country string `json:"country"`
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
// ContentLength is the length of its body the request announced.
|
||||
ContentLength int64 `json:"content_length,omitempty"`
|
||||
// RequestHeaders are the headers SWWAF_LOG_REQUEST_HEADERS names that
|
||||
// the request carried, by name in lower case.
|
||||
RequestHeaders map[string]string `json:"request_headers,omitempty"`
|
||||
HasAuthorization bool `json:"has_authorization,omitempty"`
|
||||
HasCookie bool `json:"has_cookie,omitempty"`
|
||||
// Websocket is true when the connection was upgraded, as for a
|
||||
// WebSocket.
|
||||
Websocket bool `json:"websocket,omitempty"`
|
||||
|
||||
// Response detail, from the headers of the answer: the app's, as
|
||||
// passed on, or those of smallwebwaf's own. Aborted is true when the
|
||||
// client went away early.
|
||||
ResponseContentType string `json:"response_content_type,omitempty"`
|
||||
UpstreamStatus int `json:"upstream_status,omitempty"`
|
||||
CacheControl string `json:"cache_control,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Aborted bool `json:"aborted,omitempty"`
|
||||
|
||||
// The decision.
|
||||
Action string `json:"action"`
|
||||
// WouldAction is, in observe mode, the action enforce mode would have
|
||||
// taken with a request it would have refused: ActionDenied,
|
||||
// ActionBanned, ActionCountryDenied or ActionRateLimited.
|
||||
WouldAction string `json:"would_action,omitempty"`
|
||||
// Counts are the client's requests as the rate limits counted them
|
||||
// with this one, for a request they counted.
|
||||
Counts ratelimit.Counts `json:"counts,omitzero"`
|
||||
// LimitHit is the window whose rate limit the request went over:
|
||||
// minute, hour or day.
|
||||
LimitHit string `json:"limit_hit,omitempty"`
|
||||
@@ -79,11 +120,18 @@ type Line struct {
|
||||
// BanExpires is when the ban the request made, or was refused under,
|
||||
// ends: a time, or "permanent".
|
||||
BanExpires string `json:"ban_expires,omitempty"`
|
||||
// Aborted is true when the client went away early.
|
||||
Aborted bool `json:"aborted,omitempty"`
|
||||
// DurationTotal and DurationUpstreamTotal are in milliseconds.
|
||||
DurationTotal float64 `json:"duration_total"`
|
||||
DurationUpstreamTotal float64 `json:"duration_upstream_total,omitempty"`
|
||||
|
||||
// The timings, in milliseconds. DurationChecks is the time until the
|
||||
// checks were done. DurationUpstreamConnect, DurationUpstreamFirstByte
|
||||
// and DurationUpstreamTotal run from when the request was handed to the
|
||||
// app: until there was a connection to it, until the first byte of its
|
||||
// answer arrived, and until the end. Each but DurationTotal is nil for
|
||||
// a request that did not get that far.
|
||||
DurationTotal float64 `json:"duration_total"`
|
||||
DurationChecks *float64 `json:"duration_checks,omitempty"`
|
||||
DurationUpstreamConnect *float64 `json:"duration_upstream_connect,omitempty"`
|
||||
DurationUpstreamFirstByte *float64 `json:"duration_upstream_first_byte,omitempty"`
|
||||
DurationUpstreamTotal *float64 `json:"duration_upstream_total,omitempty"`
|
||||
}
|
||||
|
||||
// Write writes line to w as one JSON line marked "type":"request".
|
||||
|
||||
@@ -50,7 +50,11 @@ func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
unset := []string{
|
||||
"upstream_status", "limit_hit", "offence", "ban_expires", "aborted",
|
||||
"forwarded_for", "content_type", "content_length", "request_headers",
|
||||
"has_authorization", "has_cookie", "websocket", "response_content_type",
|
||||
"upstream_status", "cache_control", "location", "aborted", "counts",
|
||||
"limit_hit", "offence", "ban_expires", "duration_checks",
|
||||
"duration_upstream_connect", "duration_upstream_first_byte",
|
||||
"duration_upstream_total",
|
||||
}
|
||||
for _, name := range unset {
|
||||
|
||||
Reference in New Issue
Block a user