前言
日常工作中,專案不管是在開發階段,還是已經部署上線,記錄日誌來分析專案的執行情況都是很常用的方式。比如,通過記錄 SQL 日誌來檢查是否有 N+1
問題,通過記錄請求日誌來分析 API 的響應時間等。
在 lumen-api-starter 中針對記錄日誌進行了優化,可以更方便地來記錄 SQL 和 Request日誌。
關於lumen-api-starter 的介紹可以參考系列文章的第二篇是時候使用 Lumen 8 + API Resource 開發專案了!。
本篇是在前一篇的基礎上,重新整理並獨立出了 package,可以同時支援最新版本 Laravel 和 Lumen 專案。
相關地址:
Package 地址:laravel-logger
Laravel 版本 Api 開發初始化專案:laravel-api-starter
Lumen 版本 Api 開發初始化專案:lumen-api-starter
實現過程
需求
- 能夠有效地記錄 SQL 日誌,來分析隱藏的
N+1
、慢查詢等問題 - 能夠記錄 API 請求日誌,來分析響應時間;
- 可以追溯單次請求中的執行過程(併發場景下傳統檔案日誌方式可能會出現序列)
- 日誌能夠更容易地檢視和搜尋
- 能夠反向追溯應用中所有記錄日誌的節點
- 日誌記錄不會額外對業務處理增加負擔
- 能夠靈活地配置開啟和關閉
功能
- 提供
logger_async
輔助函式,通過非同步 Job 方式來記錄日誌; - 增加 RequestLog 中介軟體來記錄 api 的請求和響應;對於單個請求關聯
UNIQUE_ID
,根據UNIQUE_ID
可以跟蹤請求執行過程 - 適配 MongoDB 驅動,支援記錄日誌到 MongoDB;collection 支援按天、按月和按年拆分;
- 日誌的 message 適配 laravel-enum,統一日誌的 message 格式;方便統計專案中記錄日誌的節點
- 提供
LOG_QUERY
、LOG_REQUEST
配置引數來開啟關閉 sql 日誌和 request 日誌
思路
- 通過 DB::listen 來監聽 SQL 的執行並生成 SQL 日誌記錄
- 通過 RequestLog 中介軟體中的 handle 方法來生成請求
UNIQUE_ID
,terminate 方法來記錄 Request 日誌 - 在單次請求過程中,所有的日誌記錄都關聯 RequestLog 中介軟體中產生的
UNIQUE_ID
- 日誌支援非同步方式記錄,來減少業務負擔;(LogJob 走 default 佇列,生產環境可以將優先順序高的非同步任務放在其他佇列中)
logger_async
輔助函式,用來便捷排程 LogJob
實現
針對 larave-logger原始碼分析下大致流程。
- RequestLog 中介軟體中觸發
RequestArrivedEvent
和RequestHandledEvent
事件 RequestArrivedListener
監聽RequestArrivedEvent
事件,往 server 中寫入UNIQUE_ID
- RequestHandledListener 監聽
RequestHandledEvent
事件,通過logger_async
排程 LogJob logger_async
輔助函式中排程 LogJob 時末尾引數傳入request()->server()
function logger_async(string $message, array $context = [])
{
return dispatch(new \Jiannei\Logger\Laravel\Jobs\LogJob($message, $context, request()->server()));
}
- LogJob 中通過
Monolog\Logger
的WebProcessor
來增加額外的 SERVER 資訊(url、ip、http_method 等)到日誌中
// vendor/jiannei/laravel-logger/src/Jobs/LogJob.php
<?php
namespace Jiannei\Logger\Laravel\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Monolog\Logger;
use Monolog\Processor\WebProcessor;
use Psr\Log\LoggerInterface;
class LogJob implements ShouldQueue
{
use InteractsWithQueue;
use Queueable;
private $context;
private $message;
private $serverData;
public function __construct(string $message, array $context = null, array $serverData = null)
{
$this->message = $message;
$this->context = $context;
$this->serverData = $serverData;
}
public function handle()
{
app()->forgetInstance(LoggerInterface::class);
// unset(app()[LoggerInterface::class]);
$logger = app(LoggerInterface::class)->getLogger();
if ($logger instanceof Logger) {
$logger->pushProcessor(new WebProcessor($this->serverData));
}
$logger->debug($this->message, $this->context);
}
}
Monolog\Logger
的WebProcessor
中檢查 serverData 中是否有UNIQUE_ID,如果有則記錄到日誌的 extra 欄位中
// vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php
<?php
// ...
class WebProcessor implements ProcessorInterface
{
/**
* @var array|\ArrayAccess
*/
protected $serverData;
/**
* Default fields
*
* Array is structured as [key in record.extra => key in $serverData]
*
* @var array
*/
protected $extraFields = [
'url' => 'REQUEST_URI',
'ip' => 'REMOTE_ADDR',
'http_method' => 'REQUEST_METHOD',
'server' => 'SERVER_NAME',
'referrer' => 'HTTP_REFERER',
];
public function __construct($serverData = null, array $extraFields = null)
{
if (null === $serverData) {
$this->serverData = &$_SERVER;
} elseif (is_array($serverData) || $serverData instanceof \ArrayAccess) {
$this->serverData = $serverData;
} else {
throw new \UnexpectedValueException('$serverData must be an array or object implementing ArrayAccess.');
}
if (isset($this->serverData['UNIQUE_ID'])) {
$this->extraFields['unique_id'] = 'UNIQUE_ID';
}
if (null !== $extraFields) {
if (isset($extraFields[0])) {
foreach (array_keys($this->extraFields) as $fieldName) {
if (!in_array($fieldName, $extraFields)) {
unset($this->extraFields[$fieldName]);
}
}
} else {
$this->extraFields = $extraFields;
}
}
}
如何使用
安裝
composer require jiannei/laravel-enum -vvv
composer require jiannei/laravel-logger -vvv
配置
- 註冊服務容器(Laravel 可以省略這一步驟)
Lumen在 bootstrap/app.php
中註冊服務容器
$app->register(\Jiannei\Logger\Laravel\Providers\ServiceProvider::class);
- 中介軟體啟用
Laravel 在 app/Http/Kernel.php
的 $middlewareGroups 中新增
protected $middlewareGroups = [
'api' => [
\Jiannei\Logger\Laravel\Http\Middleware\RequestLog::class,// 加在這個地方
],
];
Lumen 在 bootstrap/app.php
中新增
$app->middleware([
\Jiannei\Logger\Laravel\Http\Middleware\RequestLog::class,
]);
- 修改配置引數
config/logging.php
中增加以下配置,並根據自身實際情況進行部分調整,可以對比檢視最終配置 lumen-api-starter
'channels' => [// 下面這些移到原先的 channels 中
'mongo' => [
'driver' => 'custom', // 此處必須為 custom
'via' => \Jiannei\Logger\Laravel\MongoLogger::class, // 當 driver 設定為 custom 時,使用 via 配置項所指向的工廠類建立 logger
'channel' => env('LOG_MONGODB_CHANNEL', 'mongo'),
'level' => env('LOG_MONGODB_LEVEL', 'debug'), // 日誌級別
'separate' => env('LOG_MONGODB_SEPARATE', false), // false,daily,monthly,yearly
'host' => env('LOG_MONGODB_HOST', config('database.connections.mongodb.host')),
'port' => env('LOG_MONGODB_PORT', config('database.connections.mongodb.port')),
'username' => env('LOG_MONGODB_USERNAME', config('database.connections.mongodb.username')),
'password' => env('LOG_MONGODB_PASSWORD', config('database.connections.mongodb.password')),
'database' => env('LOG_MONGODB_DATABASE', config('database.connections.mongodb.database')),
],
],
// 下面是新增項,移到檔案末尾即可
'enum' => \Jiannei\Enum\Laravel\Repositories\Enums\LogEnum::class,
'query' => [
'enabled' => env('LOG_QUERY', false),
// Only record queries that are slower than the following time
// Unit: milliseconds
'slower_than' => 0,
],
'request' => [
'enabled' => env('LOG_REQUEST', false),
],
.env
中配置
LOG_CHANNEL=mongo
LOG_SLACK_WEBHOOK_URL=
LOG_QUERY=true
LOG_REQUEST=true
LOG_MONGODB_SEPARATE=daily
LOG_MONGODB_LEVEL=debug
# 如果使用的是 mongo channel 需要配置
MONGODB_HOST=mongo
MONGODB_PORT=27017
MONGODB_DATABASE=lumen-api
MONGODB_USERNAME=
MONGODB_PASSWORD=
MONGODB_AUTHENTICATION_DATABASE=admin
- 如果需要記錄日誌到 MongoDB,需要先安裝並配置laravel-mongodb
如何使用
可以參考 lumen-api-starter 中的實際使用示例。
使用
app/Repositories/Enums/LogEnum.php
中定義記錄日誌時的 message- 通過 logger_async 方法記錄日誌
logger_async(LogEnum::SYSTEM_SQL, $arrayData);
- 如果佇列任務非同步執行,則需要開啟佇列消費
php artisan queue:work
日誌內容
- 記錄到檔案中的日誌內容
[2021-01-18 12:03:36] local.DEBUG: System sql {"database":"lumen-api","duration":"11.08ms","sql":"select `roles`.*, `model_has_roles`.`model_id` as `pivot_model_id`, `model_has_roles`.`role_id` as `pivot_role_id`, `model_has_roles`.`model_type` as `pivot_model_type` from `roles` inner join `model_has_roles` on `roles`.`id` = `model_has_roles`.`role_id` where `model_has_roles`.`model_id` = '11' and `model_has_roles`.`model_type` = 'App\\\\Repositories\\\\Models\\\\User'"} {"url":"/users","ip":"172.22.0.1","http_method":"get","server":"lumen-api.test","referrer":null,"unique_id":"43f54ea9-4ad4-47cf-b9da-1d3aa150ff61"}
[2021-01-18 12:03:36] local.DEBUG: System request {"request":[],"response":{"status":"success","code":200,"message":"操作成功","data":{"data":[{"id":1,"nickname":"Evert Stracke DVM","email":"aufderhar.kaden@example.net"},{"id":2,"nickname":"Milton Toy","email":"keagan.eichmann@example.org"},{"id":3,"nickname":"Mrs. Alyce O'Hara","email":"cartwright.sidney@example.org"},{"id":4,"nickname":"Prof. Evalyn Windler I","email":"bertram.bartoletti@example.org"},{"id":5,"nickname":"Brant Skiles","email":"jane16@example.net"},{"id":6,"nickname":"Sage Rodriguez I","email":"ryder50@example.org"},{"id":7,"nickname":"Ms. Angelica Wiegand DVM","email":"kaelyn.mueller@example.net"},{"id":8,"nickname":"Newton Zieme","email":"sipes.kip@example.com"},{"id":9,"nickname":"Natalia Ruecker","email":"stroman.kiley@example.com"},{"id":10,"nickname":"Hallie Parisian","email":"rosina74@example.net"},{"id":11,"nickname":"Jiannei","email":"longjian.huang@foxmail.com"}],"meta":{"pagination":{"total":11,"count":11,"per_page":15,"current_page":1,"total_pages":1,"links":[]}}},"error":[]},"start":1610942614.450748,"end":1610942615.785696,"duration":"1.33s"} {"url":"/users","ip":"172.22.0.1","http_method":"GET","server":"lumen-api.test","referrer":null,"unique_id":"43f54ea9-4ad4-47cf-b9da-1d3aa150ff61"}
- 記錄到 Mongodb 的日誌內容
/* 1 */
{
"_id" : ObjectId("60050999ee7d025d4c62c8c2"),
"message" : "System sql",
"context" : {
"database" : "lumen-api",
"duration" : "54.19ms",
"sql" : "select count(*) as aggregate from `users`"
},
"level" : 100,
"level_name" : "DEBUG",
"channel" : "mongo",
"datetime" : ISODate("2021-01-18T12:07:53.410+08:00"),
"extra" : {
"url" : "/users",
"ip" : "172.22.0.1",
"http_method" : "get",
"server" : "lumen-api.test",
"referrer" : null,
"unique_id" : "0cda1927-bf14-4acf-88e8-1d9ed67170b5"
}
}
/* 2 */
{
"_id" : ObjectId("60050999ee7d025d4c62c8c3"),
"message" : "System sql",
"context" : {
"database" : "lumen-api",
"duration" : "2.42ms",
"sql" : "select * from `users` limit 15 offset 0"
},
"level" : 100,
"level_name" : "DEBUG",
"channel" : "mongo",
"datetime" : ISODate("2021-01-18T12:07:53.500+08:00"),
"extra" : {
"url" : "/users",
"ip" : "172.22.0.1",
"http_method" : "get",
"server" : "lumen-api.test",
"referrer" : null,
"unique_id" : "0cda1927-bf14-4acf-88e8-1d9ed67170b5"
}
}
特別說明
- SQL 日誌記錄參考:laravel-query-logger
其他
如果對您的日常工作有所幫助或啟發,歡迎三連 star + fork + follow
。
如果有任何批評建議,通過郵箱(longjian.huang@foxmail.com)的方式可以聯絡到我。
總之,歡迎各路英雄好漢。
QQ 群:1105120693
本作品採用《CC 協議》,轉載必須註明作者和本文連結