纯干货!基于monolog增强laravel框架的日志系统

背景

我们现在有一个项目,仅针对request请求增加了request-id,但是对于命令行脚本没有request-id,基于此,我重新梳理了一下laravel的Log系统并重新做了优化。

什么是日志

从数据层面来看,应用程序是一个将数据状态转换为另一种数据状态的过程。而日志,就是记录这个数据每一次的流转过程。日志是数据流转过程可视化的一种方式。在编程中,日志应该被放在优先级列表的首位。 日志信息可以包括程序的状态、错误消息、警告、调试信息等等。 日志通常被记录到文件中,但也可以发送到其他目的地,比如控制台或远程服务器。我们一般要记录的日志,分为三大块,业务日志,异常日志和数据库日志。

为什么使用日志

我们知道了什么是日志,日志的内容,那么我们为什么要使用日志呢?我认为主要有三点,第一个是当数据状态流转出现问题的时候,即业务处理失败了,我们需要知道哪一步处理出了问题,第二点是业务处理成功了,但是我们需要追溯数据、校验数据的准确性的时候,需要用到日志。第三点是基于日志的数据分析,可以把日志当做埋点,统计业务数据。

日志库monolog

Laravel的日志组件默认是使用 Seldaek/monolog 去实现,Monolog是一个高度灵活和流行的PHP日志库,Monolog 官方有提供 RedisHandler、MongoDBHandler、ElasticsearchHandler 等等,我们可以通过指定Handler 去改变日志处理的方式,将日志发送到文件,套接字,数据库,和其他网络服务。

使用日志

第一次使用monolog

官方给了一个基础的使用示范

php 复制代码
    public function testLog()
    {
        // create a log channel
        $log = new Logger('name');
        $log->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));

// add records to the log
        $log->warning('Foo');
        $log->error('Bar');

    }

执行完毕之后,发现在执行目录下生成了对应的日志文件 但是我们前面说过,laravel是默认使用monolog的,能不能使用faced模式来记录日志呢,我们修改一下这段代码

php 复制代码
    public function testLog()
    {

        Log::info('test log');

    }

果然,报错了,

php 复制代码
RuntimeException : A facade root has not been set.

为什么facade不能用呢,查看Test类发现并没有初始化laravel框架,框架初始化我是知道的,在public/index.php里面就实现了对框架的初始化,核心代码就是$app = require_once __DIR__.'/../bootstrap/app.php';,很明显,我引入这个文件,就完成了框架的初始化。 于是我搜索了一下,看看有几个地方会实现框架的初始化,我发现一共有三个文件会初始化框架,一个是入口文件index.php,一个是artisan,laravel提供的命令行工具,还有一个是Tests文件

php 复制代码
<?php

namespace Tests;

use Illuminate\Contracts\Console\Kernel;

trait CreatesApplication
{
    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        $app = require __DIR__.'/../bootstrap/app.php';

        $app->make(Kernel::class)->bootstrap();

        return $app;
    }
}

原来这是一个trait,看样子就是给phpunit使用的,用来初始化框架,太好了。于是,我引入这个trait,并在__construct中实现框架初始化

php 复制代码
use Tests\CreatesApplication;

class RequestTest extends TestCase
{
    use CreatesApplication;

    public function __construct()
    {
        parent::__construct();
        $this->createApplication();
    }

    public function testLog()
    {
        Log::info('test log');

    }
}

解决了框架没有初始化的问题。

日志的配置文件

  • laravel官方文档 那么框架的日志保存到哪里去了呢?通过阅读官方文档,我们得知配置文件在config/logging.php文件中。

  • 日志驱动类型 默认使用的是stack日志堆栈,我们看到,stack支持将日志发给多个channel。里面的path就是日志的生成路径,这里修改一下日志的存储目录

php 复制代码
'channels' => [
    'stack' => [
            'driver' => 'stack',
            'channels' => ['single'],
            'ignore_exceptions' => false,
        ],

       'daily' => [
            'driver' => 'daily',
            'tap' => [CustomizeFormatter::class],
            'path' => config('common.log_dir').'/laravel.log',
            'level' => 'debug',
            'days' => 14,
        ],
        'sql' => [
            'driver' => 'daily',
            'tap' => [CustomizeFormatter::class],
            'path' => config('common.log_dir').'/sql.log',
            'level' => 'debug',
            'days' => 14,
        ],
],

日志的类型

上下文信息

我们知道,记录日志最重要的一项就是关联的请求 ID,不能只记录一个简单的文本

sql 复制代码
[2024-10-15 15:49:41] local.INFO: test log  

那么这里我们就可以使用Log的上下文信息来实现这个功能。我们现在来实现一下。

添加request-id

我们添加request-id的时候,要考虑到不仅仅只支持request,而且要支持命令行工具,而且我们要指定日志的格式,所以我们使用tab来配置Monolog的实现,并在里面配置。在官方文档上,我们看到,添加extra-data的方式有两种,根据官方文档的说明,我们在logging.php中这样配置

sql 复制代码
        'daily' => [
            'driver' => 'daily',
            'tap' => [\App\Lib\CustomizeFormatter::class],
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'days' => 14,
        ],
  • 并新建CustomizeFormatter.php文件
sql 复制代码
    /**
     * 自定义给定的日志记录器实例。
     */
    public function __invoke(Logger $logger): void
    {
        $requestId = GenerateRequestId::getInstance()->requestId;
        foreach ($logger->getHandlers() as $handler) {
            $handler->pushProcessor(function ($record) use ($requestId) {
                //对应log的数组
                $record['context']['request_id'] = $requestId;
                return $record;
            });
            $handler->setFormatter(new LineFormatter(
                "[%datetime%] %channel%.%level_name%: %message% %context% %extra% \n", "Y-m-d H:i:s"
            ));
        }
    }
Adding extra data in the records

我们前面的pushProcessor是基于logger添加extra-data的方法,pushProcessor即在进程中添加extra-data。

  • 我们执行一下测试代码
php 复制代码
   public function testLog()
    {
        Log::info('this is test1');
        Log::info('this is test2');
    }

查看日志文件发现两个问题,第一个是格式问题,所有的log都被组装成了一条,很不清晰,第二个是同一个请求的requestId不一致。

php 复制代码
[2024-10-15T17:21:48.287502+08:00] local.INFO: this is test1 [] {"requestId":"0d5dc39c-ff74-4e43-9f15-18474991f0a4"}[2024-10-15T17:21:48.295217+08:00] local.INFO: this is test2 [] {"requestId":"cc11effa-0e79-4c52-bcad-5b699180b704"}

我们接下来优化一下,针对第一个问题,我们在setFormatter的时候加一个换行符即可,针对第二个问题,我们可以在每次实例化的时候只初始化一次request-id即可

php 复制代码
<?php

namespace App\Lib;

use Illuminate\Log\Logger;
use Illuminate\Support\Str;
use Monolog\Formatter\LineFormatter;

class CustomizeFormatter
{
    /**
     * 自定义给定的日志记录器实例。
     */
    public function __invoke(Logger $logger): void
    {
        //增加请求ID
        $requestId=Str::uuid()->toString();
        foreach ($logger->getHandlers() as $handler) {
            $handler->pushProcessor(function ($record)use($requestId) {
                $record['extra']['requestId'] =$requestId ;
                return $record;
            });
            $handler->setFormatter(new LineFormatter(
                "[%datetime%] %channel%.%level_name%: %message% %context% %extra% \n"
            ));
        }
    }
}

优化后,我们执行一版并查询日志

php 复制代码
[2024-10-15T17:36:54.685797+08:00] local.INFO: this is test1 [] {"requestId":"e45be761-289b-413b-940c-bcc36fa1b9a9"} 
[2024-10-15T17:36:54.686320+08:00] local.INFO: this is test2 [] {"requestId":"e45be761-289b-413b-940c-bcc36fa1b9a9"} 

添加request-id成功,最后,我们优化一下时间格式

php 复制代码
<?php

namespace App\Lib;

use Illuminate\Log\Logger;
use Monolog\Formatter\LineFormatter;

class CustomizeFormatter
{
    /**
     * 自定义给定的日志记录器实例。
     */
    public function __invoke(Logger $logger): void
    {
        $requestId = GenerateRequestId::getInstance()->requestId;
        foreach ($logger->getHandlers() as $handler) {
            $handler->pushProcessor(function ($record) use ($requestId) {
                //如果有request_id 不在重新生成
                if (empty($record['context']['request_id'])){
                    $record['context']['request_id'] = $requestId;
                }
                return $record;
            });
            $handler->setFormatter(new LineFormatter(
                "[%datetime%] %channel%.%level_name%: %message% %context% %extra% \n", "Y-m-d H:i:s"
            ));
        }
    }
}

日志输出

php 复制代码
[2024-10-15 17:42:55] local.INFO: this is test1 [] {"requestId":"28c06563-40ac-4b18-bb05-13101859fe58"} 
[2024-10-15 17:42:55] local.INFO: this is test2 [] {"requestId":"28c06563-40ac-4b18-bb05-13101859fe58"} 

丰富日志

针对一次请求的话,我们不能简单的记录请求路径和requestId

php 复制代码
[2024-10-15 18:06:09] local.INFO: GET http://acurd1.com/blog/list [] [] {"requestId":"8a95c1f2-9484-4e27-8afe-c302de3c56de"} 

一个完整的request请求我们要记录request和response信息。那么下面我们封装一下日志。

使用中间件记录request和response日志

  • 创建一个中间件 php artisan make:middleware AccessLog,将这个中间件注册到Http模块的Kenel.php中,全局注册。
  • AccessLog.php
php 复制代码
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Log;

class AccessLog
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $time = microtime(true);
        // 记录请求信息
        $requestMessage = [
            'url' => $request->url(),
            'method' => $request->method(),
            'ip' => $request->ips(),
            'headers' => $request->header('Authorization'),
            'params' => $request->all()
        ];
        Log::info("请求信息:", $requestMessage);

        $respone = $next($request);

        //响应的信息
        $responeData = [
            'respone' => json_decode($respone->getContent(), true) ?? ""
        ];
        $responeData['respone']['used_time']= microtime(true) - $time;

        Log::info("返回信息:", $responeData);

        return $respone;

    }
}
  • 日志记录
php 复制代码
[2024-10-15 19:05:48] local.INFO: 请求信息: {"url":"http://acurd1.com/blog/list","method":"GET","ip":["127.0.0.1"],"headers":null,"params":{"title":"php","catid":"21"}} {"requestId":"60ab6fa4-cebd-41c9-b7ce-80615d039bed"} 
[2024-10-15 19:05:51] local.INFO: 返回信息: {"respone":{"code":10000,"msg":"OK","data":[{"id":"3237453e7c","catid":21,"title":"auto_load自动加载机制"},{"id":"c025611928","catid":21,"title":"打造我的linux开发环境"}],"used_time":2.4246108531951904}} {"requestId":"60ab6fa4-cebd-41c9-b7ce-80615d039bed"} 

异常日志捕捉

在laravel中,处理异常是由App\Exceptions\Handler.php处理的,为了异常能报警,我们需要优化一下Handler的处理方式

php 复制代码
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;

class Handler extends ExceptionHandler
{
    /**
     * Render an exception into an HTTP response.
     *
     * @param Request $request
     * @param Exception $e
     * @return Response|JsonResponse
     */
    public function render($request, Exception $e)
    {
        //自定义的异常 直接返回信息
        if ($e instanceof SysException) {
            $resData = ['code' => $e->getCode(), 'msg' => $e->getMessage()];
            Log::error("自定义异常结果", $resData);
            return response()->json();
        }
        try {
            $errInfo = [
                'code' => $e->getCode(),
                'traceStr' => $e->getTraceAsString(),
                'clientIp' => $request->getClientIp(),
                'time' => date('Y-m-d H:i:s'),
                'msg' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
            ];

            Log::error("系统异常错误", $errInfo);
            //根据不同环境发送到不同的报警服务
            $env = app()->environment();

            //生产发送至webhook
            if (in_array($env, ['release', 'production'])) {

            }

            return response()->json(['code' => SysException::FAILED, 'msg' => '发生系统错误,请稍后重试']);

        } catch (Exception $e) {
            $errInfo = [
                'code' => $e->getCode(),
                'traceStr' => $e->getTraceAsString(),
                'clientIp' => $request->getClientIp(),
                'time' => date('Y-m-d H:i:s'),
                'msg' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
            ];
            Log::error("renderException", $errInfo);
        }
        return response()->json(['code' => SysException::FAILED, 'msg' => '捕捉异常错误,请稍后重试']);
        }

}

优化DB日志

laravel框架中,vendor/laravel/framework/src/Illuminate/Database实现了对数据库的事件触发,那么我们只需要监听数据库的事件即可记录相关的sql

  • 创建listener php artisan make:listener QueryListener
  • 在EventServiceProvider注册监听事件
php 复制代码
<?php

namespace App\Providers;

use App\Listeners\QueryListener;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        QueryExecuted::class=>[
            QueryListener::class
        ]
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

    }
}

logging文件配置记录sql日志

以天为单位存储,最长保留14天

php 复制代码
        'sql' => [
            'driver' => 'daily',
            'tap' => [\App\Lib\CustomizeFormatter::class],
            'path' => storage_path('logs/sql.log'),
            'level' => 'debug',
            'days' => 14,
        ],

QueryListener.php

php 复制代码
<?php

namespace App\Listeners;

use Illuminate\Support\Facades\Log;

class QueryListener
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param object $event
     * @return void
     */
    public function handle($event)
    {
        $sql = $event->sql;
        if (stristr($sql, ':')) {
            foreach ($event->bindings as $key => $val) {
                if (!is_numeric($val)) {
                    $val = "'{$val}'";
                }
                $sql = str_ireplace(':' . $key, $val, $sql);
            }
        } else {
            $sql = str_replace("?", "'%s'", $sql);
            $sql = vsprintf($sql, $event->bindings);
        }

        Log::channel("sql")->info($sql);

    }
}

我们请求一个接口后查看日志 laravel.log

php 复制代码
[2024-10-16 10:30:36] local.INFO: 请求信息: {"url":"http://acurd1.com/blog/list","method":"GET","ip":["127.0.0.1"],"headers":null,"params":{"title":"php","catid":"21"}} {"requestId":"aec1f2b9-54ec-4674-95f4-d0d0894bfd62"} 
[2024-10-16 10:30:36] local.INFO: 返回信息: {"respone":{"code":10000,"msg":"OK","data":[{"id":"3237453e7c","catid":21,"title":"auto_load自动加载机制"},{"id":"c025611928","catid":21,"title":"打造我的linux开发环境"}],"used_time":0.10953998565673828}} {"requestId":"aec1f2b9-54ec-4674-95f4-d0d0894bfd62"} 

sql.log

php 复制代码
[2024-10-16 10:30:36] local.INFO: select id,catid,title from cms_blog limit 2 [] {"requestId":"6195a1d4-0635-40da-baf0-7bcdfd254b44"} 

发现同一个业务,两个文件的requestId不一致,这是因为每次实例化CustomizeFormatter都会初始化一个requestId,我首先先到了使用单例模式解决这个问题。即在整个请求中,只初始化一个requestId即可,我们改造一下requestId的生成方式,创建一个新的单例类用来生成requestId GenerateRequestId.php

php 复制代码
<?php

namespace App\Lib;

use Illuminate\Support\Str;

class GenerateRequestId
{
    private static $instance;
    public $requestId;

    private function __construct()
    {
        $this->requestId = $this->getRequestId();
    }

    private function getRequestId()
    {
        return Str::uuid()->toString();
    }

    public static function getInstance()
    {
        if (empty(self::$instance)) {
            self::$instance = new self();
        }
        return self::$instance;
    }

}

这样就解决了同一个请求sql日志和业务日志的requestId是一致的。

慢日志报警

我们可以继续优化一下我们的sql日志,如果有执行时间超过1s的,可以error报警

php 复制代码
    public function handle(QueryExecuted $event)
    {
        $sql = $event->sql;
        if (stristr($sql, ':')) {
            foreach ($event->bindings as $key => $val) {
                if (!is_numeric($val)) {
                    $val = "'{$val}'";
                }
                $sql = str_ireplace(':' . $key, $val, $sql);
            }
        } else {
            $sql = str_replace("?", "'%s'", $sql);
            $sql = vsprintf($sql, $event->bindings);
        }

        if ($event->time > 1000){
            Log::channel("sql")->error("慢日志sql:".$sql);
            //webhook通知
            return;
        }
        Log::channel("sql")->info($sql);
    }

异步记录日志

我们知道,日志是写文件的,在一个要支持高并发的系统里面,写文件肯定会影响到系统的性能。那么我们能不能使用job,异步的记录日志呢。那么我们就需要写一个新的Log类,用来承接这个工作,那么我们就创建一个文件 AsyncLog.php

php 复制代码
<?php

namespace App\Lib;

use App\Jobs\AsyncLogJob;

class AsyncLog
{
    const LOG_QUEUE = 'site_log';
    private static $instance;
    private $requestId;
    private $channel;

    private function __construct($channel = '')
    {
        $this->channel = $channel;
        $this->requestId = GenerateRequestId::getInstance()->requestId;
    }

    public static function getInstance($channel = 'stack')
    {
        //不同的channel返回不同的实例 但是requestId是一致的
        if (empty(self::$instance) || self::$instance->channel != $channel) {
            self::$instance = new self($channel);
        }
        return self::$instance;

    }

    /**
     * Log an emergency message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function emergency(string $message, array $context = [])
    {
        AsyncLogJob::dispatch($this->channel, __FUNCTION__, $this->requestId, $message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log an alert message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function alert(string $message, array $context = [])
    {
        AsyncLogJob::dispatch($this->channel, __FUNCTION__, $this->requestId, $message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log a critical message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function critical(string $message, array $context = [])
    {
        AsyncLogJob::dispatch($this->channel, __FUNCTION__, $this->requestId, $message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log an error message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function error(string $message, array $context = [])
    {
        AsyncLogJob::dispatch($this->channel, __FUNCTION__, $this->requestId, $message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log a warning message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function warning(string $message, array $context = [])
    {
        AsyncLogJob::dispatch($this->channel, __FUNCTION__, $this->requestId, $message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log a notice to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function notice(string $message, array $context = [])
    {
        AsyncLogJob::dispatch($this->channel, __FUNCTION__, $this->requestId, $message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log an informational message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function info(string $message, array $context = [])
    {
        AsyncLogJob::dispatch($this->channel, __FUNCTION__, $this->requestId, $message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log a debug message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function debug(string $message, array $context = [])
    {
        AsyncLogJob::dispatch($this->channel, __FUNCTION__, $this->requestId, $message, $context)->onQueue(self::LOG_QUEUE);
    }


}

异步记录日志的队列 AsyncLogJob.php

php 复制代码
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class AsyncLogJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $level;
    protected $requestId;
    protected $message;
    protected $context;
    protected $channel;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($channel, $level, $requestId, $message, $context)
    {
        $this->level = $level;
        $this->message = $message;
        $this->context = $context;
        $this->requestId = $requestId;
        $this->channel = $channel;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $level = $this->level;
        $channel = $this->channel;
        //写入request_id
        $this->context['request_id'] = $this->requestId;
        Log::channel($channel)->$level($this->message, $this->context);
    }
}
  • supervisor执行任务脚本
php 复制代码
php artisan queue:work --queue=site_log --delay=3 --sleep=5 --memory=128 --timeout=60 --tries=3

业务日志输出

php 复制代码
[2024-10-16 17:34:29] local.INFO: 请求信息: {"url":"http://acurd1.com/blog/list","method":"GET","ip":["127.0.0.1"],"headers":null,"params":[],"request_id":"125190f1-9d8d-41f9-972e-9dbe95c92ee8"} [] 
[2024-10-16 17:34:29] local.INFO: 返回信息: {"respone":{"code":10000,"msg":"OK","data":[{"id":"3237453e7c","catid":21,"title":"auto_load自动加载机制"},{"id":"c025611928","catid":21,"title":"打造我的linux开发环境"}],"used_time":0.029488086700439453},"request_id":"125190f1-9d8d-41f9-972e-9dbe95c92ee8"} [] 
[2024-10-16 17:34:29] local.INFO: 请求信息: {"url":"http://acurd1.com/blog/list","method":"GET","ip":["127.0.0.1"],"headers":null,"params":[],"request_id":"ea7b63a2-3493-495d-836e-40685bd60c5f"} [] 
[2024-10-16 17:34:29] local.INFO: 返回信息: {"respone":{"code":10000,"msg":"OK","data":[{"id":"3237453e7c","catid":21,"title":"auto_load自动加载机制"},{"id":"c025611928","catid":21,"title":"打造我的linux开发环境"}],"used_time":0.056169986724853516},"request_id":"ea7b63a2-3493-495d-836e-40685bd60c5f"} [] 

数据库日志输出

php 复制代码
[2024-10-16 17:34:29] local.INFO: select id,catid,title from cms_blog limit 2 {"request_id":"125190f1-9d8d-41f9-972e-9dbe95c92ee8"} [] 
[2024-10-16 17:34:29] local.INFO: select id,catid,title from cms_blog limit 2 {"request_id":"ea7b63a2-3493-495d-836e-40685bd60c5f"} [] 

优化命令行脚本日志

在工作中,我们经常会遇到这样的困扰,sql日志里面执行了一条sql,但是不知道是哪个业务执行的,在request请求的业务日志里面,我们添加了requestId,但是php的脚本文件却难易追溯,基于此,我们继续优化php脚本

php 复制代码
<?php

namespace App\Console\Commands;

use App\Lib\AsyncLog;
use App\Models\Article;
use Illuminate\Console\Command;

class LogCommandTest extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'log:test';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
        // 获取整个命令行输入
        $commandInput = $_SERVER['argv'];
        $cmdLine = sprintf("%s %s %s", $this->description, "php", implode(" ", $commandInput));
        AsyncLog::getInstance()->info($cmdLine);
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $list = Article::query()->select("id", "title")->first()->toArray();
    }
}

这样就可以追溯到命令行脚本的整条日志链。

优化日志时间

我们看到,job消费的事件并不是日志真正记录的时间,所以我们在生成日志的时候应该把时间加上,即在AsyncLog里面追加上当前的时间,为了方便以后的日志分析,我们把时间加到context数组里面。

AsyncLog.php

php 复制代码
<?php

namespace App\Lib;

use App\Jobs\AsyncLogJob;

class AsyncLog
{
    const LOG_QUEUE = 'site_log';
    private static $instance;
    private $requestId;
    private $channel;

    private function __construct($channel = '')
    {
        $this->channel = $channel;
        $this->requestId = GenerateRequestId::getInstance()->requestId;
    }

    public static function getInstance($channel = 'stack')
    {
        //不同的channel返回不同的实例 但是requestId是一致的
        if (empty(self::$instance) || self::$instance->channel != $channel) {
            self::$instance = new self($channel);
        }
        return self::$instance;

    }

    /**
     * Log an emergency message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function emergency(string $message, array $context)
    {
        $context = $this->formatLog($context);
        AsyncLogJob::dispatch($this->channel, __FUNCTION__,$message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log an alert message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function alert(string $message, array $context = [])
    {
        $context = $this->formatLog($context);
        AsyncLogJob::dispatch($this->channel, __FUNCTION__,$message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log a critical message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function critical(string $message, array $context = [])
    {
        $context = $this->formatLog($context);
        AsyncLogJob::dispatch($this->channel, __FUNCTION__,$message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log an error message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function error(string $message, array $context = [])
    {
        $context = $this->formatLog($context);
        AsyncLogJob::dispatch($this->channel, __FUNCTION__,$message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log a warning message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function warning(string $message, array $context = [])
    {
        $context = $this->formatLog($context);
        AsyncLogJob::dispatch($this->channel, __FUNCTION__,$message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log a notice to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function notice(string $message, array $context = [])
    {
        $context = $this->formatLog($context);
        AsyncLogJob::dispatch($this->channel, __FUNCTION__,$message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log an informational message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function info(string $message, array $context = [])
    {
        $context = $this->formatLog($context);
        AsyncLogJob::dispatch($this->channel, __FUNCTION__,$message, $context)->onQueue(self::LOG_QUEUE);
    }

    /**
     * Log a debug message to the logs.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function debug(string $message, array $context = [])
    {
        $context = $this->formatLog($context);
        AsyncLogJob::dispatch($this->channel, __FUNCTION__,$message, $context)->onQueue(self::LOG_QUEUE);
    }

    //格式化日志数据,补充额外信息
    public function formatLog($context)
    {
        $context['request_id']=$this->requestId;
        $context['log_create_time']=date('Y-m-d H:i:s');
        $context['log_unix_time']=microtime(true);
        return $context;
    }
}

AsyncLogJob.php

php 复制代码
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class AsyncLogJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $level;
    protected $message;
    protected $context;
    protected $channel;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($channel, $level, $message, $context)
    {
        $this->level = $level;
        $this->message = $message;
        $this->context = $context;
        $this->channel = $channel;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $level = $this->level;
        $channel = $this->channel;
        Log::channel($channel)->$level($this->message, $this->context);
    }
}

最终,日志变成了我们想要的样子,如果想同步记录就用Log,如果想异步记录日志就用AsyncLog,非常方便。

业务日志

php 复制代码
[2024-10-16 18:14:58] local.INFO: Command description php artisan log:test {"request_id":"a0e84069-0207-40f3-92b3-9ee4c4857e38","log_create_time":"2024-10-16 18:14:54","log_unix_time":1729073694.74755} [] 
[2024-10-16 18:15:13] local.INFO: Command description php artisan log:test {"request_id":"6269bf91-3482-40a9-8e02-508db386dd61","log_create_time":"2024-10-16 18:15:12","log_unix_time":1729073712.882479} [] 
[2024-10-16 18:15:23] local.INFO: 请求信息: {"url":"http://acurd1.com/blog/list","method":"GET","ip":["127.0.0.1"],"headers":null,"params":[],"request_id":"4e77222d-e469-4261-a91e-1ec7b349d9d8","log_create_time":"2024-10-16 18:15:18","log_unix_time":1729073718.275437} [] 
[2024-10-16 18:15:23] local.INFO: 返回信息: {"respone":{"code":10000,"msg":"OK","data":[{"id":"3237453e7c","catid":21,"title":"auto_load自动加载机制"},{"id":"c025611928","catid":21,"title":"打造我的linux开发环境"}],"used_time":0.05006098747253418},"request_id":"4e77222d-e469-4261-a91e-1ec7b349d9d8","log_create_time":"2024-10-16 18:15:18","log_unix_time":1729073718.313444} [] 
[2024-10-16 18:15:38] local.INFO: 请求信息: {"url":"http://acurd1.com/blog/list","method":"GET","ip":["127.0.0.1"],"headers":null,"params":[],"request_id":"b57a1b01-a5ac-4a18-b431-eac7323930fb","log_create_time":"2024-10-16 18:15:34","log_unix_time":1729073734.577843} [] 
[2024-10-16 18:15:38] local.INFO: 返回信息: {"respone":{"code":10000,"msg":"OK","data":[{"id":"3237453e7c","catid":21,"title":"auto_load自动加载机制"},{"id":"c025611928","catid":21,"title":"打造我的linux开发环境"}],"used_time":0.02147698402404785},"request_id":"b57a1b01-a5ac-4a18-b431-eac7323930fb","log_create_time":"2024-10-16 18:15:34","log_unix_time":1729073734.598454} [] 

sql日志

php 复制代码
[2024-10-16 18:14:58] local.INFO: select `id`, `title` from `cms_blog` limit 1 {"request_id":"a0e84069-0207-40f3-92b3-9ee4c4857e38","log_create_time":"2024-10-16 18:14:54","log_unix_time":1729073694.780394} [] 
[2024-10-16 18:15:13] local.INFO: select `id`, `title` from `cms_blog` limit 1 {"request_id":"6269bf91-3482-40a9-8e02-508db386dd61","log_create_time":"2024-10-16 18:15:12","log_unix_time":1729073712.913971} [] 
[2024-10-16 18:15:23] local.INFO: select id,catid,title from cms_blog limit 2 {"request_id":"4e77222d-e469-4261-a91e-1ec7b349d9d8","log_create_time":"2024-10-16 18:15:18","log_unix_time":1729073718.309716} [] 
[2024-10-16 18:15:38] local.INFO: select id,catid,title from cms_blog limit 2 {"request_id":"b57a1b01-a5ac-4a18-b431-eac7323930fb","log_create_time":"2024-10-16 18:15:34","log_unix_time":1729073734.596944} [] 

后续我会把这套日志系统作为一个组件发布出来。

相关参考

日志库monolog laravel官方文档 关于Laravel日志你需要知道的一切 教你更优雅地写 API 之「记录日志」

相关推荐
追逐时光者5 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_5 小时前
敏捷开发流程-精简版
前端·后端
苏打水com6 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧7 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧7 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧7 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧7 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧7 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng9 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6019 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring