场景举例:调用一个接口,接口会返回当前调用了几次,但是测试人员不承认,只说调用了一次,但是我的接口是累加调用次数,写入数据库的,超过5次就限制调用,测试人员就只承认调用了一次就被限制了,现在我只想把调用时间和调用返回的信息给测试看
场景举例:为什么不直接用fastadmin自带的写入日志记录呢,或在public/index.php写一个钩子来获取请求体和响应体呢,这个问题想到过,因为使用的是宝塔,安装了防火墙免费的,有时候会莫名其妙的拦截客户端的请求,返回405,403状态等等,这些请求信息是不会写入fastadmin的runtime/log的,因为还没进入fastadmin就被拦截了
直接开始吧
第一步:
如果你使用的是宝塔,编辑/www/server/nginx/conf/nginx.conf文件,在http最后的access_log off;位置后面添加下面代码,也可以根据需求添加
# === 请添加以下配置 开始 ===
# 提前声明可由 Lua 修改的日志变量
map $request_id $trace_request_body {
default "";
}
map $request_id $trace_request_body_truncated {
default "0";
}
map $request_id $trace_response_body {
default "";
}
map $request_id $trace_response_body_truncated {
default "0";
}
log_format full_trace escape=json
'{'
'"time":"$time_iso8601",'
'"request_id":"$request_id",'
'"host":"$host",'
'"server_addr":"$server_addr",'
'"client_ip":"$remote_addr",'
'"forwarded_for":"$http_x_forwarded_for",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"server_protocol":"$server_protocol",'
'"request_length":"$request_length",'
'"request_content_type":"$content_type",'
'"request_content_length":"$content_length",'
'"request_body":"$trace_request_body",'
'"request_body_truncated":"$trace_request_body_truncated",'
'"status":"$status",'
'"request_time":"$request_time",'
'"bytes_sent":"$bytes_sent",'
'"body_bytes_sent":"$body_bytes_sent",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_connect_time":"$upstream_connect_time",'
'"upstream_header_time":"$upstream_header_time",'
'"upstream_response_time":"$upstream_response_time",'
'"response_content_type":"$sent_http_content_type",'
'"response_content_length":"$sent_http_content_length",'
'"response_content_encoding":"$sent_http_content_encoding",'
'"response_body":"$trace_response_body",'
'"response_body_truncated":"$trace_response_body_truncated",'
'"request_headers":{'
'"token":"$http_token",'
'"deviceid":"$http_deviceid",'
'"referer":"$http_referer",'
'"user_agent":"$http_user_agent"'
'}'
'}';
# === 请添加以上配置 结束 ===
第二步:在需要写入日志的网站配置nginx参数,宝塔打开网站的配置文件,在server最后的error_log后面添加下面代码,并且把原来的access_log这行注释掉
# 采集请求体前 4096 个原始字节
access_by_lua_block {
local limit = 4096
-- 避免在 UTF-8 中文字符中间截断
local function utf8_prefix(value, max_bytes)
if not value then
return ""
end
if #value <= max_bytes then
return value
end
local cut = max_bytes
-- 向前寻找当前 UTF-8 字符的起始字节
while cut > 0 do
local byte = string.byte(value, cut)
if byte < 128 or byte >= 192 then
break
end
cut = cut - 1
end
if cut <= 0 then
return ""
end
local lead = string.byte(value, cut)
local char_length = 1
if lead >= 240 and lead <= 244 then
char_length = 4
elseif lead >= 224 and lead <= 239 then
char_length = 3
elseif lead >= 194 and lead <= 223 then
char_length = 2
end
-- 最后一个字符不完整时将其删除
if cut + char_length - 1 > max_bytes then
return string.sub(value, 1, cut - 1)
end
return string.sub(value, 1, max_bytes)
end
ngx.req.read_body()
local body = ngx.req.get_body_data()
local excerpt = ""
if body then
excerpt = utf8_prefix(body, limit)
if #body > limit then
ngx.var.trace_request_body_truncated = "1"
end
else
local body_file = ngx.req.get_body_file()
if body_file then
local file = io.open(body_file, "rb")
if file then
-- 多读取4字节,便于安全处理UTF-8边界
local data = file:read(limit + 4) or ""
file:close()
excerpt = utf8_prefix(data, limit)
if #data > limit then
ngx.var.trace_request_body_truncated = "1"
end
end
end
end
ngx.var.trace_request_body = excerpt
}
# 逐块采集响应体,只累计前 4096 个原始字节
body_filter_by_lua_block {
local limit = 4096
local function utf8_prefix(value, max_bytes)
if not value then
return ""
end
if #value <= max_bytes then
return value
end
local cut = max_bytes
while cut > 0 do
local byte = string.byte(value, cut)
if byte < 128 or byte >= 192 then
break
end
cut = cut - 1
end
if cut <= 0 then
return ""
end
local lead = string.byte(value, cut)
local char_length = 1
if lead >= 240 and lead <= 244 then
char_length = 4
elseif lead >= 224 and lead <= 239 then
char_length = 3
elseif lead >= 194 and lead <= 223 then
char_length = 2
end
if cut + char_length - 1 > max_bytes then
return string.sub(value, 1, cut - 1)
end
return string.sub(value, 1, max_bytes)
end
local chunk = ngx.arg[1]
local current = ngx.ctx.trace_response_body_raw or ""
if chunk and chunk ~= "" then
local remaining = limit - #current
if remaining > 0 then
current = current .. string.sub(chunk, 1, remaining)
ngx.ctx.trace_response_body_raw = current
ngx.var.trace_response_body =
utf8_prefix(current, limit)
end
if #chunk > remaining or
(remaining <= 0 and #chunk > 0) then
ngx.var.trace_response_body_truncated = "1"
end
end
}
access_log /www/wwwlogs/xxxxx.com/full_trace/current.log
full_trace
buffer=64k
flush=1s;
# 注意:为了确保 $request_body 有值,建议在 server 块级别开启缓冲
#client_body_buffer_size 128k;
#client_body_in_single_buffer on;
到这里其实已经大功告成了,只需要重启nginx就可以了,如果需要把写入的日志按日期分类需要进行下面操作
日志按日期分类
如果你使用的是宝塔linux系统在/etc下创建xxxx.conf文件复制下面这段代码
"/www/wwwlogs/xxxxxx/full_trace/current.log" {
daily
rotate 30
dateext
dateyesterday
dateformat -%Y-%m-%d
extension .log
nocompress
missingok
notifempty
su www www
create 0640 www www
sharedscripts
postrotate
if [ -s /www/server/nginx/logs/nginx.pid ]; then
kill -USR1 "$(cat /www/server/nginx/logs/nginx.pid)"
fi
endscript
}
daily:每天轮转一次。rotate 30:保留30份历史日志。dateext:历史文件使用日期后缀。dateyesterday:文件名使用昨天日期,与日志内容日期对应。extension .log:让 .log 保持在日期后面。compress:压缩历史日志。delaycompress:刚轮转的文件暂不压缩,下一轮再压缩。create:轮转后创建新的 current.log。USR1:通知 Nginx关闭旧文件并重新打开新日志文件。这是 Nginx官方推荐的轮转方式。在宝塔添加计划任务
在宝塔新建一个计划任务,可以每晚00:00执行
LC_ALL=C LANG=C /usr/sbin/logrotate \
-s /var/lib/logrotate-api-full-trace.status \
/etc/xxxx.conf
xxxx.conf是在etc文件夹下创建的文件测试定时任务使用下面代码
LC_ALL=C LANG=C /usr/sbin/logrotate \
-f -v \
-s /var/lib/logrotate-api-full-trace.status \
/etc/logrotate-api-full-trace.conf
会创建一个新的日志文件如果想方便查日志,可以在目标网站添加的server添加
add_header X-Request-ID $request_id always;
在 PHP的 FastCGI配置中确保传递:fastcgi_param HTTP_X_REQUEST_ID $request_id;
如果目标网站的server使用的是include enable-php-74.conf;,需要到宝塔的/www/server/nginx/conf目录修改enable-php-74.conf文件 location ~ [^/]\.php(/|$)
{
try_files $uri =404;
fastcgi_pass unix:/tmp/php-cgi-74.sock;
fastcgi_param HTTP_X_REQUEST_ID $request_id;
fastcgi_index index.php;
include fastcgi.conf;
include pathinfo.conf;
}
如果使用的是thinkPHP5.0通过中间件全局返回,创建文件application/common/behavior/RequestId.php内容:
<?php
namespace app\common\behavior;
use think\response\Json;
class RequestId
{
/**
* response_send 钩子
*
* @param mixed $response
*/
public function responseSend(&$response)
{
// 获取 Nginx 传给 PHP 的 request_id
$requestId = isset($_SERVER['HTTP_X_REQUEST_ID'])
? trim($_SERVER['HTTP_X_REQUEST_ID'])
: '';
// 防止直接访问 PHP 时没有 request_id
if (
$requestId === '' ||
!preg_match('/^[A-Za-z0-9._-]{1,128}$/', $requestId)
) {
$requestId = bin2hex(random_bytes(16));
}
// 所有由 ThinkPHP 返回的响应都增加响应头
$response->header('X-Request-ID', $requestId);
// 只修改 JSON 响应体
if ($response instanceof Json) {
$data = $response->getData();
if (is_array($data)) {
// 判断是不是 [1, 2, 3] 这种顶层列表
$isList = count($data) > 0
&& array_keys($data) === range(0, count($data) - 1);
if ($isList) {
// 包装顶层数组
$data = [
'data' => $data,
'request_id' => $requestId,
];
} else {
// 常见的接口对象直接增加字段
$data['request_id'] = $requestId;
}
} elseif (is_object($data)) {
$data->request_id = $requestId;
} else {
$data = [
'data' => $data,
'request_id' => $requestId,
];
}
$response->data($data);
}
}
}
注册行为,编辑application/tags.php,加入 response_send<?php
return [
'app_init' => [],
'app_begin' => [],
'module_init' => [],
'action_begin' => [],
'view_filter' => [],
'log_write' => [],
'app_end' => [],
// 响应发送前统一处理
'response_send' => [
'app\\common\\behavior\\RequestId',
],
];
效果:
{
"code": 0,
"msg": "授权码错误,剩余9次或联系管理员解除",
"time": "1787036720",
"data": {
"error_num": 1
},
"request_id": "7a784f80125fa8d9e1e0c12b62cdd7a5"
}
总结文件:总结文件
总结图片:下载链接
完整配置:完整配置
Nginx完整配置:NGINX完整配置