Each check servewaf runs on a request is a type in its own file behind one interface, heuristic, and is registered in one of four ordered lists in heuristics.go. Order and behaviour are unchanged. Every refusal now logs heuristic, the name of the heuristic that made it, and heuristic_figures, the figures it acted on. A ban by refusal_limit or byte_limits after the answer logs after_answer_heuristic and after_answer_heuristic_figures instead, so a line can name both. All four are in docs/request-log.md, and SPEC.md names each heuristic at its step. Model: opus-5-5
11 KiB
The request log's fields
The fields of the JSON objects rproxy and servewaf write about each request.
"Request log" and "The log" in SPEC.md say 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.
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 whenrproxy'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" inSPEC.md); - a question whose body stopped arriving, which gets no answer.
A report line:
{"type":"request","servewaf_line":"report","time":"2026-10-03T12:00:00.123Z","instance":"fsn1/servewaf","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":"*/*"},"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"waf_score":0,"duration_total":3.217,"duration_checks":0.041,"duration_waf":0.029,"duration_upstream_total":3.104}
Every line
Every one of these lines has:
type:request(the process's own messages haveprocess);- the standard web log fields;
request_id,peer_ipandclient_group;asn,as_nameandcountry;actionandduration_total.
Standard web log fields
time: whenservewafhad read the question's body, RFC 3339 with milliseconds, in UTC. Thereportline gives the same time as thedecisionline.instance:SWWAF_INSTANCE_NAME.client_ip: the visitor's address, asrproxyworked it out.method,pathandquery: the visitor's request.scheme: how the visitor reachedrproxyor the trusted proxy in front of it.host: the site the visitor asked for.protocol: the question's own,HTTP/1.1.status:- on the
decisionline, whatservewafanswered:200forallow, the status ofdeny, or0forclose, as the visitor is then sent nothing; - on the
reportline, what the visitor got,0if nothing.
- on the
request_bytes:- on the
decisionline, how much of the question's body the Core Rule Set read; - on the
reportline, the bytes of the visitor's body thatrproxyread, with what a WebSocket carried from the visitor.
- on the
response_bytes:- on the
decisionline, the body ofservewaf's answer; - on the
reportline, the bytes of the answer's body, with what a WebSocket carried from the app.
- on the
refereranduser_agent.
Request detail
request_id: the idrproxymade for the request when it arrived.peer_ip: the address ofrproxy's TCP peer, normally traefik.forwarded_for: theX-Forwarded-Forheader the visitor's request came with.client_group: the client as rate limiting counts it, an IPv4 address as a /32 or an IPv6 group.asn,as_nameandcountry: 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
servewafanswered.
content_type: the visitor'sContent-Type.content_length: the body length the visitor announced.request_headers: the headers listed inSWWAF_LOG_REQUEST_HEADERS, by name in lower case.has_authorizationandhas_cookie: true when the request had those headers. Their values are never logged.
Response detail
response_content_type: the type ofservewaf's own answer.aborted: true whenrproxywent away before the answer.
Decision
-
action: what was done:forward: answeredallow, so thatrproxypasses it to the app;denied: refused forSWWAF_DENY_NETS, or for a reputation source with the actiondeny;banned: refused under a ban, or for abanrule, a trap path or a CrowdSec decision, each of which bans the client;country_denied,rate_limited,rule_blockedandwaf_blocked: refused by that check;too_large: over a header limit;admin: answered at one ofservewaf's own endpoints, or404for a path under/_smallwebwaf/.
-
would_action: inobservemode, whatenforcemode would have done with a request it would have refused.actionthen names what was done, normallyforward. -
heuristic: the heuristic that refused the request, inobservemode too. It is left out when none did. -
heuristic_figures: an object of the figures that heuristic acted on. Each heuristic and its figures:deny_nets:netblock, the entry ofSWWAF_DENY_NETSthe client is in;active_ban:netblock, the ban's netblock, andban_expires;countries:country, andsetting, the country list that refused it;blocklists:lists, the blocklist URLs that list the client;crowdsec:scenario, the decision's scenario, andexpires, when the decision ends;dnsbl:zones, the DNS blocklist zones that list the client, their keys masked;abuseipdb:score, andmin_score,SWWAF_ABUSEIPDB_MIN_SCORE;rate_limits:window,countandlimit;trap_paths:path, the trap path asked for;rule_files:rule_idandrule_action, of the rule that refused it;header_limits:fields,fields_limit,cookiesandcookies_limit;core_rule_set:score, andthreshold,SWWAF_WAF_ANOMALY_THRESHOLD;refusal_limit:window,countandlimit, of the error burst;byte_limits:window,countandlimit, the count in bytes.
-
after_answer_heuristic: the heuristic that banned the client afterservewafanswered the request, inobservemode too:refusal_limitorbyte_limits. -
after_answer_heuristic_figures: an object of the figures that heuristic acted on, as listed above.byte_limitsbans when the report comes, so only thereportline names it. -
limit_percentandlimit_percent_setting: the client's percentage of the rate limits and the setting that lowered it, when one did.bytes_percentandbytes_percent_settingdo 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,hourandday: requests;minute_bytes,hour_bytesandday_bytes: bytes.
A count can have a fraction (see "Counting method" in
SPEC.md). The bytes of an allowed request are counted when its report comes. So on thedecisionline, the bytes are without this request's. -
limit_hit: the limit broken:minute,hourordayfor a rate limit;minute_bytes,hour_bytesorday_bytesfor a byte limit;error_burst.
A request whose bytes broke a limit was passed on whole, so its
actionstaysforward. Only itsreportline gives that limit. -
rule_ids: the rule file rules that matched, in order, up to the one that refused. -
waf_scoreandwaf_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, orpermanent.
Timings
All in milliseconds:
duration_total:- on the
decisionline, fromservewafhaving read the question's body to the line; - on the
reportline, fromrproxyhaving read the request's headers to the end of the answer.
- on the
duration_checks: to the end of the checks;duration_waf: the part of that the Core Rule Set took;duration_upstream_total: on thereportline of an allowed request, from the hand-over to the app to the end of the answer.
Requests to its own endpoints
The line of a request to servewaf's own endpoints has these fields, with these
meanings where they differ:
client_ipandpeer_ip: the request's TCP peer.request_id: a new random one of 26 letters and digits.scheme:http.time: when the request's headers had been read.request_bytes: the bytes read of a ban to add, and otherwise0.action:admin.duration_total: from the request's headers being read to the line.
rproxy's line
smallwebwaf rproxy writes one short line for each request. request_id joins
it to servewaf's two. It has these fields and no others:
type:request.time: when the request arrived, RFC 3339 with milliseconds, in UTC.request_id: the idrproxymade for the request when it arrived.client_ip: the visitor's address.hostandmethod.pathandquery: as the visitor sent them.status: what the visitor got,0if nothing.request_bytes: the bytes of the visitor's body thatrproxyread, with what a WebSocket carried from the visitor.response_bytes: the bytes of the answer's body, with what a WebSocket carried from the app.duration_request: milliseconds from the request's headers being read until the app had been sent the whole request, or the answer started if that was sooner. It includes the wait forservewaf.duration_response: milliseconds from then until the end of the answer.disposition: what was done with the request:allow,denyorclose. It isservewaf's answer, or the value ofSWRP_DEFAULT_DISPOSITION. It is left out when:servewafwas not asked, as for the health check;- the visitor went away while
servewafwas being asked; servewaf's answer had no validX-Smallwebwaf-Disposition;rproxycould not pass onservewaf's answer, and answered502.
default_disposition:truewhenSWRP_DEFAULT_DISPOSITIONgavedisposition, and otherwise left out.
For example:
{"type":"request","time":"2026-10-03T12:00:00.123Z","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","client_ip":"203.0.113.9","host":"app.example","method":"GET","path":"/","query":"","status":200,"request_bytes":0,"response_bytes":5120,"duration_request":1.204,"duration_response":3.104,"disposition":"allow"}