Access Log

Structured access logging per route or group. Log request and response data in JSON or key=value format to stdout, stderr, or a file.

Configuration

{
  "name": "access-log",
  "type": "accessLog",
  "accessLog": {
    "path": "/dev/stdout",
    "json": true,
    "onRequest": {
      "fields": {
        "id": "${id}",
        "method": "${request.method}",
        "path": "${request.path}",
        "clientIp": "${request.clientIp}"
      }
    },
    "onResponse": {
      "fields": {
        "id": "${id}",
        "status": "${response.status}",
        "bytes": "${response.bytes}",
        "duration_ms": "${duration.ms}"
      }
    }
  }
}

All fields

FieldTypeDefaultDescription
pathstringrequiredOutput path: stdout, stderr, /dev/stdout, /dev/stderr, or a file path
jsonboolfalsetrue = JSON objects, false = key=value pairs
onRequestobjectFields logged when the request arrives
onRequest.fieldsmapKey-value pairs with interpolation variables
onResponseobjectFields logged when the response completes
onResponse.fieldsmapKey-value pairs with interpolation variables

All interpolation variables

VariableAvailable inDescriptionExample
${id}bothAuto-generated UUID (same for request and response)550e8400-e29b-41d4-a716-446655440000
${request.method}bothHTTP methodGET
${request.path}bothOriginal URL path (before rewrite)/api/v1/users
${request.host}bothHostname without portapi.example.com
${request.authority}bothFull Host headerapi.example.com:8443
${request.scheme}bothProtocolhttps
${request.clientIp}bothClient IP (respects X-Forwarded-For)10.0.0.1
${request.header.<NAME>}bothAny request header${request.header.Authorization}
${response.status}onResponseHTTP status code200
${response.bytes}onResponseBytes written to client1234
${response.header.<NAME>}onResponseAny response header${response.header.Content-Type}
${duration.ms}onResponseDuration in milliseconds42
${duration.us}onResponseDuration in microseconds42000
${duration.s}onResponseDuration in seconds (float)0.042

Examples

JSON access log to stdout

{
  "name": "json-log",
  "type": "accessLog",
  "accessLog": {
    "path": "stdout",
    "json": true,
    "onResponse": {
      "fields": {
        "id": "${id}",
        "method": "${request.method}",
        "path": "${request.path}",
        "host": "${request.host}",
        "status": "${response.status}",
        "bytes": "${response.bytes}",
        "duration_ms": "${duration.ms}",
        "client_ip": "${request.clientIp}"
      }
    }
  }
}

Output:

{"id":"abc123","method":"GET","path":"/api/v1/users","host":"api.example.com","status":"200","bytes":"1234","duration_ms":"42","client_ip":"10.0.0.1"}

Request + response logging (correlation)

{
  "name": "full-log",
  "type": "accessLog",
  "accessLog": {
    "path": "stdout",
    "json": true,
    "onRequest": {
      "fields": {
        "event": "request",
        "id": "${id}",
        "method": "${request.method}",
        "path": "${request.path}",
        "client_ip": "${request.clientIp}",
        "user_agent": "${request.header.User-Agent}"
      }
    },
    "onResponse": {
      "fields": {
        "event": "response",
        "id": "${id}",
        "status": "${response.status}",
        "bytes": "${response.bytes}",
        "duration_ms": "${duration.ms}",
        "content_type": "${response.header.Content-Type}"
      }
    }
  }
}

The ${id} is the same UUID for both the request and response log entries, so you can correlate them.

Key=value format (logfmt)

{
  "name": "logfmt-log",
  "type": "accessLog",
  "accessLog": {
    "path": "stdout",
    "json": false,
    "onResponse": {
      "fields": {
        "method": "${request.method}",
        "path": "${request.path}",
        "status": "${response.status}",
        "duration_ms": "${duration.ms}"
      }
    }
  }
}

Output:

method=GET path=/api/v1/users status=200 duration_ms=42

Log to a file

{
  "name": "file-log",
  "type": "accessLog",
  "accessLog": {
    "path": "/var/log/vrata/access.log",
    "json": true,
    "onResponse": {
      "fields": {
        "method": "${request.method}",
        "path": "${request.path}",
        "status": "${response.status}",
        "duration_ms": "${duration.ms}"
      }
    }
  }
}

The file is created if it doesn’t exist.

Minimal (response only, essentials)

{
  "name": "minimal-log",
  "type": "accessLog",
  "accessLog": {
    "path": "stdout",
    "json": true,
    "onResponse": {
      "fields": {
        "status": "${response.status}",
        "duration_ms": "${duration.ms}",
        "path": "${request.path}"
      }
    }
  }
}

Include auth headers for audit

{
  "name": "audit-log",
  "type": "accessLog",
  "accessLog": {
    "path": "stdout",
    "json": true,
    "onRequest": {
      "fields": {
        "id": "${id}",
        "method": "${request.method}",
        "path": "${request.path}",
        "user_id": "${request.header.X-User-ID}",
        "tenant": "${request.header.X-Tenant}"
      }
    },
    "onResponse": {
      "fields": {
        "id": "${id}",
        "status": "${response.status}",
        "duration_ms": "${duration.ms}"
      }
    }
  }
}

Works well with JWT middleware’s claimToHeaders — JWT extracts claims into headers, access log captures them.

No access log

Access logging is opt-in. If no route or group references an access log middleware, nothing is logged.