|
|
<?php
|
|
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
|
|
use Closure;
|
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
|
use Illuminate\Http\Request;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
|
|
class LogRequest
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
* Handle an incoming request.
|
|
|
*
|
|
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
|
|
*/
|
|
|
public function handle(Request $request, Closure $next): Response
|
|
|
{
|
|
|
$request->attributes->set('start_time', microtime(true));
|
|
|
return $next($request);
|
|
|
}
|
|
|
|
|
|
// 响应发送给客户端后执行
|
|
|
public function terminate(Request $request, Response $response): void
|
|
|
{
|
|
|
$startTime = $request->attributes->get('start_time');
|
|
|
$endTime = microtime(true);
|
|
|
|
|
|
// 收集日志数据
|
|
|
$data = [
|
|
|
'method' => $request->method(),
|
|
|
'url' => $request->fullUrl(),
|
|
|
'ip' => $request->ip(),
|
|
|
'headers' => json_encode($this->getLoggableHeaders($request), JSON_UNESCAPED_UNICODE),
|
|
|
'input' => $this->truncate(json_encode($request->all(), JSON_UNESCAPED_UNICODE), 5000),
|
|
|
// 这里应用截断逻辑:如果 response 超过 10KB,将直接存储 'Data too long for column'
|
|
|
'response' => $this->truncate((string) $response->getContent(), 10000),
|
|
|
'status_code' => $response->status(),
|
|
|
'response_time' => round($endTime - $startTime, 3),
|
|
|
'created_at' => now(),
|
|
|
'updated_at' => now()
|
|
|
];
|
|
|
|
|
|
$tableName = 'zz_request_' . date('Ym');
|
|
|
$this->createTableIfNotExists($tableName);
|
|
|
DB::table($tableName)->insert($data);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 检查并创建月度表
|
|
|
*/
|
|
|
protected function createTableIfNotExists($tableName)
|
|
|
{
|
|
|
if (!Schema::hasTable($tableName)) {
|
|
|
// 使用 Schema 构建器创建表
|
|
|
Schema::create($tableName, function (Blueprint $table) {
|
|
|
$table->increments('id');
|
|
|
$table->string('method', 10)->comment('请求方法');
|
|
|
$table->text('url')->comment('请求URL');
|
|
|
$table->string('ip', 45)->comment('客户端IP');
|
|
|
$table->text('headers')->nullable()->comment('请求头');
|
|
|
$table->text('input')->nullable()->comment('请求参数');
|
|
|
$table->text('response');
|
|
|
$table->integer('status_code')->comment('响应状态码');
|
|
|
$table->decimal('response_time', 10, 3)->comment('响应时间(毫秒)');
|
|
|
$table->timestamps();
|
|
|
$table->index('created_at', 'idx_created_at');
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 简单的长度检查:要么全要,要么给个提示
|
|
|
* 不再进行复杂的截断操作
|
|
|
*/
|
|
|
protected function truncate($string, $maxBytes = 5000)
|
|
|
{
|
|
|
// 如果长度没超标,直接返回原数据
|
|
|
if (strlen($string) <= $maxBytes) {
|
|
|
return $string;
|
|
|
}
|
|
|
|
|
|
// 如果超标了,直接返回一个固定的提示字符串
|
|
|
return 'Data too long for column';
|
|
|
}
|
|
|
|
|
|
protected function getLoggableHeaders(Request $request)
|
|
|
{
|
|
|
$whitelist = [
|
|
|
'authorization',
|
|
|
'content-type',
|
|
|
'user-agent',
|
|
|
'x-requested-with',
|
|
|
'x-forwarded-for',
|
|
|
'x-real-ip',
|
|
|
'x-correlation-id',
|
|
|
'trace-id',
|
|
|
'request-id',
|
|
|
'host'
|
|
|
];
|
|
|
|
|
|
$result = [];
|
|
|
|
|
|
foreach ($whitelist as $header) {
|
|
|
if (!$request->headers->has($header)) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
$value = $request->headers->get($header);
|
|
|
|
|
|
// 脱敏处理
|
|
|
if (in_array(strtolower($header), ['authorization', 'cookie'])) {
|
|
|
$result[$header] = '[' . strtoupper(substr($header, 0, 3)) . ']';
|
|
|
}
|
|
|
// 或者更精细控制
|
|
|
elseif (strtolower($header) === 'authorization' && str_starts_with($value, 'Bearer ')) {
|
|
|
$token = substr($value, 7);
|
|
|
$result[$header] = 'Bearer ' . substr($token, 0, 6) . '...' . substr($token, -4);
|
|
|
} else {
|
|
|
$result[$header] = mb_strlen($value) > 200
|
|
|
? substr($value, 0, 197) . '...'
|
|
|
: $value;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return $result;
|
|
|
}
|
|
|
} |