Files
smallwebwaf/docs/request-log.md
T
sneak 4a2d57dca2
check / check (push) Successful in 4m23s
servewaf answers rproxy's question about each request and takes its report (closes #170)
`smallwebwaf servewaf` runs today's checks on the visitor's request each
question carries, answers rproxy, and passes nothing to an app. rproxy reports
every request servewaf answered. A report counts an allowed request's bytes and
writes servewaf's second log line. The table holds a copy of what a report
needs, never the request, so a report cannot reach a request being handled.

Judgement call: a question's body is read before any check; over 64 KiB gets 400.
Judgement call: a report's bytes and durations have ceilings, so they cannot overflow.
Judgement call: a `close` disposition for SWWAF_BAN_RESPONSE=close.

Model: opus-5-5
2026-11-04 21:10:03 +00:00

7.9 KiB

The request log's fields

The fields of the JSON object smallwebwaf writes for each request. "Request log" in SPEC.md says when a line is written and how the log is sent to a syslog server. A field that does not apply to a request is left out.

Example

{"type":"request","time":"2026-10-03T12:00:00.123Z","instance":"fsn1app1/gitea","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"response_content_type":"text/html; charset=utf-8","upstream_status":200,"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"duration_total":3.217,"duration_checks":0.041,"duration_upstream_connect":0.052,"duration_upstream_first_byte":2.874,"duration_upstream_total":3.104}

Every line

Every line has:

  • type: request (the process's own messages have process);
  • the standard web log fields;
  • request_id, peer_ip and client_group;
  • asn, as_name and country;
  • action and duration_total.

Standard web log fields

  • time: when the request arrived, RFC 3339 with milliseconds, in UTC;
  • instance: SWWAF_INSTANCE_NAME;
  • client_ip, method, scheme, host, path, query, protocol;
  • status: what the client was sent, 0 if nothing;
  • request_bytes and response_bytes: body bytes each way;
  • referer and user_agent.

Request detail

  • request_id: the X-Request-ID a trusted proxy sent, or a new random one of 26 letters and digits.
  • peer_ip: the TCP peer, normally traefik.
  • forwarded_for: the X-Forwarded-For header as received.
  • client_group: the client as rate limiting counts it, an IPv4 address as a /32 or an IPv6 group.
  • asn, as_name and country: the client's, as looked up. Each is empty when:
    • lookups are off;
    • the client is not looked up;
    • the source cannot place the client;
    • GeoJS had not answered by the time the request went on.
  • content_type and content_length: the request's own.
  • request_headers: the headers listed in SWWAF_LOG_REQUEST_HEADERS, by name in lower case.
  • has_authorization and has_cookie: true when the request had those headers. Their values are never logged.
  • websocket: true when the app switched the connection to another protocol.

Response detail

  • upstream_status: what the app answered. It differs from status when smallwebwaf answered itself.
  • response_content_type, cache_control and location: the answer's headers.
  • aborted: true when the client went away early.

Decision

  • action: what was done:

    • forward: passed to the app;
    • denied: refused for SWWAF_DENY_NETS, or for a reputation source with the action deny;
    • banned: refused under a ban, or for a ban rule, a trap path or a CrowdSec decision, each of which bans the client;
    • country_denied, rate_limited, rule_blocked and waf_blocked: refused by that check;
    • too_large and timed_out: a size or time limit was passed;
    • upstream_error: the app could not be reached, or its answer broke off;
    • admin: answered at one of smallwebwaf's own endpoints.
  • would_action: in observe mode, what enforce mode would have done with a request it would have refused. action then names what was done, normally forward.

  • limit_percent and limit_percent_setting: the client's percentage of the rate limits and the setting that lowered it, when one did. bytes_percent and bytes_percent_setting do the same for the byte limits.

  • counts: the client's totals as rate limiting counts them, this request included. It is left out when rate limiting did not count the request. It holds:

    • minute, hour and day: requests;
    • minute_bytes, hour_bytes and day_bytes: bytes.

    A count can have a fraction (see "Counting method" in SPEC.md).

  • limit_hit: the limit broken:

    • minute, hour or day for a rate limit;
    • minute_bytes, hour_bytes or day_bytes for a byte limit;
    • error_burst.

    A request whose bytes broke a limit was passed on whole, so its action stays forward.

  • rule_ids: the rule file rules that matched, in order, up to the one that refused.

  • waf_score and waf_rule_ids: the Core Rule Set's score and the rules that matched, for a request it inspected.

  • reputation: the sources the client is on, in this order:

    • blocklist URLs;
    • the CrowdSec decision list's URL;
    • DNS blocklist zones;
    • abuseipdb.
  • offence: limit, for a request that broke a limit.

  • ban_expires: when the client's ban ends, for a request that made one or was refused under one: a time, or permanent.

Timings

All in milliseconds:

  • duration_total: from the request's headers being read to its log line;
  • duration_checks: to the end of the checks, before forwarding;
  • duration_waf: the part of that the Core Rule Set took;
  • duration_upstream_connect: from the hand-over to the app until there was a connection;
  • duration_upstream_first_byte: until the first byte of the answer;
  • duration_upstream_total: until the end of the answer.

servewaf's lines

smallwebwaf servewaf writes two lines for each question it answers, joined by request_id. The field servewaf_line says which of the two a line is:

  • decision: the line it writes once it has answered the question;
  • report: the line it writes when rproxy's report comes, with everything known. The repetition is intended.

A request to its own endpoints writes one line, without servewaf_line. A report writes no line but the report line of the request it names.

These questions write no line:

  • a question refused with 400 (see "The question" under "servewaf" in SPEC.md);
  • a question whose body stopped arriving, which gets no answer.

The decision line

It has the fields above, with these meanings where they differ:

  • client_ip: X-Smallwebwaf-Client.
  • peer_ip: X-Smallwebwaf-Peer.
  • host: the site the visitor asked for.
  • protocol: the question's own, HTTP/1.1.
  • content_length: X-Smallwebwaf-Content-Length.
  • request_id: the question's X-Request-ID.
  • status: what servewaf answered: 200 for allow, the status of deny, or 0 for close, as the visitor is then sent nothing.
  • request_bytes: how much of the question's body the Core Rule Set read.
  • response_bytes: the body of servewaf's answer.
  • action: forward for allow, and admin for the 404 for a path under /_smallwebwaf/.
  • aborted: true when rproxy went away before the answer.
  • duration_total: from the question's headers being read to the line.

The line leaves out upstream_status, websocket and the duration_upstream_... fields, since servewaf passes nothing to the app.

The bytes of an allowed request are counted when its report comes. So counts holds the client's bytes without this request's.

The report line

It has every field of the decision line, and time stays when the request arrived. These fields say what the report says:

  • status: what the visitor got, 0 if nothing;
  • request_bytes and response_bytes: the body bytes each way, with what a WebSocket carried each way;
  • duration_total: from the request's headers being read by rproxy to the end of the answer;
  • duration_upstream_total: from the hand-over to the app to the end of the answer, for an allowed request.

For an allowed request, the report's counting also changes these fields:

  • counts: the byte totals now include this request's;
  • limit_hit, offence and ban_expires: set when the report's bytes broke a byte limit.