feat: Supports recording request header, request body, response header and response body information in the access log (#2265)

This commit is contained in:
Forgottener
2025-05-21 16:15:05 +08:00
committed by GitHub
parent 93436db13c
commit fa3c5ea0fc
7 changed files with 760 additions and 0 deletions

View 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. 请确保合理配置该插件,避免记录敏感信息到日志中

View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -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: {}

View 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

View 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
)

View 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=

View 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,
)
}