Files
smallwebwaf/docs/request-log.md
T
sneak eff99f0b13
check / check (push) Waiting to run
SPEC: each requirement once, lists moved to docs/, repeats and sections dropped
Every requirement the second review found in two to five places is now
stated once, with links from the other places. "Risks the design has to
handle", "Rollout per service" and most of "Architecture" are gone, their
two points of their own moved to "Attack detection" and "Anomaly
thresholds". The request log's fields, the metrics and the image's
contents move to docs/request-log.md, docs/metrics.md and docs/image.md,
each linked from the spec. The off rule names SWWAF_LOOKUP_SOURCE and
SWWAF_WAF_MODE, the reputation bound is given, and the six Core Rule Set
changes are a numbered list.

Model: fable-5-1
2026-10-22 20:43:32 +00:00

6.4 KiB

The request log's fields

smallwebwaf writes one JSON object per line on stdout for every request. The requirements are under "Request log" in SPEC.md; this file lists the fields. 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}

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. It goes to the app in X-Request-ID.
  • 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 requests in the minute, hour and day as rate limiting counts them, this request included, as minute, hour and day; its bytes as minute_bytes, hour_bytes and day_bytes. A count can have a fraction, since each window is counted in two buckets with the earlier one weighted. counts is left out when rate limiting did not count the request.
  • limit_hit: the limit broken: minute, hour or day for a rate limit; minute_bytes, hour_bytes or day_bytes for a byte limit; or 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: the kind of offence recorded, when one was.
  • 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.

Every line

Every line has type, the standard web log fields, request_id, peer_ip, client_group, asn, as_name, country, action and duration_total. The process's own messages share the stream with "type":"process"; request lines carry "type":"request".

Sending the log to a syslog server

While SWWAF_LOG_REMOTE_URL is set, every line on stdout is also sent to that server, as the message of an RFC 5424 syslog record. Over UDP each record is one datagram. Over TCP and TLS each record follows its length in bytes and a space. stdout is unchanged. The record carries:

  • the facility from SWWAF_LOG_REMOTE_FACILITY;
  • the severity informational;
  • the time;
  • the host's name;
  • the app name from SWWAF_LOG_REMOTE_APP_NAME.

Lines wait in a buffer of SWWAF_LOG_REMOTE_BUFFER lines, so a server that is slow or down never holds up a request. When the buffer is full, the oldest line is dropped. A failed connection is tried again a second later, and twice as long after each further failure, up to a minute. A line too long for one UDP datagram is dropped alone. As smallwebwaf stops, it spends at most two seconds sending the lines still waiting, and drops the rest. stdout has carried them.