ThinkPHP 6 请求维度日志统计应在中间件中结构化采集,使用 Request 对象获取方法、URL、UA 等字段,配合毫秒耗时计算,以 JSON 行格式写入独立文件,并添加请求 ID 与时间戳;导出 CSV 需加 UTF-8 BOM 头,流式读取防内存溢出,字段设计须覆盖排查所需维度。

ThinkPHP 6 的日志统计怎么加请求维度
直接改 think\log\driver\File 不现实,它只按时间切片、不记录请求上下文。得在请求生命周期里埋点——推荐在中间件中统计,比控制器层更统一、不易遗漏。
关键不是“记日志”,而是“结构化采集”。比如用 Request 对象取 $request->method()、$request->url(true)、$request->header('user-agent'),再配合 Debug::getMillisecond() 算耗时。
- 避免在每个控制器里手动写统计逻辑,维护成本高
- 不要用
Log::write()直接打原始字符串,后续导出难解析 - 建议用 JSON 行格式(每行一个 JSON 对象)写入独立日志文件,例如
runtime/log/request_stats_20240512.log
如何让日志自动带时间戳和请求ID
ThinkPHP 6 默认不生成全局请求 ID,但 think\Request 支持设置自定义属性。中间件里可以这样补:
// app/middleware/RequestStat.php
public function handle($request, \Closure $next)
{
$request->withAttr('req_id', uniqid('req_', true));
$start = microtime(true);
<pre class="brush:php;toolbar:false;">$response = $next($request);
$cost = round((microtime(true) - $start) * 1000, 2);
$logData = [
'req_id' => $request->attr('req_id'),
'method' => $request->method(),
'url' => $request->url(true),
'status' => $response->getCode(),
'cost_ms' => $cost,
'ua' => $request->header('user-agent', ''),
'ip' => $request->ip(),
'time' => date('Y-m-d H:i:s'),
'date' => date('Y-m-d'),
];
// 写入独立统计日志
$file = runtime_path() . 'log/request_stats_' . date('Ymd') . '.log';
file_put_contents($file, json_encode($logData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND | LOCK_EX);
return $response;}
注意:别用 Log::channel('xxx') 复用默认日志通道,它的格式和轮转策略会干扰统计分析。
导出 CSV 时中文字段乱码怎么解决
直接用 fputcsv() 写 UTF-8 内容到 CSV,在 Windows Excel 里默认当 ANSI 解,必然乱码。必须加 BOM 头。
- 导出前先写
\xEF\xBB\xBF(UTF-8 BOM) - 字段值用
mb_convert_encoding($val, 'GBK', 'UTF-8')转码是错的——Excel for Mac 不认 GBK - 正确做法:保持 UTF-8 编码 + 加 BOM + 用双引号包裹含逗号/换行的字段
示例导出片段:
// controller 方法内
$filename = 'request_stats_' . date('Ymd_His') . '.csv';
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
<p>$fp = fopen('php://output', 'w');
fputs($fp, "\xEF\xBB\xBF"); // 写 BOM</p><p>// 写表头
fputcsv($fp, ['请求ID', '方法', 'URL', '状态码', '耗时(ms)', 'IP', '时间']);</p><p>// 读当日日志并逐行解析写入
$logFile = runtime_path() . 'log/request<em>stats</em>' . date('Ymd') . '.log';
if (is_file($logFile)) {
$lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$data = json_decode($line, true);
if ($data) {
fputcsv($fp, [
$data['req_id'] ?? '',
$data['method'] ?? '',
mb_strimwidth($data['url'] ?? '', 0, 100, '…'), // 防超长截断
$data['status'] ?? '',
$data['cost_ms'] ?? '',
$data['ip'] ?? '',
$data['time'] ?? '',
]);
}
}
}
fclose($fp);</p>
大日志文件导出卡死或内存溢出
单个日志文件超过 50MB 后,file() 全加载进内存基本就崩了。必须流式处理。
- 用
fgets()逐行读,而不是file() - 导出 CSV 时也别拼大字符串,
fputcsv()直接写php://output - 加限流:比如只导出最近 10000 条,或按
date字段过滤(需日志里有该字段) - 如果真要查历史全量,建议提前用 Logstash 或 Python 脚本做离线清洗,Web 接口只服务近 7 天
真正难的不是代码写出来,而是日志字段设计是否支撑后续筛选——比如漏了 controller 和 action,排查接口性能时就得靠 URL 正则硬匹配。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











