mirror of
https://github.com/alibaba/higress.git
synced 2026-04-21 20:17:29 +08:00
feat: Supports recording request header, request body, response header and response body information in the access log (#2265)
This commit is contained in:
154
plugins/wasm-go/extensions/log-request-response/README.md
Normal file
154
plugins/wasm-go/extensions/log-request-response/README.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# log-request-response 插件
|
||||||
|
|
||||||
|
这个插件用于在 Higress 的访问日志中添加以下信息:
|
||||||
|
|
||||||
|
- HTTP 请求头(添加为 `%FILTER_STATE(wasm.log-request-headers:PLAIN)%`)
|
||||||
|
- POST、PUT、PATCH 请求的请求体内容(添加为 `%FILTER_STATE(wasm.log-request-body:PLAIN)%`)
|
||||||
|
- 响应头(添加为 `%FILTER_STATE(wasm.log-response-headers:PLAIN)%`)
|
||||||
|
- 响应体内容(添加为 `%FILTER_STATE(wasm.log-response-body:PLAIN)%`)
|
||||||
|
|
||||||
|
## 配置参数
|
||||||
|
|
||||||
|
在 Higress 控制台配置该插件时,使用以下结构化的 YAML 配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 请求相关配置
|
||||||
|
request:
|
||||||
|
# 请求头配置
|
||||||
|
headers:
|
||||||
|
# 是否记录请求头(默认:false)
|
||||||
|
enabled: true
|
||||||
|
# 请求体配置
|
||||||
|
body:
|
||||||
|
# 是否记录请求体内容(默认:false)
|
||||||
|
enabled: true
|
||||||
|
# 最大记录长度限制,单位字节(默认:10KB)
|
||||||
|
maxSize: 10240
|
||||||
|
# 需要记录请求体的内容类型(默认包含常见的内容类型)
|
||||||
|
contentTypes:
|
||||||
|
- application/json
|
||||||
|
- application/xml
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
- text/plain
|
||||||
|
|
||||||
|
# 响应相关配置
|
||||||
|
response:
|
||||||
|
# 响应头配置
|
||||||
|
headers:
|
||||||
|
# 是否记录响应头(默认:false)
|
||||||
|
enabled: true
|
||||||
|
# 响应体配置
|
||||||
|
body:
|
||||||
|
# 是否记录响应体内容(默认:false)
|
||||||
|
enabled: true
|
||||||
|
# 最大记录长度限制,单位字节(默认:10KB)
|
||||||
|
maxSize: 10240
|
||||||
|
# 需要记录响应体的内容类型(默认包含常见的内容类型)
|
||||||
|
contentTypes:
|
||||||
|
- application/json
|
||||||
|
- application/xml
|
||||||
|
- text/plain
|
||||||
|
- text/html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
1. 请求处理时,插件会根据配置决定是否记录请求头和请求体
|
||||||
|
2. 只有当请求方法为 POST、PUT 或 PATCH,且内容类型在配置的 `request.body.contentTypes` 列表中时,才会记录请求体
|
||||||
|
3. 响应处理时,插件会根据配置决定是否记录响应头和响应体
|
||||||
|
4. 只有当响应的内容类型在配置的 `response.body.contentTypes` 列表中时,才会记录响应体
|
||||||
|
5. 所有记录的内容都会被限制在配置的 `maxSize` 指定的大小内
|
||||||
|
6. 插件对请求体和响应体都使用流式处理方式,不会阻止或修改原始内容传递
|
||||||
|
7. 记录的内容会被存储在 Envoy 的 Filter State 中,可以通过访问日志配置获取
|
||||||
|
|
||||||
|
## 编译方法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 先整理依赖
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# 编译
|
||||||
|
tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags="custommalloc nottinygc_finalizer" ./main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 访问日志配置
|
||||||
|
|
||||||
|
要在 Higress 访问日志中显示插件添加的 Filter State 数据,需要修改 Higress 的访问日志配置。编辑 ConfigMap:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl edit cm -n higress-system higress-config
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `envoyAccessLogService.config.accessLog` 下的 `format` 字段中添加以下内容:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"request_headers": "%FILTER_STATE(wasm.log-request-headers:PLAIN)%",
|
||||||
|
"request_body": "%FILTER_STATE(wasm.log-request-body:PLAIN)%",
|
||||||
|
"response_headers": "%FILTER_STATE(wasm.log-response-headers:PLAIN)%",
|
||||||
|
"response_body": "%FILTER_STATE(wasm.log-response-body:PLAIN)%"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
完整的访问日志配置可能会像这样(添加到现有配置中):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mesh:
|
||||||
|
accessLogFile: "/dev/stdout"
|
||||||
|
accessLogFormat: |
|
||||||
|
{
|
||||||
|
"authority": "%REQ(:AUTHORITY)%",
|
||||||
|
"bytes_received": "%BYTES_RECEIVED%",
|
||||||
|
"bytes_sent": "%BYTES_SENT%",
|
||||||
|
"downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%",
|
||||||
|
"downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
|
||||||
|
"duration": "%DURATION%",
|
||||||
|
"method": "%REQ(:METHOD)%",
|
||||||
|
"path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%",
|
||||||
|
"protocol": "%PROTOCOL%",
|
||||||
|
"request_id": "%REQ(X-REQUEST-ID)%",
|
||||||
|
"requested_server_name": "%REQUESTED_SERVER_NAME%",
|
||||||
|
"response_code": "%RESPONSE_CODE%",
|
||||||
|
"response_flags": "%RESPONSE_FLAGS%",
|
||||||
|
"route_name": "%ROUTE_NAME%",
|
||||||
|
"start_time": "%START_TIME%",
|
||||||
|
"trace_id": "%REQ(X-B3-TRACEID)%",
|
||||||
|
"upstream_cluster": "%UPSTREAM_CLUSTER%",
|
||||||
|
"upstream_host": "%UPSTREAM_HOST%",
|
||||||
|
"upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%",
|
||||||
|
"upstream_service_time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%",
|
||||||
|
"upstream_transport_failure_reason": "%UPSTREAM_TRANSPORT_FAILURE_REASON%",
|
||||||
|
"user_agent": "%REQ(USER-AGENT)%",
|
||||||
|
"x_forwarded_for": "%REQ(X-FORWARDED-FOR)%",
|
||||||
|
"request_headers": "%FILTER_STATE(wasm.log-request-headers:PLAIN)%",
|
||||||
|
"request_body": "%FILTER_STATE(wasm.log-request-body:PLAIN)%",
|
||||||
|
"response_headers": "%FILTER_STATE(wasm.log-response-headers:PLAIN)%",
|
||||||
|
"response_body": "%FILTER_STATE(wasm.log-response-body:PLAIN)%"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 日志输出示例
|
||||||
|
|
||||||
|
配置完成后,Higress 的访问日志中将包含这些额外的字段(取决于您的配置启用了哪些选项):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"authority": "example.com",
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/users",
|
||||||
|
"response_code": 200,
|
||||||
|
"request_headers": "{\"host\":\"example.com\",\"path\":\"/api/users\",\"method\":\"POST\",\"content-type\":\"application/json\"}",
|
||||||
|
"request_body": "{\"name\":\"测试用户\",\"email\":\"test@example.com\"}",
|
||||||
|
"response_headers": "{\"content-type\":\"application/json\",\"status\":\"200\"}",
|
||||||
|
"response_body": "{\"id\":123,\"status\":\"success\"}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 所有日志记录选项默认都是关闭的(false),需要明确启用才会记录相应内容
|
||||||
|
2. 对于大型请求体或响应体,可以通过 `request.body.maxSize` 和 `response.body.maxSize` 参数限制记录的长度,以避免日志过大
|
||||||
|
3. 插件使用流式处理方式处理请求体和响应体,不会对原始内容产生任何影响
|
||||||
|
4. 只有指定内容类型的 POST、PUT、PATCH 请求才会记录请求体内容
|
||||||
|
5. 只有指定内容类型的响应才会记录响应体内容
|
||||||
|
6. 请确保合理配置该插件,避免记录敏感信息到日志中
|
||||||
1
plugins/wasm-go/extensions/log-request-response/VERSION
Normal file
1
plugins/wasm-go/extensions/log-request-response/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.0.0
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
envoy:
|
||||||
|
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.3
|
||||||
|
entrypoint: /usr/local/bin/envoy
|
||||||
|
# 注意这里对wasm开启了debug级别日志,正式部署时则默认info级别
|
||||||
|
command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
|
||||||
|
depends_on:
|
||||||
|
- httpbin
|
||||||
|
networks:
|
||||||
|
- wasmtest
|
||||||
|
ports:
|
||||||
|
- "10000:10000"
|
||||||
|
volumes:
|
||||||
|
- ./envoy.yaml:/etc/envoy/envoy.yaml
|
||||||
|
- ./main.wasm:/etc/envoy/main.wasm
|
||||||
|
|
||||||
|
httpbin:
|
||||||
|
image: kennethreitz/httpbin:latest
|
||||||
|
networks:
|
||||||
|
- wasmtest
|
||||||
|
ports:
|
||||||
|
- "12345:80"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
wasmtest: {}
|
||||||
137
plugins/wasm-go/extensions/log-request-response/envoy.yaml
Normal file
137
plugins/wasm-go/extensions/log-request-response/envoy.yaml
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
admin:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
protocol: TCP
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_value: 9901
|
||||||
|
static_resources:
|
||||||
|
listeners:
|
||||||
|
- name: listener_0
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
protocol: TCP
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_value: 10000
|
||||||
|
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
|
||||||
|
access_log:
|
||||||
|
- name: envoy.access_loggers.file
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
|
||||||
|
path: "/dev/stdout"
|
||||||
|
format: |
|
||||||
|
{
|
||||||
|
"request_headers": "%FILTER_STATE(wasm.log-request-headers:PLAIN)%",
|
||||||
|
"request_body": "%FILTER_STATE(wasm.log-request-body:PLAIN)%",
|
||||||
|
"response_headers": "%FILTER_STATE(wasm.log-response-headers:PLAIN)%",
|
||||||
|
"response_body": "%FILTER_STATE(wasm.log-response-body:PLAIN)%",
|
||||||
|
"ai_log": "%FILTER_STATE(wasm.ai_log:PLAIN)%",
|
||||||
|
"authority": "%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%",
|
||||||
|
"bytes_received": "%BYTES_RECEIVED%",
|
||||||
|
"bytes_sent": "%BYTES_SENT%",
|
||||||
|
"downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%",
|
||||||
|
"downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
|
||||||
|
"duration": "%DURATION%",
|
||||||
|
"istio_policy_status": "%DYNAMIC_METADATA(istio.mixer:status)%",
|
||||||
|
"method": "%REQ(:METHOD)%",
|
||||||
|
"path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%",
|
||||||
|
"protocol": "%PROTOCOL%",
|
||||||
|
"request_id": "%REQ(X-REQUEST-ID)%",
|
||||||
|
"requested_server_name": "%REQUESTED_SERVER_NAME%",
|
||||||
|
"response_code": "%RESPONSE_CODE%",
|
||||||
|
"response_flags": "%RESPONSE_FLAGS%",
|
||||||
|
"route_name": "%ROUTE_NAME%",
|
||||||
|
"start_time": "%START_TIME%",
|
||||||
|
"trace_id": "%REQ(X-B3-TRACEID)%",
|
||||||
|
"upstream_cluster": "%UPSTREAM_CLUSTER%",
|
||||||
|
"upstream_host": "%UPSTREAM_HOST%",
|
||||||
|
"upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%",
|
||||||
|
"upstream_service_time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%",
|
||||||
|
"upstream_transport_failure_reason": "%UPSTREAM_TRANSPORT_FAILURE_REASON%",
|
||||||
|
"user_agent": "%REQ(USER-AGENT)%",
|
||||||
|
"x_forwarded_for": "%REQ(X-FORWARDED-FOR)%",
|
||||||
|
"response_code_details": "%RESPONSE_CODE_DETAILS%"
|
||||||
|
}
|
||||||
|
scheme_header_transformation:
|
||||||
|
scheme_to_overwrite: https
|
||||||
|
stat_prefix: ingress_http
|
||||||
|
route_config:
|
||||||
|
name: local_route
|
||||||
|
virtual_hosts:
|
||||||
|
- name: local_service
|
||||||
|
domains: ["*"]
|
||||||
|
routes:
|
||||||
|
- match:
|
||||||
|
prefix: "/"
|
||||||
|
route:
|
||||||
|
cluster: httpbin
|
||||||
|
http_filters:
|
||||||
|
- name: wasmdemo
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||||
|
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||||
|
value:
|
||||||
|
config:
|
||||||
|
name: wasmdemo
|
||||||
|
vm_config:
|
||||||
|
runtime: envoy.wasm.runtime.v8
|
||||||
|
code:
|
||||||
|
local:
|
||||||
|
filename: /etc/envoy/main.wasm
|
||||||
|
configuration:
|
||||||
|
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||||
|
value: |
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"headers": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"enabled": true,
|
||||||
|
"maxSize": 25,
|
||||||
|
"contentTypes": [
|
||||||
|
"application/json",
|
||||||
|
"application/xml",
|
||||||
|
"application/x-www-form-urlencoded",
|
||||||
|
"text/plain"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"headers": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"enabled": true,
|
||||||
|
"maxSize": 100,
|
||||||
|
"contentTypes": [
|
||||||
|
"application/json",
|
||||||
|
"application/xml",
|
||||||
|
"text/plain",
|
||||||
|
"text/html"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- name: envoy.filters.http.router
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||||
|
clusters:
|
||||||
|
- name: httpbin
|
||||||
|
connect_timeout: 30s
|
||||||
|
type: LOGICAL_DNS
|
||||||
|
# Comment out the following line to test on v6 networks
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: httpbin
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: httpbin
|
||||||
|
port_value: 80
|
||||||
21
plugins/wasm-go/extensions/log-request-response/go.mod
Normal file
21
plugins/wasm-go/extensions/log-request-response/go.mod
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module github.com/alibaba/higress/plugins/wasm-go/extensions/log-request-response
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||||
|
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||||
|
github.com/tidwall/gjson v1.17.3
|
||||||
|
github.com/tidwall/sjson v1.2.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||||
|
github.com/magefile/mage v1.14.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
github.com/tidwall/resp v0.1.1 // indirect
|
||||||
|
)
|
||||||
23
plugins/wasm-go/extensions/log-request-response/go.sum
Normal file
23
plugins/wasm-go/extensions/log-request-response/go.sum
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||||
|
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||||
|
github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU=
|
||||||
|
github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0=
|
||||||
|
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||||
|
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||||
|
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||||
|
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
399
plugins/wasm-go/extensions/log-request-response/main.go
Normal file
399
plugins/wasm-go/extensions/log-request-response/main.go
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||||
|
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||||
|
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||||
|
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for log keys in Filter State
|
||||||
|
const (
|
||||||
|
pluginName = "log-request-response"
|
||||||
|
logKeyRequestHeaders = "log-request-headers"
|
||||||
|
logKeyRequestBody = "log-request-body"
|
||||||
|
logKeyResponseHeaders = "log-response-headers"
|
||||||
|
logKeyResponseBody = "log-response-body"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for context keys
|
||||||
|
const (
|
||||||
|
contextKeyRequestBodyBuffer = "request_body_buffer"
|
||||||
|
contextKeyResponseBodyBuffer = "response_body_buffer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP/2 header name mapping
|
||||||
|
var http2HeaderMap = map[string]string{
|
||||||
|
":authority": "authority",
|
||||||
|
":method": "method",
|
||||||
|
":path": "path",
|
||||||
|
":scheme": "scheme",
|
||||||
|
":status": "status",
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
// Plugin name
|
||||||
|
pluginName,
|
||||||
|
// Set custom function for parsing plugin configuration
|
||||||
|
wrapper.ParseConfig(parseConfig),
|
||||||
|
// Set custom function for processing request headers
|
||||||
|
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||||
|
// Set custom function for processing streaming request body
|
||||||
|
wrapper.ProcessStreamingRequestBody(onStreamingRequestBody),
|
||||||
|
// Set custom function for processing response headers
|
||||||
|
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
|
||||||
|
// Set custom function for processing streaming response body
|
||||||
|
wrapper.ProcessStreamingResponseBody(onStreamingResponseBody),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginConfig Custom plugin configuration
|
||||||
|
type PluginConfig struct {
|
||||||
|
// Request configuration
|
||||||
|
Request struct {
|
||||||
|
// Headers configuration
|
||||||
|
Headers struct {
|
||||||
|
// Whether to enable request headers logging
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
// Body configuration
|
||||||
|
Body struct {
|
||||||
|
// Whether to enable request body logging
|
||||||
|
Enabled bool
|
||||||
|
// Maximum size limit for logging (bytes)
|
||||||
|
MaxSize int
|
||||||
|
// Content types to be logged
|
||||||
|
ContentTypes []string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Response configuration
|
||||||
|
Response struct {
|
||||||
|
// Headers configuration
|
||||||
|
Headers struct {
|
||||||
|
// Whether to enable response headers logging
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
// Body configuration
|
||||||
|
Body struct {
|
||||||
|
// Whether to enable response body logging
|
||||||
|
Enabled bool
|
||||||
|
// Maximum size limit for logging (bytes)
|
||||||
|
MaxSize int
|
||||||
|
// Content types to be logged
|
||||||
|
ContentTypes []string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The YAML configuration filled in the console will be automatically converted to JSON,
|
||||||
|
// so we can directly parse the configuration from this JSON parameter
|
||||||
|
func parseConfig(json gjson.Result, config *PluginConfig) error {
|
||||||
|
// Parse request headers configuration
|
||||||
|
config.Request.Headers.Enabled = json.Get("request.headers.enabled").Bool()
|
||||||
|
|
||||||
|
// Parse request body configuration
|
||||||
|
config.Request.Body.Enabled = json.Get("request.body.enabled").Bool()
|
||||||
|
config.Request.Body.MaxSize = int(json.Get("request.body.maxSize").Int())
|
||||||
|
|
||||||
|
// Set default maximum size for request body
|
||||||
|
if config.Request.Body.MaxSize <= 0 {
|
||||||
|
config.Request.Body.MaxSize = 10 * 1024 // Default 10KB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body content types
|
||||||
|
if contentTypes := json.Get("request.body.contentTypes").Array(); len(contentTypes) > 0 {
|
||||||
|
for _, ct := range contentTypes {
|
||||||
|
config.Request.Body.ContentTypes = append(config.Request.Body.ContentTypes, ct.String())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default content types
|
||||||
|
config.Request.Body.ContentTypes = []string{
|
||||||
|
"application/json",
|
||||||
|
"application/xml",
|
||||||
|
"application/x-www-form-urlencoded",
|
||||||
|
"text/plain",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response headers configuration
|
||||||
|
config.Response.Headers.Enabled = json.Get("response.headers.enabled").Bool()
|
||||||
|
|
||||||
|
// Parse response body configuration
|
||||||
|
config.Response.Body.Enabled = json.Get("response.body.enabled").Bool()
|
||||||
|
config.Response.Body.MaxSize = int(json.Get("response.body.maxSize").Int())
|
||||||
|
|
||||||
|
// Set default maximum size for response body
|
||||||
|
if config.Response.Body.MaxSize <= 0 {
|
||||||
|
config.Response.Body.MaxSize = 10 * 1024 // Default 10KB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response body content types
|
||||||
|
if contentTypes := json.Get("response.body.contentTypes").Array(); len(contentTypes) > 0 {
|
||||||
|
for _, ct := range contentTypes {
|
||||||
|
config.Response.Body.ContentTypes = append(config.Response.Body.ContentTypes, ct.String())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default content types
|
||||||
|
config.Response.Body.ContentTypes = []string{
|
||||||
|
"application/json",
|
||||||
|
"application/xml",
|
||||||
|
"text/plain",
|
||||||
|
"text/html",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeHeaderName standardizes HTTP/2 header names by removing the colon prefix
|
||||||
|
// or mapping them to more standard names
|
||||||
|
func normalizeHeaderName(name string) string {
|
||||||
|
// If it's a known HTTP/2 header, map it to a standard name
|
||||||
|
if standardName, exists := http2HeaderMap[name]; exists {
|
||||||
|
return standardName
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other headers that might start with colon, just remove the colon
|
||||||
|
if strings.HasPrefix(name, ":") {
|
||||||
|
return name[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the original name for regular headers
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// processStreamingBody common function to process streaming body
|
||||||
|
func processStreamingBody(
|
||||||
|
ctx wrapper.HttpContext,
|
||||||
|
enabled bool,
|
||||||
|
maxSize int,
|
||||||
|
bufferKey string,
|
||||||
|
logKey string,
|
||||||
|
chunk []byte,
|
||||||
|
isEndStream bool,
|
||||||
|
) []byte {
|
||||||
|
// If body logging is not enabled or max size is <= 0, just return the chunk as is
|
||||||
|
if !enabled || maxSize <= 0 {
|
||||||
|
return chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the buffer from context
|
||||||
|
buffer, _ := ctx.GetContext(bufferKey).([]byte)
|
||||||
|
|
||||||
|
// If we haven't reached max size yet, append chunk to buffer
|
||||||
|
if len(buffer) < maxSize {
|
||||||
|
// Calculate how much of this chunk we can add
|
||||||
|
remainingCapacity := maxSize - len(buffer)
|
||||||
|
if remainingCapacity > 0 {
|
||||||
|
if len(chunk) <= remainingCapacity {
|
||||||
|
buffer = append(buffer, chunk...)
|
||||||
|
ctx.SetContext(bufferKey, buffer)
|
||||||
|
} else {
|
||||||
|
buffer = append(buffer, chunk[:remainingCapacity]...)
|
||||||
|
// reach max size, record and clear
|
||||||
|
bodyStr := string(buffer)
|
||||||
|
setPropertyWithMarshal(logKey, bodyStr)
|
||||||
|
// clear buffer
|
||||||
|
ctx.SetContext(bufferKey, []byte{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we reach the end of stream, create log entry
|
||||||
|
if isEndStream && len(buffer) > 0 {
|
||||||
|
bodyStr := string(buffer)
|
||||||
|
setPropertyWithMarshal(logKey, bodyStr)
|
||||||
|
// clear buffer
|
||||||
|
ctx.SetContext(bufferKey, []byte{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always return the original chunk unmodified
|
||||||
|
return chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
// setPropertyWithMarshal marshals the given string value into a JSON-safe format
|
||||||
|
// and sets it as a property in the Envoy filter state with the specified key.
|
||||||
|
// This ensures proper escaping of special characters when the value is included in JSON.
|
||||||
|
func setPropertyWithMarshal(key string, value string) {
|
||||||
|
// Create a helper map to properly escape the string using JSON marshaling
|
||||||
|
helper := map[string]string{
|
||||||
|
"placeholder": value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the helper map to JSON
|
||||||
|
marshalledHelper, _ := json.Marshal(helper)
|
||||||
|
|
||||||
|
// Extract the properly escaped value using gjson
|
||||||
|
marshalledRaw := gjson.GetBytes(marshalledHelper, "placeholder").Raw
|
||||||
|
|
||||||
|
var marshalledStr string
|
||||||
|
if len(marshalledRaw) >= 2 {
|
||||||
|
// Remove the surrounding quotes from the JSON string
|
||||||
|
marshalledStr = marshalledRaw[1 : len(marshalledRaw)-1]
|
||||||
|
} else {
|
||||||
|
log.Errorf("failed to marshal json string, raw string is: %s", value)
|
||||||
|
marshalledStr = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the property with the marshaled string
|
||||||
|
if err := proxywasm.SetProperty([]string{key}, []byte(marshalledStr)); err != nil {
|
||||||
|
log.Errorf("failed to set %s in filter state, err: %v, raw:\n%s", key, err, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// onHttpRequestHeaders processes the request headers and logs them if enabled
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig) types.Action {
|
||||||
|
// Get all request headers
|
||||||
|
headers, err := proxywasm.GetHttpRequestHeaders()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to get request headers: %v", err)
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
method := ""
|
||||||
|
contentType := ""
|
||||||
|
|
||||||
|
// Check if request headers need to be logged
|
||||||
|
if config.Request.Headers.Enabled {
|
||||||
|
jsonStr := "{}"
|
||||||
|
for _, header := range headers {
|
||||||
|
var err error
|
||||||
|
normalizedName := normalizeHeaderName(header[0])
|
||||||
|
jsonStr, err = sjson.Set(jsonStr, normalizedName, header[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to convert request header to JSON: name=%s, value=%s, error=%v", normalizedName, header[1], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPropertyWithMarshal(logKeyRequestHeaders, jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get request method and Content-Type for subsequent processing
|
||||||
|
for _, header := range headers {
|
||||||
|
if strings.ToLower(header[0]) == ":method" {
|
||||||
|
method = header[1]
|
||||||
|
} else if strings.ToLower(header[0]) == "content-type" {
|
||||||
|
contentType = header[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-POST/PUT/PATCH requests, or if request body logging is not enabled, no need to log the request body
|
||||||
|
if !config.Request.Body.Enabled || (method != "POST" && method != "PUT" && method != "PATCH") {
|
||||||
|
ctx.DontReadRequestBody()
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the content type is in the configured list for logging
|
||||||
|
shouldLogBody := false
|
||||||
|
for _, allowedType := range config.Request.Body.ContentTypes {
|
||||||
|
if strings.Contains(contentType, allowedType) {
|
||||||
|
shouldLogBody = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldLogBody {
|
||||||
|
ctx.DontReadRequestBody()
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a buffer to accumulate request body chunks
|
||||||
|
ctx.SetContext(contextKeyRequestBodyBuffer, []byte{})
|
||||||
|
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// onStreamingRequestBody processes each chunk of the request body in streaming mode
|
||||||
|
// This allows us to log the request body without affecting the original request
|
||||||
|
func onStreamingRequestBody(ctx wrapper.HttpContext, config PluginConfig, chunk []byte, isEndStream bool) []byte {
|
||||||
|
return processStreamingBody(
|
||||||
|
ctx,
|
||||||
|
config.Request.Body.Enabled,
|
||||||
|
config.Request.Body.MaxSize,
|
||||||
|
contextKeyRequestBodyBuffer,
|
||||||
|
logKeyRequestBody,
|
||||||
|
chunk,
|
||||||
|
isEndStream,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// onHttpResponseHeaders processes the response headers and logs them if enabled
|
||||||
|
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig) types.Action {
|
||||||
|
// Get all response headers
|
||||||
|
headers, err := proxywasm.GetHttpResponseHeaders()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to get response headers: %v", err)
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if response headers need to be logged
|
||||||
|
if config.Response.Headers.Enabled {
|
||||||
|
jsonStr := "{}"
|
||||||
|
for _, header := range headers {
|
||||||
|
var err error
|
||||||
|
normalizedName := normalizeHeaderName(header[0])
|
||||||
|
jsonStr, err = sjson.Set(jsonStr, normalizedName, header[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to convert response header to JSON: name=%s, value=%s, error=%v", normalizedName, header[1], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPropertyWithMarshal(logKeyResponseHeaders, jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if response body needs to be logged
|
||||||
|
if !config.Response.Body.Enabled {
|
||||||
|
ctx.DontReadResponseBody()
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Content-Type for response body logging
|
||||||
|
contentType := ""
|
||||||
|
for _, header := range headers {
|
||||||
|
if strings.ToLower(header[0]) == "content-type" {
|
||||||
|
contentType = header[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip response body logging if content type is not in the configured list
|
||||||
|
if contentType != "" {
|
||||||
|
shouldLogBody := false
|
||||||
|
for _, allowedType := range config.Response.Body.ContentTypes {
|
||||||
|
if strings.Contains(contentType, allowedType) {
|
||||||
|
shouldLogBody = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldLogBody {
|
||||||
|
ctx.DontReadResponseBody()
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a buffer to accumulate response body chunks
|
||||||
|
ctx.SetContext(contextKeyResponseBodyBuffer, []byte{})
|
||||||
|
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// onStreamingResponseBody processes each chunk of the response body in streaming mode
|
||||||
|
// This allows us to log the response body without affecting the original response
|
||||||
|
func onStreamingResponseBody(ctx wrapper.HttpContext, config PluginConfig, chunk []byte, isEndStream bool) []byte {
|
||||||
|
return processStreamingBody(
|
||||||
|
ctx,
|
||||||
|
config.Response.Body.Enabled,
|
||||||
|
config.Response.Body.MaxSize,
|
||||||
|
contextKeyResponseBodyBuffer,
|
||||||
|
logKeyResponseBody,
|
||||||
|
chunk,
|
||||||
|
isEndStream,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user