[Laravel]链路追踪
在实际项目运作的过程中,我们经常需要快速定位一个问题,特别在做了微服务之后,需要通过一个标志进行全链路的追踪;
于是我们需要:
–> 在http请求的输入输出处加入一个traceId做标记;
–> 在写入日志文件的时候,带入这个traceId;
–> 全局异常捕捉,并将traceId带入;
–> 在http输出时,将traceId输出给前端(方便定位业务问题)
1、在http请求的输入输出处加入一个traceId做标记;
1.1)在App\Http\Middleware下创建一个AccessLog.php文件,用来捕捉请求:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AccessLog
{
/**
* @description: 记录请求日志
* @auther: guoqingfeng
* @Date: 2023/3/6
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
//这里过滤到你不想捕捉的请求
if (strrpos($request->url(), 'admin/logs') !== false) {
return $next($request);
}
//如果请求自定义了traceId,则用请求的;否则生成一个进行链路追踪(预留用作做微服务时候使用)
if (!empty($request['traceId'])) {
$_SESSION['traceId'] = $request['traceId'];
} else {
$_SESSION['traceId'] = md5(time() . mt_rand(1, 1000000));
}
// 记录请求信息
$requestMessage = [
'url' => $request->url(),
'method' => $request->method(),
'ip' => $request->ips(),
'headers' => $request->header('Authorization'),
'params' => $request->all()
];
\Log::channel('request')->info("请求信息:", $requestMessage);
$respone = $next($request);
$responeData = [
'respone' => json_decode($respone->getContent(), true) ?? ""
];
\Log::channel('request')->info("返回信息:", $responeData);
return $respone;
}
}
1.2)找到App\Http\Kernel.php加入一个中间件的引用:
<?php
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
\App\Http\Middleware\AccessLog::class,
];
}
2、在写入日志文件的时候,带入这个traceId;
2.1)在app下创建一个Logging文件夹,建立以下两个文件:
ApplogFormatter.php:
<?php
namespace App\Logging;
use App\Logging\JsonFormatter;
class ApplogFormatter
{
/**
* Customize the given logger instance.
*
* @param \Illuminate\Log\Logger $logger
* @return void
*/
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
$handler->setFormatter(new JsonFormatter());
}
}
}
JsonFormatter.php:
<?php
namespace App\Logging;
use Monolog\Formatter\JsonFormatter as BaseJsonFormatter;
class JsonFormatter extends BaseJsonFormatter
{
public function format(array $record) : string
{
//获取全局的traceId,进行记录
$traceId = $_SESSION['traceId'] ?? '';
// 这个就是最终要记录的数组,最后转成Json并记录进日志(你可以自定义想要的格式)
$time = $record['datetime']->format('Y-m-d H:i:s');
$content = '';
if (!empty($record['context'])) {
$content = json_encode($record['context']);
}
return "[{$time}] {$record['channel']}.{$record['level_name']}: [{$traceId}]{$record['message']} {$content}\n";
}
}
2.2)到app\config\logging.php里,找到你需要用到的日志channels,设置tap:
'channels' => [
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel-' . php_sapi_name() . '.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 180,
'tap' => [App\Logging\ApplogFormatter::class],
],
];
3、全局异常捕捉,并将traceId带入;
进入app\Exceptions\Handler.php,重载render方法:
/**
* @description: 全局异常捕捉,输出处理
* 此方法不能代替所有该有的异常处理,只是作为最后的防线地进行友好提示
* @auther: guoqingfeng
* @Date: 2023/3/7
* @param $request
* @param Throwable $e
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response
*/
public function render($request, Throwable $e)
{
//如果是调试模式下,直接输出异常
if (env('APP_DEBUG')) {
return parent::render($request, $e);
}
//因为已经重载了Log方法,这里只需要拦截意外的全局异常,进行一个普通的log,就可以把traceId追加进去
\Log::error(sprintf("%s[%s]: %s", $e->getFile(), $e->getLine(), $e->getMessage()));
\Log::error($e->getTraceAsString());
return CommonService::jsonError($e->getMessage() ?? '未知错误', (int)$e->getCode() ?? 500);
}
效果如下:
截止到这里,我们已经可以通过一个traceId查询出一个系统所有日志的内容,同理也可以用这个tradeId进行微服务跨系统下的日志追踪(如果可以把上面的一套动作打包成组件那就更好了,之后的微服务只要引用这个组件就可以轻松实现全链路的追踪)。
4、将traceId输出给前端(方便定位业务问题)
这个自行封装一个json输出就好了,这里给一个例子:
<?php
class CommonService
{
static function jsonMessage(mixed $data): JsonResponse
{
$result = [
'code' => 0,
'message' => 'ok',
'type' => 'success',
'result' => $data,
'traceId' => $_SESSION['traceId'] ?? '',
];
return response()->json($result);
}
}
完成这一步之后,当对接出现异常时,可以用这个traceId进行链路追踪
本作品采用《CC 协议》,转载必须注明作者和本文链接
Laravel 日志有
withContext
和shareContext
方法,可以在中间件里面设置一下就可以,简化配置,后续写日志,就会自动合并在 context 里面了