纯干货!基于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 之「记录日志」

相关推荐
Iced_Sheep34 分钟前
干掉 if else 之策略模式
后端·设计模式
XINGTECODE1 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
先天牛马圣体2 小时前
如何提升大型AI模型的智能水平
后端
java亮小白19972 小时前
Spring循环依赖如何解决的?
java·后端·spring
2301_811274312 小时前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
草莓base3 小时前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Ljw...3 小时前
表的增删改查(MySQL)
数据库·后端·mysql·表的增删查改