Request Logging
Every completed HTTP request produces a structured RequestLog entry with 23 fields covering timing, identity, routing, security, and performance. Entries are serialized to JSON using a SIMD-accelerated serializer and written through a bounded async channel to one or more output destinations.
Optional fields are omitted from the JSON object when absent — no null noise in your log aggregator.
Quick Start
Section titled “Quick Start”Add log to any site block to enable access logging to stdout with the default JSON format:
api.example.com { reverse_proxy localhost:8080 log}That is all that is required. Every request to api.example.com will produce one JSON line on stdout.
How It Works
Section titled “How It Works”The logging pipeline is asynchronous and non-blocking. The proxy thread never waits for I/O.
The batch writer runs as a Pingora BackgroundService. It drains the channel in batches of up to 200 entries, flushing either when the batch is full or every 500 ms — whichever comes first. On shutdown, any remaining entries in the batch are flushed before the writer exits.
Backpressure policy: when the channel is full (8 192 pending entries), new entries are dropped and a WARN log is emitted. Proxy latency always wins over logging completeness.
Log Fields
Section titled “Log Fields”Every request produces the following fields. Fields marked optional are omitted from the JSON output when not present.
| JSON key | Type | Required | Description | Example |
|---|---|---|---|---|
timestamp | string (RFC 3339) | yes | When the request arrived, UTC | "2026-04-05T14:23:01.452Z" |
request_id | string | yes | UUID v7 unique to this request (time-sortable) | "01924f5c-7e2a-7d00-b3f4-deadbeef1234" |
method | string | yes | HTTP method | "GET" |
path | string | yes | Request path, without query string | "/api/users" |
query | string | optional | Raw query string if present | "page=1&limit=20" |
host | string | yes | Host header, or :authority for HTTP/2 | "api.example.com" |
status | integer | yes | HTTP response status code | 200 |
response_time_us | integer | yes | Total time from request received to response sent, microseconds | 1234 |
client_ip | string | yes | Client IP address from the direct TCP connection — not X-Forwarded-For | "192.168.1.100" |
user_agent | string | optional | User-Agent request header | "Mozilla/5.0 ..." |
referer | string | optional | Referer request header | "https://example.com" |
bytes_sent | integer | yes | Response body size in bytes | 4096 |
bytes_received | integer | yes | Request body size in bytes | 256 |
tls_version | string | optional | TLS version negotiated; absent for plaintext | "TLSv1.3" |
http_version | string | yes | HTTP protocol version | "HTTP/2" |
is_bot | boolean | yes | Whether the request was classified as a bot | false |
country | string | optional | Two-letter country code from GeoIP lookup | "US" |
upstream_addr | string | yes | Backend address that served this request | "127.0.0.1:8080" |
upstream_response_time_us | integer | yes | Time the upstream took to respond, microseconds | 980 |
cache_status | string | optional | Cache result if applicable | "HIT" or "MISS" |
compression | string | optional | Compression algorithm applied to the response | "gzip" or "br" |
trace_id | string | optional | W3C trace ID for distributed tracing correlation | "4bf92f3577b34da6a3ce929d0e0e4736" |
upstream_error_body | string | optional | First 1 KB of the upstream response body on 5xx errors; absent on successful responses | "connection refused" |
All timing fields use microseconds for sub-millisecond precision. Both response_time_us and upstream_response_time_us are always present; their difference accounts for Dwaar’s processing overhead.
Output Destinations
Section titled “Output Destinations”stdout
Section titled “stdout”The default. Writes one JSON object per line to stdout. Use this when a process supervisor, container runtime, or log shipper (Fluentd, Vector, Loki) consumes stdout.
log# equivalent to:log { output stdout}stderr
Section titled “stderr”Writes JSON lines to stderr instead of stdout. Useful when stdout carries application output and you want to route logs separately at the shell level.
log { output stderr}File with rotation
Section titled “File with rotation”Writes JSON lines to a file. Rotates when the file exceeds max_size. On rotation, the active file is renamed to access.log.1, any existing .1 is shifted to .2, and so on up to the keep limit. The oldest file beyond keep is deleted. Rotation uses atomic POSIX rename — no log lines are lost during the shift.
log { output file /var/log/dwaar/access.log { max_size 100mb keep 5 }}| Option | Default | Description |
|---|---|---|
max_size | none (unbounded) | Rotate when the active file reaches this size. Accepts kb, mb, gb suffixes (case-insensitive) or a plain byte count. |
keep | none | Number of rotated files to retain (.1 through .N). Files beyond this count are deleted. |
After a rotation cycle with keep 3, you will see:
access.log ← current, just startedaccess.log.1 ← previous rotationaccess.log.2 ← one before thataccess.log.3 ← oldest retainedUnix socket
Section titled “Unix socket”Writes JSON lines to a SOCK_STREAM Unix domain socket. This is the preferred transport when a Permanu agent or any structured-log consumer (Vector, Fluent Bit) runs on the same host.
log { output unix /run/dwaar/log.sock}Disconnect resilience: when the socket is unavailable, incoming log lines are buffered in memory up to 1 000 lines. On the next successful connection, buffered lines are flushed before new entries are written. If the buffer fills before the socket recovers, the oldest lines are evicted — proxy latency still wins. Reconnect attempts are rate-limited to once per second.
Configuration
Section titled “Configuration”Directives
Section titled “Directives”| Directive | Context | Description |
|---|---|---|
log | site block | Enable access logging with defaults (stdout, JSON, INFO) |
log { ... } | site block | Enable logging with explicit options |
skip_log | site block, route block | Suppress the access log entry for matching requests |
log_append { ... } | site block | Inject additional fields into every log entry for this site |
log_name <name> | site block | Assign a named logger for per-site log routing |
log block options
Section titled “log block options”log { output <destination> # stdout | stderr | discard | file <path> { ... } | unix <path> format <format> # json (default) | console level <level> # INFO (default) | DEBUG | WARN | ERROR}| Option | Values | Default | Description |
|---|---|---|---|
output | stdout, stderr, discard, file <path>, unix <path> | stdout | Where to send log entries |
format | json, console | json | json emits one JSON object per line; console emits a human-readable text line |
level | DEBUG, INFO, WARN, ERROR | INFO | Minimum log level to emit |
Suppress logging for a route
Section titled “Suppress logging for a route”Use skip_log inside a route block to silence health-check endpoints or other high-frequency, low-value paths:
api.example.com { reverse_proxy localhost:8080 log
route /health { skip_log respond "ok" 200 }}Append custom fields
Section titled “Append custom fields”Use log_append to inject request-scoped fields into every log entry for a site. Values are evaluated as templates at request time.
app.example.com { reverse_proxy localhost:3000 log
log_append { tenant {http.request.header.X-Tenant-ID} region eu-west-1 }}Per-Site Logging
Section titled “Per-Site Logging”Each site block configures its own logger independently. Sites with no log directive produce no access logs. Sites with different outputs write to their respective destinations simultaneously.
api.example.com { reverse_proxy localhost:8080 log { output unix /run/dwaar/api.sock level DEBUG }}
static.example.com { file_server /var/www log { output file /var/log/dwaar/static.log { max_size 50mb keep 3 } }}
internal.example.com { reverse_proxy localhost:9000 # No log directive — no access log for this site.}Complete Example
Section titled “Complete Example”A realistic multi-site Dwaarfile with distinct log configurations per domain:
{ admin localhost:2019}
# Public API — structured logs to a Unix socket consumed by Vectorapi.example.com { reverse_proxy localhost:8080 localhost:8081 { lb_policy round_robin health_uri /health }
log { output unix /run/dwaar/api-logs.sock format json level INFO }
log_append { service api-gateway env production }
route /health { skip_log respond "ok" 200 }}
# Marketing site — file logs with rotation, human-readable format for tailingwww.example.com { file_server /var/www/html
log { output file /var/log/dwaar/www.log { max_size 100mb keep 7 } format console level INFO }}
# Admin panel — verbose debug logs to stdout, captured by journaldadmin.example.com { reverse_proxy localhost:4000
tls { client_auth require }
log { output stdout format json level DEBUG }}
# Metrics scrape endpoint — no logging to avoid noisemetrics.internal { metrics /metrics skip_log}Related
Section titled “Related”- Analytics — aggregated traffic metrics built on top of request logs
- Prometheus — per-route request counters and latency histograms
- Tracing — distributed trace propagation and
trace_idcorrelation