LoginSignup
13
13

More than 3 years have passed since last update.

Laravel(monolog)で構造化ログを実装する

Last updated at Posted at 2019-06-14

構造化ログ

JSON形式でログを出力しちゃおう、という話です。
構造化ロギング

人間にわかりやすいログ

[2019-05-23 10:42:51] local.ERROR: Division by zero in /var/www/hoge.jp/app/Dummy.php:81

構造化ログ

{"level":"ERROR","datetime":"2019-06-04 04:54:32","env":"local","message":"Division by zero","file":"\/var\/www\/hoge.jp\/app\/Dummy.php","line":81,"extra":[]}

なにがいいのか

例えば本番環境のログを調べたいときに、サーバ8台分のログ全部をgrepして、tailして、catして…とかは正直うんざりです。場合によってはawkが必要だったり、perlあたりでなにかコマンド書く必要があるかもしれません。

ならば最初からJSONで出力し、かつ大枠のフォーマットを決めておけばどうでしょう。
ログをElasticsearchなどに投入しておけば、あとでKIBANAを用いていくらでも検索が可能で、目的のログをいち早く分析できます。2019-01-01 00:00:00〜2019-03-31 23:59:59までのAWS S3に関するエラーコード「ERR-001」のログ一覧を表示、なんてことも容易になります。

Laravelで構造化ログを出力する

Laravelで、と書いてますが Laravelはmonologを使っています ので、monolog で、ということになります。

monolog

詳細は省いて概要だけまとめます。

Handler

monologの物理的な出力を管理するのがHandlerです。
例えばRedisにログを吐き出すHandlerや、ログローテートをしてくれるHandlerなどがあります。

Formatter

読んで字のごとく、ログのフォーマットを管理します。Handlerに設定できます。

構造化ログに変換するには

上記から察するに Formatter でごにょごにょすればよさそうです。

受け渡す情報はどうするか

しかし、ログメッセージからどのように情報を取得すればいいでしょうか。
2019-01-01 00:00:00〜2019-03-31 23:59:59までのAWS S3に関するエラーコード「ERR-001」のログ一覧を出すためには、少なくとも「AWS S3に関するログだよ」&「エラーコードはERR-001だよ」を渡したいですが

Log::error('ERROR!');

上記のようなLaravelのログファサードを使って情報を詰めるにはちょっと限界がありそうです。

context

しかし、ログファサードには第2引数を記述することができます。配列で指定するcontextは、好きなものを詰めてログにわたすことができます。

Log::error('ERROR!', ['genre' => 'AWS-S3', 'code' => 'ERR-001']);

こんな感じで情報は引き渡せます。

Formatterの記述

じつに簡単です。サンプルプログラムを見てもらうと分かる通り、ただの配列操作です。最後にjsonにしてreturnします。
今回はNormalizerFormatterを継承し、それをオーバーライドする形で記述してみました。

<?php
declare(strict_types=1);

namespace App\Logging\Formatters;

use Carbon\Carbon;
use Illuminate\Hashing\HashManager;
use Monolog\Formatter\NormalizerFormatter;

/**
 * Class ApplicationLogFormatter
 * @package App\Logging\Formatters
 */
class ApplicationLogFormatter extends NormalizerFormatter
{
    /**
     * @var array
     */
    private $keys = [
        'code',
        'genre',
    ];

    /**
     * ログフォーマッタ
     * @param array $record
     * @return string
     */
    public function format(array $record): string
    {
        $formatted = parent::format($record);
        $segments = [
            'level' => $formatted['level_name'],
            'datetime' => $formatted['datetime'],
            'env' => $formatted['channel'],
            'message' => $formatted['message'],
            'extra' => $formatted['extra'],
        ];

        foreach ($this->keys as $key) {
            if (isset($formatted['context'][$key])) {
                $segments[$key] = $formatted['context'][$key];
            }
        }
        return json_encode($segments) . PHP_EOL;
    }
}

Formatterを設定する

Laravelのマニュアルにあるように、独自のFormatterを設定するには、ロガーインスタンスのカスタマイズが必要です。
しかし難しいことはなにひとつなく、なんとconfigに書くだけです。

'single' => [
    'driver' => 'single',
    'tap' => [App\Logging\Customizer::class],
    'path' => storage_path('logs/laravel.log'),
    'level' => 'debug',
],
<?php

namespace App\Logging;

use App\Logging\Formatters\ApplicationLogFormatter;

class Customizer
{
    /**
     * Customize the given logger instance.
     *
     * @param  \Illuminate\Log\Logger  $logger
     * @return void
     */
    public function __invoke($logger)
    {
        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter(app()->make(ApplicationLogFormatter::class));
        }
    }
}

サンプル

{"level":"ERROR","datetime":"2019-06-04 04:54:32","env":"local","message":"ERROR!","genre":"AWS-S3","code":"ERR-001","extra":[]}

この形でElasticsearchなどに登録されていれば、ログは一気に見つけやすく、分析しやすくなります。

まとめ

上記は簡単なサンプルだけですので、例えばException発生時のtrace情報はどうしよう、とかいろいろ設定の余地があります。
しかし、大体は配列と文字列操作だけの話なので、特段難しいことはないと思います。

自分の環境ではlocal環境では通常のログ、dev環境以降は構造化ログにしてみたり、contextのキーをいちいち書くのが面倒なので、オリジナルのLoggerファサードを作り、

AppLog::genre('AWS-S3')->code('ERR-001')->error('ERROR!);

みたいな記述ができるようにしています。

13
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
13