# The request log's fields The fields of the JSON object `smallwebwaf` writes for each request. "Request log" in [`SPEC.md`](../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. `smallwebwaf rproxy` writes a shorter line, which "`rproxy`'s line" below gives. ## 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 but `rproxy`'s 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`: a size limit was passed, or a header limit in `servewaf`; - `timed_out`: a 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`](../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`](../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 `servewaf` having read the question's body to the line. - `time`: when `servewaf` had read the question's body. 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 as the `decision` line gives it. 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. ## `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 id `rproxy` made for the request when it arrived. - `client_ip`: the visitor's address. - `host` and `method`. - `path` and `query`: as the visitor sent them. - `status`: what the visitor got, `0` if nothing. - `request_bytes`: the bytes of the visitor's body that `rproxy` read, 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 for `servewaf`. - `duration_response`: milliseconds from then until the end of the answer. - `disposition`: what was done with the request: `allow`, `deny` or `close`. It is `servewaf`'s answer, or the value of `SWRP_DEFAULT_DISPOSITION`. It is left out when: - `servewaf` was not asked, as for the health check; - the visitor went away while `servewaf` was being asked; - `servewaf`'s answer had no valid `X-Smallwebwaf-Disposition`; - `rproxy` could not pass on `servewaf`'s answer, and answered `502`. - `default_disposition`: `true` when `SWRP_DEFAULT_DISPOSITION` gave `disposition`, 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"} ```