Laravel 使用 Seq 作为日志存储

1,753 阅读2分钟

laravel-logo-white.png

简介

在日常开发里面,找Bug需要经常用到日志,但是Laravel默认日志是存储到服务器本地文件中,每次去查日志都需要进入服务器,使用vicat等命令进行查看,而且当服务器水平扩展的时候,日志文件分布在不同容器内,让我们查日志更是难上加难,无法对日志进行集中分析。

今天会把Laravel日志接入到Seq里面,让日志可以统一管理、集中分析,让日志变成我们找Bug的好帮手。

Seq 是用于结构化应用程序日志数据的实时搜索和分析服务器,类似ElasticSearch。

准备Seq

我们需要一台Seq服务器,可以直接在Seq官方下载安装,支持Windows安装包或Docker方式。

因为Seq没有提供PHP SDK,所以我们使用HTTP方式写入日志。

POST https://localhost:5341/api/events/raw?clef

Body格式化如下:

{
  "Events": [{
    "Timestamp": "2021-12-02T22:00:00.12345+10:00",
    "Level": "Warning",
    "MessageTemplate": "Disk space is low on {Drive}",
    "Properties": {
      "Drive": "C:",
      "MachineName": "nblumhardt-rmbp"
    }
  }]
}

使用Postman测试一下,可以成功写入到Seq。

Snipaste_2021-12-02_16-51-14.jpg

Snipaste_2021-12-02_16-50-59.jpg

Laravel 日志组件

Laravel的日志组件默认是使用 Seldaek/monolog 去实现,所以我们可以使用更换不同的 Monolog Handler 去改变日志处理的方式,Monolog 官方有提供 RedisHandler、MongoDBHandler、ElasticsearchHandler 等等,当然你也可以去 Packagist 寻找其他第三方的 Handler。

Laravel 日志组件自定义配置

Laravel 日志组件提供自定义 Monolog Handler 的方法:

Laravel 版本 <= 5.5 方法 官方文档

你可以使用 configureMonologUsing 方法来配置应用程序对 Monolog 的完全控制。在 $app 变量返回之前,在 bootstrap/app.php 文件中调用此方法。

$app->configureMonologUsing(function (Monolog\Logger $monolog) {
    $redis = new Predis\Client('tcp://127.0.0.1:6379');
    $handler = new \Monolog\Handler\RedisHandler($redis, 'logs');
    $monolog->pushHandler($handler);
});

return $app;

Laravel 版本 >= 5.6 方法 官方文档

你可以在 config/logging.php 创建一个 channels,配置不同的 MonoLog Handler,with 是配置 Handler 的构建函数。

'redislog' => [
    'driver' => 'monolog',
    'handler' => \Monolog\Handler\RedisHandler::class,
    // with
    'with' => [
        'redis' => new \Predis\Client('tcp://127.0.0.1:6379'),
        'key' => 'logs',
    ],
],

覆盖 LogServiceProvider 方法

根据 Laravel 服务容器特性,你还可以自己创建一个 LogServiceProvider 去覆盖 Laravel 自身的 LogServiceProvider

  • app/Log 下创建 LogManager.php,继承 Illuminate\Log\LogManager
<?php

namespace App\Log;

use Illuminate\Log\LogManager as BaseLogManager;

class LogManager extends BaseLogManager
{
    // TODO: 重写对应的方法
}
  • app/Providers 下创建 LogServiceProvider.php
<?php

namespace App\Providers;

use App\Log\LogManager;
use Illuminate\Log\LogServiceProvider as BaseLogServiceProvider;

class LogServiceProvider extends BaseLogServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('log', function () {
            return new LogManager($this->app);
        });
    }
}
  • config/app.php 中的 providers 数组,追加 App\Providers\LogServiceProvider::class
'providers' => [
    /*
     * Application Service Providers...
     */
    App\Providers\AppServiceProvider::class,
    App\Providers\AuthServiceProvider::class,
    App\Providers\EventServiceProvider::class,
    App\Providers\RouteServiceProvider::class,
    App\Providers\LogServiceProvider::class, // <-- LogServiceProvider
],

正常情况下,不建议使用覆盖 LogServiceProvider 的方式,因为在 Laravel 5.6 之后日志组件已经比较自由了,很多地方都可以通过 config/logging.php 去配置。

Laravel 日志组件接入Seq

因为这次需要接入的项目是 Laravel 5.5,我们使用官方的自定义 Monolog Handler 方式,通过自定义 Handler ,把Log写入到Seq里面。

  • app/Utilities/SeqMonologHandler 创建 SeqHandler.php

通过自定义 Handler,可以实现不同的接入方式,具体可以参考 Monolog 官方文档

<?php

namespace App\Utilities\SeqMonologHandler;

use Carbon\Carbon;
use GuzzleHttp\Client;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;

class SeqHandler extends AbstractProcessingHandler
{
    const SEQ_API_URI = '/api/events/raw';

    protected $logLevelMap = [
        '100' => 'Debug',
        '200' => 'Information',
        '250' => 'Information',
        '300' => 'Warning',
        '400' => 'Error',
        '500' => 'Error',
        '550' => 'Fatal',
        '600' => 'Fatal',
    ];

    protected $client;

    protected $serverUrl;

    public function __construct(string $serverUrl = '', $level = Logger::DEBUG, $bubble = true)
    {
        $this->serverUrl = $serverUrl;

        $this->client = new Client();

        parent::__construct($level, $bubble);
    }

    protected function write(array $record)
    {
        $url = $this->serverUrl . SeqHandler::SEQ_API_URI;

        $eventBody = [
            'Events' => [[
                'Timestamp' => Carbon::now()->toIso8601String(),
                "Level" => $this->logLevelMap[$record['level']] ?: 'Information',
                "MessageTemplate" => $record['formatted'],
            ]]
        ];

        try {
            $this->client->post($url, [
                "body" => json_encode($eventBody)
            ]);
        } catch (\Exception $exception) {
            return;
        }
    }
}
  • 然后在 bootstrap\app.phpreturn $app 之前,配置一下 Monolog Handler
$app->configureMonologUsing(function (Monolog\Logger $monolog) {
    $handler = new App\Utilities\SeqMonologHandler\SeqHandler('http://127.0.0.1:5341');
    $monolog->pushHandler($handler);
});
  • 测试一下效果

Snipaste_2021-12-03_08-07-32.jpg

参考资源

learnku.com/articles/37…

laravel.com/docs/5.6/lo…

laravel.com/docs/5.5/er…