Simple implementation of an Envoy Tap Filter. Really, thats it.
All that this repo does is shows the “helloworld” of setting up the TAP filter to write request/response to a file and to use the ADMIN interface to dynamically receive the forked metrics. This repo demonstrates both HTTP
and gRPC
message processing
Use this…well, just to understand the basics…i dont’ know how its operationalized beyond mesh frameworks like istio traffic tapping.
This section will briefly cover the following headers, when you would use them and how to add them to your API calls.
You can find the source here
to use, you need golang 1.15+ and envoy installed locally
You can get envoy like this:
docker cp `docker create envoyproxy/envoy-dev:latest`:/usr/local/bin/envoy .
To TAP traffic and write it a file, set server.yaml
to use
http_filters:
- name: envoy.filters.http.tap
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.tap.v3.Tap
common_config:
# admin_config:
# config_id: test_config_id
static_config:
match_config:
http_request_headers_match:
headers:
- name: foo
exact_match: bar
output_config:
streaming: false
sinks:
- format: JSON_BODY_AS_BYTES
file_per_tap:
path_prefix: /tmp/
- name: envoy.filters.http.router
Then run envoy
./envoy -c server.yaml
In a new window, send over a request
curl -v -H "foo: bar" -H "content-type: application/json" -H "user: sal" -d '{"foo":"bar"}' http://localhost:8080/post
What you’ll see in the /tmp/
folder is a file like this with a random text prefixed by _
(eg, /tmp/_473128456949399711.json
)
{
"http_buffered_trace": {
"request": {
"headers": [
{
"key": ":authority",
"value": "localhost:8080"
},
{
"key": ":path",
"value": "/post"
},
{
"key": ":method",
"value": "POST"
},
{
"key": ":scheme",
"value": "http"
},
{
"key": "user-agent",
"value": "curl/7.74.0"
},
{
"key": "accept",
"value": "*/*"
},
{
"key": "foo",
"value": "bar"
},
{
"key": "content-type",
"value": "application/json"
},
{
"key": "user",
"value": "sal"
},
{
"key": "content-length",
"value": "13"
},
{
"key": "x-forwarded-proto",
"value": "http"
},
{
"key": "x-request-id",
"value": "230534ca-ebec-402e-83a8-a9702bf8fd78"
},
{
"key": "x-envoy-expected-rq-timeout-ms",
"value": "15000"
}
],
"body": {
"truncated": false,
"as_bytes": "eyJmb28iOiJiYXIifQ=="
},
"trailers": []
},
"response": {
"headers": [
{
"key": ":status",
"value": "200"
},
{
"key": "date",
"value": "Tue, 23 Nov 2021 21:33:22 GMT"
},
{
"key": "content-type",
"value": "application/json"
},
{
"key": "content-length",
"value": "506"
},
{
"key": "server",
"value": "envoy"
},
{
"key": "access-control-allow-origin",
"value": "*"
},
{
"key": "access-control-allow-credentials",
"value": "true"
},
{
"key": "x-envoy-upstream-service-time",
"value": "26"
}
],
"body": {
"truncated": false,
"as_bytes": "ewogICJhcmdzIjoge30sIAogICJkYXRhIjogIntcImZvb1wiOlwiYmFyXCJ9IiwgCiAgImZpbGVzIjoge30sIAogICJmb3JtIjoge30sIAogICJoZWFkZXJzIjogewogICAgIkFjY2VwdCI6ICIqLyoiLCAKICAgICJDb250ZW50LUxlbmd0aCI6ICIxMyIsIAogICAgIkNvbnRlbnQtVHlwZSI6ICJhcHBsaWNhdGlvbi9qc29uIiwgCiAgICAiRm9vIjogImJhciIsIAogICAgIkhvc3QiOiAibG9jYWxob3N0IiwgCiAgICAiVXNlciI6ICJzYWwiLCAKICAgICJVc2VyLUFnZW50IjogImN1cmwvNy43NC4wIiwgCiAgICAiWC1BbXpuLVRyYWNlLUlkIjogIlJvb3Q9MS02MTlkNWUyMi0xZGMzYTU4OTVkNWM2MzNiNDE3ZDg2ZTIiLCAKICAgICJYLUVudm95LUV4cGVjdGVkLVJxLVRpbWVvdXQtTXMiOiAiMTUwMDAiCiAgfSwgCiAgImpzb24iOiB7CiAgICAiZm9vIjogImJhciIKICB9LCAKICAib3JpZ2luIjogIjcyLjgzLjY3LjE3NCIsIAogICJ1cmwiOiAiaHR0cHM6Ly9sb2NhbGhvc3QvcG9zdCIKfQo="
},
"trailers": []
}
}
}
the response body is b64encoded JSON response from the upstream
If you set streaming: true
, you’ll see streamed segments
{
"http_streamed_trace_segment": {
"trace_id": "11638135731790493088",
"request_headers": {
"headers": [
{
"key": ":authority",
"value": "localhost:8080"
The following sets up TAP but instead of using the Static config, the parameters are set via a remote application and streamed back to that app:
On server.yaml
, first enable the admin config
http_filters:
- name: envoy.filters.http.tap
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.tap.v3.Tap
common_config:
admin_config:
config_id: test_config_id
Then run envoy
./envoy -c server.yaml
Start the remote TAP monitor:
go run main.go
What the tap monitor server does is remotely program and receive TAP responses
programming is done with a simple POST to /tap
endpoint of envoy
c := `
config_id: test_config_id
tap_config:
match_config:
http_request_headers_match:
headers:
- name: foo
exact_match: bar
output_config:
streaming: false
sinks:
- format: JSON_BODY_AS_BYTES
streaming_admin: {}`
body := []byte(c)
resp, err := http.Post("http://localhost:9000/tap", "application/json", bytes.NewReader(body))
if err != nil {
fmt.Printf("%v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Status: [%s]\n", resp.Status)
What the specs for TAP state is that once you program envoy endpoint, any inbound request that fulfils the TAP specs will get streamed back to the listener.
So, in a new window, send over a request
curl -v -H "foo: bar" -H "content-type: application/json" -H "user: sal" -d '{"foo":"bar"}' http://localhost:8080/post
What you should see is the HttpBufferedTrace
in the response (since we set streaming: false
)
$ go run main.go
Status: [200 OK]
{
"http_buffered_trace": {
"request": {
"headers": [
{
"key": ":authority",
"value": "localhost:8080"
},
{
"key": ":path",
"value": "/post"
},
{
"key": ":method",
"value": "POST"
},
{
"key": ":scheme",
"value": "https"
},
{
"key": "user-agent",
"value": "curl/7.74.0"
},
{
"key": "accept",
"value": "*/*"
},
{
"key": "foo",
"value": "bar"
},
{
"key": "content-type",
"value": "application/json"
},
{
"key": "user",
"value": "sal"
},
{
"key": "content-length",
"value": "13"
},
{
"key": "x-forwarded-proto",
"value": "http"
},
{
"key": "x-request-id",
"value": "71292137-0566-4962-ace1-eb2d4ad9120b"
},
{
"key": "x-envoy-expected-rq-timeout-ms",
"value": "15000"
}
],
"body": {
"truncated": false,
"as_bytes": "eyJmb28iOiJiYXIifQ=="
},
"trailers": []
},
"response": {
"headers": [
{
"key": ":status",
"value": "200"
},
{
"key": "date",
"value": "Fri, 04 Jun 2021 19:51:22 GMT"
},
{
"key": "content-type",
"value": "application/json"
},
{
"key": "content-length",
"value": "506"
},
{
"key": "server",
"value": "envoy"
},
{
"key": "access-control-allow-origin",
"value": "*"
},
{
"key": "access-control-allow-credentials",
"value": "true"
},
{
"key": "x-envoy-upstream-service-time",
"value": "19"
}
],
"body": {
"truncated": false,
"as_bytes": "ewogICJhcmdzIjoge30sIAogICJkYXRhIjogIntcImZvb1wiOlwiYmFyXCJ9IiwgCiAgImZpbGVzIjoge30sIAogICJmb3JtIjoge30sIAogICJoZWFkZXJzIjogewogICAgIkFjY2VwdCI6ICIqLyoiLCAKICAgICJDb250ZW50LUxlbmd0aCI6ICIxMyIsIAogICAgIkNvbnRlbnQtVHlwZSI6ICJhcHBsaWNhdGlvbi9qc29uIiwgCiAgICAiRm9vIjogImJhciIsIAogICAgIkhvc3QiOiAibG9jYWxob3N0IiwgCiAgICAiVXNlciI6ICJzYWwiLCAKICAgICJVc2VyLUFnZW50IjogImN1cmwvNy43NC4wIiwgCiAgICAiWC1BbXpuLVRyYWNlLUlkIjogIlJvb3Q9MS02MGJhODQzYS00ZDU4NGExNjMxN2Q2M2RjMzQyOTczOGQiLCAKICAgICJYLUVudm95LUV4cGVjdGVkLVJxLVRpbWVvdXQtTXMiOiAiMTUwMDAiCiAgfSwgCiAgImpzb24iOiB7CiAgICAiZm9vIjogImJhciIKICB9LCAKICAib3JpZ2luIjogIjcyLjgzLjY3LjE3NCIsIAogICJ1cmwiOiAiaHR0cHM6Ly9sb2NhbGhvc3QvcG9zdCIKfQo="
},
"trailers": []
}
}
}
If you want to see the streaming response, set streaming: true
and the output will be like
{
"http_buffered_trace": {
"request": {
"headers": [
{
"key": ":authority",
"value": "localhost:8080"
},
{
"key": ":path",
"value": "/post"
},
{
"key": ":method",
"value": "POST"
},
{
"key": ":scheme",
"value": "https"
One glaring thing i omitted in this repo is how to parse the JSON data back on the admin listener.
It seems envoy returns a new line delimited pretty-printed JSON back!…i have no idea how to effectively parse that in golang…there is certainly away…if you know it, LMK
For gRPC we will run a simple client->envoy->server just like the http sample above. The admin tap application will connect to envoy and receive a copy of a specific, named RPC that traverses envoy. The tap application will then attempt to decode the protobuf that is contained in that specif call.
As above,
docker cp `docker create envoyproxy/envoy-dev:latest`:/usr/local/bin/envoy .
(optional compile protos)
$ protoc --version
libprotoc 3.19.1
protoc -I/usr/local/include -I . --go_out=. \
--descriptor_set_out=src/echo/echo.pb --go_opt=paths=source_relative \
--go-grpc_opt=require_unimplemented_servers=false --go-grpc_out=. \
--go-grpc_opt=paths=source_relative src/echo/echo.proto
go run src/grpc_server.go \
--grpcport 0.0.0.0:50051 \
--tlsCert=certs/grpc_server_crt.pem \
--tlsKey=certs/grpc_server_key.pem
now run and enable the admin TAP listener
./envoy -c basic.yaml
The TAP application will attempt to parse the Unary requests
c := `
config_id: test_config_id
tap_config:
match_config:
http_request_headers_match:
headers:
- name: ":path"
exact_match: "/echo.EchoServer/SayHelloUnary"
output_config:
streaming: false
max_buffered_rx_bytes: 5000
max_buffered_tx_bytes: 5000
sinks:
- format: JSON_BODY_AS_BYTES
streaming_admin: {}`
If you want,you can specify other APIs endpoints or wildcard all endpoints. For the latter, you will have to selectively parse back the messages. In the case of the attached rpcs, it
doesn’t really matter since they are all return echo.EchoReply
. Note, this sample does not support compression Length-Prefixed-Message
in the grpc protocol)
go run parser/main.go
go run src/grpc_client.go \
--host 127.0.0.1:8080 \
--tlsCert=certs/CA_crt.pem \
--servername=grpc.domain.com
What you should see in the TAP client is the parsed protobuf messages that traversed envoy.
As an extended example, you can also parse GCP Pubsub protos:
parser/main.go
to catch /google.pubsub.v1.Publisher/ListTopics
and import the pubsub protobufimport (
pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1"
)
..
pm := pubsubpb.ListTopicsResponse{}
c := `
config_id: test_config_id
tap_config:
match_config:
http_request_headers_match:
headers:
- name: ":path"
exact_match: "/google.pubsub.v1.Publisher/ListTopics"
output_config:
streaming: false
max_buffered_rx_bytes: 5000
max_buffered_tx_bytes: 5000
sinks:
- format: JSON_BODY_AS_BYTES
streaming_admin: {}`
Run Envoy
envoy -c gcp_envoy.yaml
Run parser
go run parser/main.go
Finally run pubsub client
cd grpc/gcp
go run pubsub.go
thats all folks,
Other reference envoy samples
Envoy External Authorization server (envoy.ext_authz) with OPA HelloWorld
gRPC per method observability with envoy, Istio, OpenCensus and GKE
https://pkg.go.dev/github.com/envoyproxy/go-control-plane/envoy/data/tap/v3
https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/tap/v3/tap.proto#extensions-filters-http-tap-v3-tap https://www.envoyproxy.io/docs/envoy/latest/api-v3/data/tap/v3/http.proto
envoy_config_tap_v3 "github.com/envoyproxy/go-control-plane/envoy/config/tap/v3"
envoy_config_tap_v3_pb "github.com/envoyproxy/go-control-plane/envoy/data/tap/v3"
This site supports webmentions. Send me a mention via this form.