mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 23:21:08 +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