Tap
The tap middleware sends a copy of the requests and responses going through it to Traefik services, as JSON records.
Because the destinations are ordinary Traefik services, they benefit from load balancing, health checks, retries and
serversTransport options like any other service, and they can be anything reachable over HTTP: a message queue
gateway, an audit store, or a sidecar of your own.
Requests and responses are sent as two separate records, each to its own service, so that both streams can be consumed and scaled independently. Set only one of the two to capture a single side.
Sensitive data
Records carry the headers as they go through Traefik, Authorization and Cookie included,
and the bodies too once recordBody is enabled.
Restrict what is captured with recordBody and maxRecordBodySize,
and treat the tap services and their storage as holding production secrets.
Difference with the Mirroring service
The Mirroring service replays
requests to another service, and the replayed requests are handled as real traffic.
The tap middleware sends a description of the exchange, response included, and never replays anything.
Position in the middleware chain
The records describe the request and the response as they are at the position of the tap middleware in the chain.
- Placed first, it records the request as received from the client, and the response as sent to the client.
- Placed last, it records the request as sent to the backend, and the response as received from the backend.
To get both, use two tap middlewares, one first and one last in the chain.
Their records can be matched with the traceId, or with a correlation header listed in requestHeaders.
Configuration Examples¶
http:
middlewares:
test-tap:
tap:
request:
service: audit-requests
path: /records/requests
recordBody: true
maxRecordBodySize: 4096
rejectOnRecordError: true
timeout: 5s
response:
service: audit-responses
path: /records/responses
recordBody: true
maxRecordBodySize: 4096
requestHeaders:
- X-Request-Id
rejectOnRecordError: false
timeout: 5s[http.middlewares]
[http.middlewares.test-tap.tap]
[http.middlewares.test-tap.tap.request]
service = "audit-requests"
path = "/records/requests"
recordBody = true
maxRecordBodySize = 4096
rejectOnRecordError = true
timeout = "5s"
[http.middlewares.test-tap.tap.response]
service = "audit-responses"
path = "/records/responses"
recordBody = true
maxRecordBodySize = 4096
requestHeaders = ["X-Request-Id"]
rejectOnRecordError = false
timeout = "5s"labels:
- "traefik.http.middlewares.test-tap.tap.request.service=audit-requests"
- "traefik.http.middlewares.test-tap.tap.request.path=/records/requests"
- "traefik.http.middlewares.test-tap.tap.request.recordBody=true"
- "traefik.http.middlewares.test-tap.tap.request.maxRecordBodySize=4096"
- "traefik.http.middlewares.test-tap.tap.request.rejectOnRecordError=true"
- "traefik.http.middlewares.test-tap.tap.request.timeout=5s"
- "traefik.http.middlewares.test-tap.tap.response.service=audit-responses"
- "traefik.http.middlewares.test-tap.tap.response.path=/records/responses"
- "traefik.http.middlewares.test-tap.tap.response.recordBody=true"
- "traefik.http.middlewares.test-tap.tap.response.maxRecordBodySize=4096"
- "traefik.http.middlewares.test-tap.tap.response.requestHeaders=X-Request-Id"
- "traefik.http.middlewares.test-tap.tap.response.rejectOnRecordError=false"
- "traefik.http.middlewares.test-tap.tap.response.timeout=5s"{
//...
"Tags" : [
"traefik.http.middlewares.test-tap.tap.request.service=audit-requests",
"traefik.http.middlewares.test-tap.tap.request.path=/records/requests",
"traefik.http.middlewares.test-tap.tap.request.recordBody=true",
"traefik.http.middlewares.test-tap.tap.request.maxRecordBodySize=4096",
"traefik.http.middlewares.test-tap.tap.request.rejectOnRecordError=true",
"traefik.http.middlewares.test-tap.tap.request.timeout=5s",
"traefik.http.middlewares.test-tap.tap.response.service=audit-responses",
"traefik.http.middlewares.test-tap.tap.response.path=/records/responses",
"traefik.http.middlewares.test-tap.tap.response.recordBody=true",
"traefik.http.middlewares.test-tap.tap.response.maxRecordBodySize=4096",
"traefik.http.middlewares.test-tap.tap.response.requestHeaders=X-Request-Id",
"traefik.http.middlewares.test-tap.tap.response.rejectOnRecordError=false",
"traefik.http.middlewares.test-tap.tap.response.timeout=5s"
]
}apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-tap
spec:
tap:
request:
service:
name: audit-requests
port: 80
path: /records/requests
recordBody: true
maxRecordBodySize: 4096
rejectOnRecordError: true
timeout: 5s
response:
service:
name: audit-responses
port: 80
path: /records/responses
recordBody: true
maxRecordBodySize: 4096
requestHeaders:
- X-Request-Id
rejectOnRecordError: false
timeout: 5sConfiguration Options¶
| Field | Description | Default | Required |
|---|---|---|---|
request |
Where and how the request records are sent. When omitted, requests are not recorded. More information here. | No | |
response |
Where and how the response records are sent. When omitted, responses are not recorded. More information here. | No |
At least one of request and response must be set.
request¶
| Field | Description | Default | Required |
|---|---|---|---|
service |
Name of the service the records are sent to. | Yes | |
path |
Path of the requests sending the records to the service. | / | No |
recordBody |
Whether the body is part of the records. | false | No |
maxRecordBodySize |
Maximum number of body bytes in a record, and held in memory. More information here. | With recordBody |
|
rejectOnRecordError |
Rejects the request with a 500 when the record cannot be sent. More information here. |
false | No |
timeout |
Maximum duration allowed to send a record. A value of 0 means no limit. |
10s | No |
response¶
| Field | Description | Default | Required |
|---|---|---|---|
service |
Name of the service the records are sent to. | Yes | |
path |
Path of the requests sending the records to the service. | / | No |
recordBody |
Whether the body is part of the records. | false | No |
maxRecordBodySize |
Maximum number of body bytes in a record, and held in memory. More information here. | With recordBody |
|
requestHeaders |
Request headers described in the records, along with the request line. More information here. | No | |
rejectOnRecordError |
Rejects the request with a 500 when the record cannot be sent. More information here. |
false | No |
timeout |
Maximum duration allowed to send a record. A value of 0 means no limit. |
10s | No |
rejectOnRecordError¶
When the record cannot be sent:
- With
false, the error is logged, and the request is served. - With
true, the request is rejected with a500 Internal Server Error.
With true, the record is sent first:
the request record before the request reaches the backend,
and the response record before the response reaches the client.
Nothing is served unrecorded.
The request and response settings are independent.
On response, true also means:
- The response is held until its record is complete, then the rest of it is streamed.
The record is complete once the status is known, or with
recordBody, once the firstmaxRecordBodySizebytes of the body are captured, or the response ends. WebSocket upgrades are served as usual, and their record is sent as withfalse. - Informational responses, such as
103 Early Hints, are dropped.
In both cases, records are sent inline, within timeout:
the time taken by the tap service adds to the latency of every request.
Keep timeout short, and the tap service close to Traefik.
requestHeaders¶
A response record describes the response alone, which is enough to store it, but not to tell which exchange it
belongs to without fetching the matching request record. requestHeaders adds a request object to the response
records, holding the request line and the listed headers, and nothing else: the body of a request is never part of a
response record.
It is typically used to carry a correlation identifier set by a middleware standing before the tap in the chain.
Headers absent from the request are absent from the record.
The option does not exist on the request records, which already describe the whole request.
maxRecordBodySize¶
maxRecordBodySize is the maximum number of body bytes in a record.
A larger body is cut in the record, which sets bodyTruncated to true.
The backend and the client still get the whole body.
It also bounds the memory used for the body:
only the recorded bytes are held, and the rest of the body is streamed.
It has no default, as no limit fits every route, and must be set with recordBody.
A value of -1 means no limit, and holds the whole body in memory.
Records¶
A record is sent as a POST request to the configured service and path, with a JSON body:
{
"id": "9f8b1c8a3d4e5f60718293a4b5c6d7e8",
"kind": "response",
"time": "2026-08-28T14:02:11.123456789Z",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"response": {
"status": 201,
"headers": {
"Content-Type": ["application/json"]
},
"body": "eyJpZCI6MX0=",
"duration": 14234000
}
}
| Field | Description |
|---|---|
id |
Identifier of the exchange. The request record and the response record of the same exchange share it. |
kind |
Either request or response. |
time |
Time at which the request was received. |
traceId |
Trace the request belongs to, when tracing is enabled. |
request |
Method, URL, host, protocol, remote address, headers and body. Set on request records, and on response records when requestHeaders is set. |
response |
Status, headers, body, and duration in nanoseconds. The duration ends with the response, or when the record is sent with rejectOnRecordError. Set on response records. |
Bodies are part of the records only when recordBody is enabled.
They are base64-encoded, as they are not necessarily valid UTF-8, and carry a bodyTruncated flag when they exceed maxRecordBodySize.
The same information is also carried by two headers, so that a service handling both kinds of records can dispatch them without parsing the payload:
| Header | Description |
|---|---|
X-Tap-Id |
The record id. |
X-Tap-Record |
The record kind. |
A tap service is expected to answer with a status below 400.
Anything else is treated as a failure to record.