背景
我们现在有一个项目,仅针对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 之「记录日志」