Envoy’s access logs aren’t just a record of requests; they’re a real-time, structured stream of the exact network conversations happening at your edge.
Let’s see what that looks like. Imagine a simple Envoy configuration that’s forwarding traffic.
admin:
access_log_path: /dev/stdout
address:
socket_address:
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: cluster_0
http_filters:
- name: envoy.filters.http.router
typed_config: {}
clusters:
- name: cluster_0
connect_timeout: 0.25s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: example.com
port_value: 80
When a request comes in, Envoy can be configured to write a line to its access log. By default, this is often a plain-text format. But we want structured data, specifically JSON, for easier parsing by downstream systems.
To get JSON output, you need to specify the json_format field within the access_log configuration for your listener. This tells Envoy to serialize the access log entry into a JSON object.
# ... within listener_0's http_connection_manager config ...
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /dev/stdout
json_format: {} # This empty object requests default JSON formatting
When a request hits our listener_0 on port 8080 and gets routed to example.com:80, the access log might look like this JSON output on /dev/stdout:
{
"start_time": "2023-10-27T10:00:00.123Z",
"request_id": "123e4567-e89b-12d3-a456-426614174000",
"duration_ms": 50,
"response_flags": "",
"bytes_sent": 1024,
"bytes_received": 256,
"local_address": "192.168.1.10:8080",
"remote_address": "10.0.0.5:54321",
"downstream_local_port": 8080,
"downstream_remote_port": 54321,
"upstream_service_time_ms": 40,
"upstream_host": "tcp://example.com:80",
"method": "GET",
"path": "/some/resource",
"protocol": "HTTP/1.1",
"response_code": 200,
"response_code_details": "via_upstream",
"user_agent": "curl/7.68.0",
"request_headers": {
"x-request-id": "123e4567-e89b-12d3-a456-426614174000",
"host": "example.com",
"user-agent": "curl/7.68.0"
},
"response_headers": {
"content-type": "application/json",
"date": "Fri, 27 Oct 2023 10:00:00 GMT"
}
}
This default JSON includes many useful fields. However, you often need more control. You might want to add specific request headers, or perhaps only log certain fields to reduce log volume. This is where custom_json_format comes in.
custom_json_format allows you to define a template for your JSON output, using Envoy’s built-in formatters. These formatters are essentially key-value pairs where the value is a string that can reference various request and response attributes.
Here’s how you’d configure a custom JSON format:
# ... within listener_0's http_connection_manager config ...
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /dev/stdout
json_format:
request_id: "%REQ(X-REQUEST-ID)%"
method: "%REQ(:METHOD)%"
path: "%REQ(:PATH)%"
status: "%RESPONSE_CODE%"
duration_ms: "%DURATION%"
user_agent: "%REQ(USER-AGENT)%"
upstream_host: "%UPSTREAM_HOST%"
remote_addr: "%REMOTE_ADDRESS%"
In this json_format block, we’re explicitly defining the fields we want.
%REQ(HEADER-NAME)%extracts a request header. Notice:METHODand:PATHare special pseudo-headers for HTTP.%RESPONSE_CODE%gets the HTTP status code.%DURATION%is the total request duration.%UPSTREAM_HOST%shows which upstream cluster was targeted.%REMOTE_ADDRESS%is the client’s IP.
The actual output for the same request would now look like this:
{
"request_id": "123e4567-e89b-12d3-a456-426614174000",
"method": "GET",
"path": "/some/resource",
"status": 200,
"duration_ms": 50,
"user_agent": "curl/7.68.0",
"upstream_host": "tcp://example.com:80",
"remote_addr": "10.0.0.5:54321"
}
This gives you fine-grained control. You can even embed other JSON objects. For instance, to log a subset of request headers:
# ... within listener_0's http_connection_manager config ...
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /dev/stdout
json_format:
request_id: "%REQ(X-REQUEST-ID)%"
method: "%REQ(:METHOD)%"
path: "%REQ(:PATH)%"
status: "%RESPONSE_CODE%"
duration_ms: "%DURATION%"
client_info:
user_agent: "%REQ(USER-AGENT)%"
remote_addr: "%REMOTE_ADDRESS%"
This would produce:
{
"request_id": "123e4567-e89b-12d3-a456-426614174000",
"method": "GET",
"path": "/some/resource",
"status": 200,
"duration_ms": 50,
"client_info": {
"user_agent": "curl/7.68.0",
"remote_addr": "10.0.0.5:54321"
}
}
The power of json_format lies in its extensibility. You can reference almost any aspect of the request and response lifecycle, from connection details to specific headers and timings. The key is understanding the available formatters and how they map to Envoy’s internal data structures.
What most people miss is that json_format can also include literal strings that become JSON keys and values, and that you can combine literal strings with formatters. For example, to ensure a field like envoy_version is always present, you can do:
json_format:
envoy_version: "v1.25.0" # Literal string
request_id: "%REQ(X-REQUEST-ID)%"
method: "%REQ(:METHOD)%"
This allows you to enrich your logs with static metadata about the Envoy instance itself, making it easier to correlate logs across different deployments.
The next step is often to filter which requests get logged at all, using the access_log_filter field within the FileAccessLog configuration.