日志
前言
开发了多个服务 A、B、C,希望有个服务,对全局的日志进行管理。
参照 session 共享机制,各服务将日志写入 redis,由日志服务进行持久化和管理。
感谢 教你更优雅地写API之「记录日志」 提供实现思路
配置
.env
// 服务标记,区分日志属于哪个服务
APP_NAME=log
// 统一前缀
REDIS_PREFIX=ms_database_
database.php
将日志写到 redis
'redis' => [
'log' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_QUEUE_DB', 3),
],
],
logging.php
自定义的 channels,主要是扩展 Handler,用于区分日志类别
use Extend\Logger\Handlers;
use Monolog\Formatter\JsonFormatter;
'channels' => [
'debug' => [
'name' => 'debug',
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => Handlers\DebugHandler::class,
'formatter' => JsonFormatter::class,
],
'request' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => Handlers\RequestHandler::class,
'formatter' => JsonFormatter::class,
],
'exception' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => Handlers\ExceptionHandler::class,
'formatter' => JsonFormatter::class,
],
]
文件
目录结构
根目录增加 Extend 目录,放置自己的类库(composer扩展包开发还没学会)
Logger 文件夹
- Events
- RequestHandledEvent.php
…
- RequestHandledEvent.php
- Handlers
- RequestHandler.php
…
- RequestHandler.php
- Listeners
- RequestHandledListener.php
…
- RequestHandledListener.php
- Schedules
- RequestSchedule.php
…
- RequestSchedule.php
流程
App\Providers\EventServiceProvider 注册事件和监听器
use Extend\Logger\Events;
use Extend\Logger\Listeners;
protected $listen = [
Events\RequestArrivedEvent::class => [Listeners\RequestArrivedListener::class],
Events\RequestHandledEvent::class => [Listeners\RequestHandledListener::class],
Events\DebugEvent::class => [Listeners\DebugListener::class],
Events\ExceptionEvent::class => [Listeners\ExceptionListener::class],
];
App\Http\Kernel 埋点
protected $middlewareGroups = [
'api' => [
\Extend\Logger\Http\Middleware\RequestLog::class,
],
];
Extend\Logger\Http\Middleware\RequestLog 触发事件
declare(strict_types=1);
namespace Extend\Logger\Http\Middleware;
use Extend\Logger\Events\RequestArrivedEvent;
use Extend\Logger\Events\RequestHandledEvent;
use Closure;
use Illuminate\Http\Request;
class RequestLog
{
public function handle(Request $request, Closure $next)
{
event(new RequestArrivedEvent($request));
return $next($request);
}
public function terminate(Request $request, $response)
{
event(new RequestHandledEvent($request, $response));
}
}
代码示例
event
主要是绑定运行时参数
declare(strict_types=1);
namespace Extend\Logger\Events;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Event;
class RequestHandledEvent extends Event
{
private Request $request;
public $response;
public function __construct(Request $request, $response)
{
$this->request = $request;
$this->response = $response;
}
public function getRequest(): Request
{
return $this->request;
}
public function getResponse()
{
return $this->response;
}
}
listener
解析 event, 记录 log。其中 channel 对应 logging.php 里的配置
declare(strict_types=1);
namespace Extend\Logger\Listeners;
use Extend\Logger\Events\RequestHandledEvent;
use Illuminate\Support\Facades\Log;
use Monolog\Logger;
use Monolog\Processor\WebProcessor;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
class RequestHandledListener
{
public function handle(RequestHandledEvent $event)
{
$request_id = $event->getRequest()->server->get('UNIQUE_ID') ?: '';
$start = $event->getRequest()->server('REQUEST_TIME_FLOAT');
$end = microtime(true);
$request = $event->getRequest()->all();
if ($files = $event->getRequest()->allFiles()) {
foreach ($files as $key => $uploadedFile) {
$request[$key] = [
'originalName' => $uploadedFile->getClientOriginalName(),
'mimeType' => $uploadedFile->getClientMimeType(),
];
}
}
$response = $event->response instanceof SymfonyResponse
? json_decode($event->response->getContent(), true)
: (string) $event->response;
$context = [
'serverless' => env('APP_NAME', 'ms'),
'request_id' => $request_id,
'request' => $request,
'response' => $response,
'start' => $start,
'end' => $end,
];
$message = 'request';
/** @var Logger $logger */
$logger = Log::channel('request');
$logger->pushProcessor(new WebProcessor(request()->server()));
$logger->info($message, $context);
}
}
handler
采用 redis 记录
declare(strict_types=1);
namespace Extend\Logger\Handlers;
use Illuminate\Support\Facades\Redis;
use Monolog\Handler\RedisHandler;
use Monolog\Logger;
class RequestHandler extends RedisHandler
{
public function __construct($level = Logger::DEBUG, bool $bubble = true, int $capSize = 0)
{
$redis = Redis::connection('log')->client();
parent::__construct($redis, 'request', $level, $bubble, $capSize);
}
}
schedule
用于将 redis 数据保存到数据库
declare(strict_types=1);
namespace Extend\Logger\Schedules;
use Illuminate\Support\Facades\Redis;
abstract class BaseSchedule
{
protected const KEY = '';
protected const LIMIT = '1';
public function __invoke()
{
config(['logging.query_executed' => false]);
$redis = Redis::connection('log')->client();
$limit = static::LIMIT;
do {
$value = $redis->lPop(static::KEY);
if (is_null($value)) {
break;
}
if (!$info = json_decode($value, true)) {
$message = json_last_error_msg();
event(new DebugEvent($message, ['message' => $message, 'value' => $value]));
continue;
}
DB::beginTransaction();
$this->createInfo($info);
DB::commit();
} while ($limit--);
config(['logging.query_executed' => true]);
}
abstract protected function createInfo($info);
}
<?php
declare(strict_types=1);
namespace Extend\Logger\Schedules;
use App\Models\Tables\RequestModel;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class RequestSchedule extends BaseSchedule
{
protected const KEY = 'request';
protected const LIMIT = '100';
protected function createInfo($info)
{
$model = new RequestModel();
$model->request_id = $info['message'];
$model->level = $info['level_name'];
$context = $info['context'];
$model->serverless = $context['serverless'];
$model->params = json_encode($context['params'], JSON_UNESCAPED_UNICODE);
$extra = $info['extra'];
$model->url = $extra['url'];
$model->ip = $extra['ip'];
$model->http_method = $extra['http_method'];
$model->server = $extra['server'];
$model->referrer = $extra['referrer'] ?: '';
$model->save();
}
}
后记
平时日志直接用 Log::info 记录到文件,使用起来不是很方便。接触了overtrue/laravel-query-logger
才发现日志其实可以扩展和自定义,决定自己试试。
一边查资料,一边看monolog
源码,断断续续总算是让服务跑起来了,大概是这么回事:
- 运行时记录(不中断运行)
// 旧 $a->save(); Log::info('a保存成功'); $b->save(); // 新 $a->save(); event(new DebugEvent('a保存成功')); $b->save();
- 运行时记录(中断运行)
// 旧 if(!$request->id){ Log::error('异常了'); abort(500, '异常了'); } // 新 // App\Exceptions\Handler $this->renderable(function (\Exception $e, Request $request) { event(new ExceptionEvent($e, $request)); });
- 自定义事件
参看上面的 reqeust - 系统事件绑定自定义监听
// sql 记录 QueryExecuted::class => [Listeners\QueryListener::class],
还不成熟,感谢阅读
推荐文章: