Skip to content

%REQ_ALL_HEADERS% / %RESP_ALL_HEADERS% access log format operators #45244

@willianpaixao

Description

@willianpaixao

Title

formatter: add REQ_ALL_HEADERS / RESP_ALL_HEADERS operators to serialize all HTTP headers

Description

Problem

Envoy's access log formatting currently requires each HTTP header to be individually named via %REQ(header-name)% / %RESP(header-name)%. This means operators must know in advance which headers are relevant and update their access log configuration every time a new header becomes interesting.

This creates two real-world pain points:

  1. Security observability gaps. Unexpected or malicious headers (e.g. spring.cloud.function.routing-expression exploited during Log4Shell, or novel exfiltration headers) are silently dropped from logs because they were never enumerated in configuration. Threat detection pipelines cannot flag what they cannot see.

  2. Operational overhead. Adding a single header to access logs requires a config change, validation, and a rolling redeploy across the fleet. For large deployments this is a meaningful operational cost just to start logging a header that was already on the wire.

Proposed solution

Add two new access log command operators:

  • %REQ_ALL_HEADERS% -- serializes all HTTP request headers as a JSON object
  • %RESP_ALL_HEADERS% -- same for response headers

When used with json_format, the output is a nested JSON object (not a flat string), allowing downstream log consumers (SIEM, Elasticsearch, etc.) to query individual headers without additional deserialization.

Design choices

Decision Rationale
Ship as an extension formatter (envoy.formatter.all_headers), not a core operator Opt-in, requires explicit configuration. Directly addresses security concern about accidentally logging sensitive data. Follows the req_without_query precedent.
exclude_headers config field (repeated string, case-insensitive) Operators control what is redacted. Documentation recommends excluding authorization, cookie, set-cookie.
max_value_bytes config field (uint32, 0 = unlimited) Bounds per-value log size, preventing multi-MB log lines from large header values.
Comma-join for repeated headers RFC 7230 compliant. Simpler than emitting arrays. set-cookie caveat documented.
Include pseudo-headers (:method, :path, :authority, :status) They are naturally present in the HeaderMap and useful for observability.
Return Protobuf::Struct from formatValue() Produces a nested JSON object in json_format logs, same pattern as DYNAMIC_METADATA.

Example configuration

access_log:
  - name: envoy.access_loggers.file
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
      path: /dev/stdout
      log_format:
        json_format:
          timestamp: "%START_TIME%"
          status: "%RESPONSE_CODE%"
          request_headers: "%REQ_ALL_HEADERS%"
          response_headers: "%RESP_ALL_HEADERS%"
        formatters:
          - name: envoy.formatter.all_headers
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.formatter.all_headers.v3.AllHeaders
              max_value_bytes: 256
              exclude_headers:
                - authorization
                - cookie
                - set-cookie

Example output (JSON format)

{
  "timestamp": "2026-05-25T12:00:00.000Z",
  "status": 200,
  "request_headers": {
    ":method": "GET",
    ":path": "/api/v1/resource",
    ":authority": "example.com",
    "user-agent": "curl/8.0",
    "x-request-id": "abc-123",
    "accept": "text/html,application/json"
  },
  "response_headers": {
    ":status": "200",
    "content-type": "application/json",
    "x-envoy-upstream-service-time": "12"
  }
}

Related issues

Directly related:

Header redaction / obfuscation (key maintainer concern):

Performance considerations

HeaderMap::iterate() is O(n headers) and involves string copies into a protobuf struct. This is non-trivial on high-throughput paths. Recommended mitigations:

  • Pair this operator with an access log filter (e.g. sample-based, or only when %RESPONSE_FLAGS% != -) rather than using it on every request.
  • Use max_value_bytes to bound per-value size.
  • The extension-formatter approach makes this fully opt-in.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementFeature requests. Not bugs or questions.formatter

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions