[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文件夹,建立以下两个文件:

[Laravel]链路追踪

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:

[Laravel]链路追踪

'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);
    }

效果如下:

[Laravel]链路追踪

截止到这里,我们已经可以通过一个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);
    }
}

[Laravel]链路追踪

完成这一步之后,当对接出现异常时,可以用这个traceId进行链路追踪

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 1

Laravel 日志有 withContextshareContext 方法,可以在中间件里面设置一下就可以,简化配置,后续写日志,就会自动合并在 context 里面了

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!