hlg212 opened a new issue, #12781:
URL: https://github.com/apache/apisix/issues/12781
### Current Behavior
**Version:** apache/apisix:3.14.0-debian
I added a global http-logger plugin to collect `req_body` and `resp_body`.
However, I found that in the logs received by the backend, `req_body` is empty,
while `resp_body` is captured correctly.
Then, I switched the logging plugin to kafka-logger. Testing showed that
`req_body` could be obtained normally.
Here's how I verified the issue:
I added log outputs in the code of `log-util.lua`, then overwrote the file
in the image. Specifically, within the `get_request_body` method, I added a
log: `core.log.warn("get_request_body, req_body: ", req_body)`. After making a
request, I checked the APISIX logs. When using the http-logger, the printed
`req_body` was `nil`. In contrast, with the kafka-logger, it printed the normal
request data.
**docker-compose.yaml**
```yaml
version: "3"
services:
apisix:
image: apache/apisix:${APISIX_IMAGE_TAG:-3.14.0-debian}
restart: always
volumes:
- ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
-
./apisix-master/apisix/utils/log-util.lua:/usr/local/apisix/apisix/utils/log-util.lua:ro
depends_on:
- etcd
##network_mode: host
ports:
- "9180:9180/tcp"
- "9080:9080/tcp"
- "9091:9091/tcp"
- "9443:9443/tcp"
networks:
apisix:
etcd:
image: artifact.srdcloud.cn/public-docker-virtual/bitnami/etcd:3.5.11
restart: always
volumes:
- etcd_data:/bitnami/etcd
environment:
ETCD_ENABLE_V2: "true"
ALLOW_NONE_AUTHENTICATION: "yes"
ETCD_ADVERTISE_CLIENT_URLS: "http://etcd:2379"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
ports:
- "2379:2379/tcp"
networks:
apisix:
apisix_dashboard:
image: apache/apisix-dashboard:latest
restart: always
ports:
- "9000:9000"
volumes:
-
"./dashboard_conf/conf.yaml:/usr/local/apisix-dashboard/conf/conf.yaml"
networks:
apisix:
networks:
apisix:
driver: bridge
volumes:
etcd_data:
driver: local
```
**http-logger configuration:**
```
{
"_meta": {
"disable": false
},
"include_req_body": true,
"include_resp_body": true,
"uri": "http://mockbin:8580/bin/b3feb263-77e0-468e-bbb2-71a7900c93f8"
}
```
**kafka-logger configuration:**
```
{
"_meta": {
"disable": false
},
"brokers": [
{
"host": "kafka",
"port": 31423
}
],
"include_req_body": true,
"include_resp_body": true,
"kafka_topic": "ability-logs",
"key": "key1"
}
```
**log-util.lua:**
```
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local core = require("apisix.core")
local plugin = require("apisix.plugin")
local expr = require("resty.expr.v1")
local content_decode = require("apisix.utils.content-decode")
local ngx = ngx
local pairs = pairs
local ngx_now = ngx.now
local ngx_header = ngx.header
local os_date = os.date
local str_byte = string.byte
local str_sub = string.sub
local math_floor = math.floor
local ngx_update_time = ngx.update_time
local req_get_body_data = ngx.req.get_body_data
local is_http = ngx.config.subsystem == "http"
local req_get_body_file = ngx.req.get_body_file
local MAX_REQ_BODY = 524288 -- 512 KiB
local MAX_RESP_BODY = 524288 -- 512 KiB
local io = io
local lru_log_format = core.lrucache.new({
ttl = 300, count = 512
})
local _M = {}
local function get_request_body(max_bytes)
local req_body = req_get_body_data()
core.log.warn("get_request_body,req_body: ", req_body)
if req_body then
if max_bytes and #req_body >= max_bytes then
req_body = str_sub(req_body, 1, max_bytes)
end
return req_body
end
local file_name = req_get_body_file()
if not file_name then
return nil
end
core.log.info("attempt to read body from file: ", file_name)
local f, err = io.open(file_name, 'r')
if not f then
return nil, "fail to open file " .. err
end
req_body = f:read(max_bytes)
f:close()
return req_body
end
local function gen_log_format(format)
local log_format = {}
for k, var_name in pairs(format) do
if var_name:byte(1, 1) == str_byte("$") then
log_format[k] = {true, var_name:sub(2)}
else
log_format[k] = {false, var_name}
end
end
core.log.info("log_format: ", core.json.delay_encode(log_format))
return log_format
end
local function get_custom_format_log(ctx, format, max_req_body_bytes)
local log_format = lru_log_format(format or "", nil, gen_log_format,
format)
local entry = core.table.new(0, core.table.nkeys(log_format))
for k, var_attr in pairs(log_format) do
if var_attr[1] then
local key = var_attr[2]
if key == "request_body" then
local max_req_body_bytes = max_req_body_bytes or MAX_REQ_BODY
local req_body, err = get_request_body(max_req_body_bytes)
if err then
core.log.error("fail to get request body: ", err)
else
entry[k] = req_body
end
else
entry[k] = ctx.var[var_attr[2]]
end
else
entry[k] = var_attr[2]
end
end
local matched_route = ctx.matched_route and ctx.matched_route.value
if matched_route then
entry.service_id = matched_route.service_id
entry.route_id = matched_route.id
end
return entry
end
-- export the log getter so we can mock in tests
_M.get_custom_format_log = get_custom_format_log
-- for test
function _M.inject_get_custom_format_log(f)
get_custom_format_log = f
_M.get_custom_format_log = f
end
local function latency_details_in_ms(ctx)
local latency = (ngx_now() - ngx.req.start_time()) * 1000
local upstream_latency, apisix_latency = nil, latency
if ctx.var.upstream_response_time then
upstream_latency = ctx.var.upstream_response_time * 1000
apisix_latency = apisix_latency - upstream_latency
-- The latency might be negative, as Nginx uses different time
measurements in
-- different metrics.
-- See
https://github.com/apache/apisix/issues/5146#issuecomment-928919399
if apisix_latency < 0 then
apisix_latency = 0
end
end
return latency, upstream_latency, apisix_latency
end
_M.latency_details_in_ms = latency_details_in_ms
local function get_full_log(ngx, conf)
local ctx = ngx.ctx.api_ctx
local var = ctx.var
local service_id
local route_id
local url = var.scheme .. "://" .. var.host .. ":" .. var.server_port
.. var.request_uri
local matched_route = ctx.matched_route and ctx.matched_route.value
if matched_route then
service_id = matched_route.service_id or ""
route_id = matched_route.id
else
service_id = var.host
end
local consumer
if ctx.consumer then
consumer = {
username = ctx.consumer.username
}
end
local latency, upstream_latency, apisix_latency =
latency_details_in_ms(ctx)
local log = {
request = {
url = url,
uri = var.request_uri,
method = ngx.req.get_method(),
headers = ngx.req.get_headers(),
querystring = ngx.req.get_uri_args(),
size = var.request_length
},
response = {
status = ngx.status,
headers = ngx.resp.get_headers(),
size = var.bytes_sent
},
server = {
hostname = core.utils.gethostname(),
version = core.version.VERSION
},
upstream = var.upstream_addr,
service_id = service_id,
route_id = route_id,
consumer = consumer,
client_ip = core.request.get_remote_client_ip(ngx.ctx.api_ctx),
start_time = ngx.req.start_time() * 1000,
latency = latency,
upstream_latency = upstream_latency,
apisix_latency = apisix_latency
}
if conf.include_resp_body then
log.response.body = ctx.resp_body
end
core.log.warn("include_req_body: ",include_req_body)
if conf.include_req_body then
local log_request_body = true
core.log.warn("log_request_body: true")
if conf.include_req_body_expr then
if not conf.request_expr then
local request_expr, err =
expr.new(conf.include_req_body_expr)
if not request_expr then
core.log.error('generate request expr err ' .. err)
return log
end
conf.request_expr = request_expr
end
local result = conf.request_expr:eval(ctx.var)
if not result then
log_request_body = false
end
end
core.log.warn("alter log_format: ", log_request_body)
if log_request_body then
local max_req_body_bytes = conf.max_req_body_bytes or
MAX_REQ_BODY
local body, err = get_request_body(max_req_body_bytes)
if err then
core.log.error("fail to get request body: ", err)
return
end
log.request.body = body
core.log.warn("body : ", body)
end
end
return log
end
_M.get_full_log = get_full_log
-- for test
function _M.inject_get_full_log(f)
get_full_log = f
_M.get_full_log = f
end
local function is_match(match, ctx)
local match_result
for _, m in pairs(match) do
local expr, _ = expr.new(m)
match_result = expr:eval(ctx.var)
if match_result then
break
end
end
return match_result
end
function _M.get_log_entry(plugin_name, conf, ctx)
-- If the "match" configuration is set and the matching conditions are
not met,
-- then do not log the message.
if conf.match and not is_match(conf.match, ctx) then
return
end
local metadata = plugin.plugin_metadata(plugin_name)
core.log.info("metadata: ", core.json.delay_encode(metadata))
local entry
local customized = false
local has_meta_log_format = metadata and metadata.value.log_format
and core.table.nkeys(metadata.value.log_format) > 0
if conf.log_format or has_meta_log_format then
customized = true
entry = get_custom_format_log(ctx, conf.log_format or
metadata.value.log_format,
conf.max_req_body_bytes)
else
if is_http then
entry = get_full_log(ngx, conf)
else
-- get_full_log doesn't work in stream
core.log.error(plugin_name, "'s log_format is not set")
end
end
if ctx.llm_summary then
entry.llm_summary = ctx.llm_summary
end
if ctx.llm_request then
entry.llm_request = ctx.llm_request
end
if ctx.llm_response_text then
entry.llm_response_text = ctx.llm_response_text
end
return entry, customized
end
function _M.get_req_original(ctx, conf)
local data = {
ctx.var.request, "\r\n"
}
for k, v in pairs(ngx.req.get_headers()) do
core.table.insert_tail(data, k, ": ", v, "\r\n")
end
core.table.insert(data, "\r\n")
if conf.include_req_body then
local max_req_body_bytes = conf.max_req_body_bytes or MAX_REQ_BODY
local req_body = get_request_body(max_req_body_bytes)
core.table.insert(data, req_body)
end
return core.table.concat(data, "")
end
function _M.check_log_schema(conf)
if conf.include_req_body_expr then
local ok, err = expr.new(conf.include_req_body_expr)
if not ok then
return nil, "failed to validate the 'include_req_body_expr'
expression: " .. err
end
end
if conf.include_resp_body_expr then
local ok, err = expr.new(conf.include_resp_body_expr)
if not ok then
return nil, "failed to validate the 'include_resp_body_expr'
expression: " .. err
end
end
return true, nil
end
function _M.collect_body(conf, ctx)
if conf.include_resp_body then
local log_response_body = true
if conf.include_resp_body_expr then
if not conf.response_expr then
local response_expr, err =
expr.new(conf.include_resp_body_expr)
if not response_expr then
core.log.error('generate response expr err ' .. err)
return
end
conf.response_expr = response_expr
end
if ctx.res_expr_eval_result == nil then
ctx.res_expr_eval_result = conf.response_expr:eval(ctx.var)
end
if not ctx.res_expr_eval_result then
log_response_body = false
end
end
if log_response_body then
local max_resp_body_bytes = conf.max_resp_body_bytes or
MAX_RESP_BODY
if ctx._resp_body_bytes and ctx._resp_body_bytes >=
max_resp_body_bytes then
return
end
local final_body = core.response.hold_body_chunk(ctx, true,
max_resp_body_bytes)
if not final_body then
return
end
local response_encoding = ngx_header["Content-Encoding"]
if not response_encoding then
ctx.resp_body = final_body
return
end
local decoder =
content_decode.dispatch_decoder(response_encoding)
if not decoder then
core.log.warn("unsupported compression encoding type: ",
response_encoding)
ctx.resp_body = final_body
return
end
local decoded_body, err = decoder(final_body)
if err ~= nil then
core.log.warn("try decode compressed data err: ", err)
ctx.resp_body = final_body
return
end
ctx.resp_body = decoded_body
end
end
end
function _M.get_rfc3339_zulu_timestamp(timestamp)
ngx_update_time()
local now = timestamp or ngx_now()
local second = math_floor(now)
local millisecond = math_floor((now - second) * 1000)
return os_date("!%Y-%m-%dT%T.", second) .. core.string.format("%03dZ",
millisecond)
end
return _M
```
### Expected Behavior
_No response_
### Error Logs
**http-logger log:**
```
2025/11/27 09:34:50 [warn] 54#54: *498217 [lua] log-util.lua:206:
get_full_log(): include_req_body: nil while logging request, client:
172.31.0.1, server: _, request: "POST /header/test HTTP/1.1", host:
"localhost:9080"
2025/11/27 09:34:50 [warn] 54#54: *498217 [lua] log-util.lua:210:
get_full_log(): log_request_body: true while logging request, client:
172.31.0.1, server: _, request: "POST /header/test HTTP/1.1", host:
"localhost:9080"
2025/11/27 09:34:50 [warn] 54#54: *498217 [lua] log-util.lua:228:
get_full_log(): alter log_format: true while logging request, client:
172.31.0.1, server: _, request: "POST /header/test HTTP/1.1", host:
"localhost:9080"
2025/11/27 09:34:50 [warn] 54#54: *498217 [lua] log-util.lua:46:
get_request_body(): get_request_body,req_body: nil while logging request,
client: 172.31.0.1, server: _, request: "POST /header/test HTTP/1.1", host:
"localhost:9080"
2025/11/27 09:34:50 [warn] 54#54: *498217 [lua] log-util.lua:237:
get_full_log(): body : nil while logging request, client: 172.31.0.1, server:
_, request: "POST /header/test HTTP/1.1", host: "localhost:9080"
```
**kafka-logger log:**
```
2025/11/27 09:36:11 [warn] 63#63: *506672 [lua] log-util.lua:206:
get_full_log(): include_req_body: nil while logging request, client:
172.31.0.1, server: _, request: "POST /header/test HTTP/1.1", host:
"localhost:9080"
2025/11/27 09:36:11 [warn] 63#63: *506672 [lua] log-util.lua:210:
get_full_log(): log_request_body: true while logging request, client:
172.31.0.1, server: _, request: "POST /header/test HTTP/1.1", host:
"localhost:9080"
2025/11/27 09:36:11 [warn] 63#63: *506672 [lua] log-util.lua:228:
get_full_log(): alter log_format: true while logging request, client:
172.31.0.1, server: _, request: "POST /header/test HTTP/1.1", host:
"localhost:9080"
2025/11/27 09:36:11 [warn] 63#63: *506672 [lua] log-util.lua:46:
get_request_body(): get_request_body,req_body: {
"a111":"c222"
} while logging request, client: 172.31.0.1, server: _, request: "POST
/header/test HTTP/1.1", host: "localhost:9080"
2025/11/27 09:36:11 [warn] 63#63: *506672 [lua] log-util.lua:237:
get_full_log(): body : {
"a111":"c222"
} while logging request, client: 172.31.0.1, server: _, request: "POST
/header/test HTTP/1.1", host: "localhost:9080"
172.31.0.1 - - [27/Nov/2025:09:36:11 +0000] localhost:9080 "POST
/header/test HTTP/1.1" 200 14 0.000 "-" "PostmanRuntime/7.29.2" - - -
"http://localhost:9080"
```
### Steps to Reproduce
1.Run docker-compose up -d
2.Access the APISIX Dashboard at: http://localhost:9000/
3.Configure a global plugin in the Plugin section on the left
4.Make a request to your route and observe the logs printed by APISIX
### Environment
- APISIX version (run `apisix version`):
apisix:3.14.0-debian
- Operating system (run `uname -a`):
win11 docker
- OpenResty / Nginx version (run `openresty -V` or `nginx -V`):
- etcd version, if relevant (run `curl
http://127.0.0.1:9090/v1/server_info`):
bitnami/etcd:3.5.11
- APISIX Dashboard version, if relevant:
apache/apisix-dashboard:latest
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]