.../articles/

Laravelのログ出力のカスタマイズ

Laravelのアクセスログとクエリログを独自フォーマットで出力する方法をご紹介します。

環境

  • Laravel6

Loggingクラスの作成

まず、app/Logging/ にActiondログの内容を定義するFormatterクラスを作成します。ActionとQueryは出力内容に関するフォーマット部分が違っています。また、これは環境にもよるのですが Auth::id() によりユーザIDを取得する際にDBへのアクセスが発生することがある為、QueryLogFormatterでは staticの変数 $disableUserId を使ってそれを制御しています。この制御方法はもっと適切な方法がありましたら教えて欲しいです。


// app/Logging/ActionLogFormatter.php
<?php 
namespace App\Logging;

use Illuminate\Support\Facades\Auth;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\UidProcessor;

class ActionLogFormatter
{
    private $dateFormat = 'Y-m-d H:i:s.v';
    public function __invoke($logger)
    {
        // ログのフォーマットと日付のフォーマットを指定する
        $format = '[%datetime%] action %channel%.%level_name% %extra.uid% %extra.userid%" %message% %context% ' . PHP_EOL;
        $lineFormatter = new LineFormatter($format, $this->dateFormat, true, true);

        // 出力されるログをリクエストと紐付けるためのIDをつけるための処理
        $uidProcessor = app()->make(UidProcessor::class);

        // ログ出力から除外するnamespaceの指定
        $introProcessor = new IntrospectionProcessor(Logger::DEBUG, [
            'Monolog\\',
            'Illuminate\\',
            'App\\Providers\\',
            'App\\Logging\\',
        ]);

        // フォーマとおよび内容の設定
        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter($lineFormatter);
            $handler->pushProcessor($introProcessor);
            $handler->pushProcessor($uidProcessor);
            $handler->pushProcessor(function (array $record) {
                $record['extra']['userid'] = Auth::id() ?? '';
                return $record;
            });
        }
    }
}
// app/Logging/QueryLogFormatter.php
<?php
namespace App\Logging;
use Illuminate\Support\Facades\Auth;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\UidProcessor;

class QueryLogFormatter
{
    /**
     * NOTE
     *  ユーザIDを取得する Auth::id() を実行するとDBアクセスが発生してクエリログ出力の無限ループを回避するためのフラグ
     * @var bool
     */
    private static bool $disableUserId = false;

    private $dateFormat = 'Y-m-d H:i:s.v';
    public function __invoke($logger)
    {
        $format = '[%datetime%] query %channel%.%level_name% %extra.uid% %extra.userid% query %extra.file%(%extra.line%) %message% %context% ' . PHP_EOL;
        if (self::$disableUserId) {
            $format = '[%datetime%] query %channel%.%level_name% %extra.uid% %extra.file%(%extra.line%) %message% %context% ' . PHP_EOL;
        }

        // ログのフォーマットと日付のフォーマットを指定する
        $lineFormatter = new LineFormatter($format, $this->dateFormat, true, true);

        $uidProcessor = app()->make(UidProcessor::class);
        $introProcessor = new IntrospectionProcessor(Logger::DEBUG, [
            'Monolog\\',
            'Illuminate\\',
            'App\\Providers\\',
            'App\\Logging\\',
        ]);

        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter($lineFormatter);
            $handler->pushProcessor($introProcessor);
            $handler->pushProcessor($uidProcessor);

            $handler->pushProcessor(function (array $record) {
                if (self::$disableUserId) {
                    return $record;
                }
                try {
                    self::$disableUserId = true;
                    $record['extra']['userid'] = Auth::id() ?? '';
                } finally {
                    self::$disableUserId = false;
                }
                return $record;
            });
        }
    }
}

logging.phpの設定

次に、Laravelではログについての設定は config/logging.php に記述する。このファイルはインストール時に作成されているので編集します。作成したイベントとクエリのログを出力するためのチャンネルを追加します。tapに先程作ったFormatterクラスを指定することで、ログ出力時に指定されたチャンネルでFormatterを切り替えることができます。

// config/logging.php
<?php
return [
    'channels' => [

        ...

        'action' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'info',
            'days' => 7,
            'permission' => 0777,
            'tap' => [App\Logging\ActionLogFormatter::class],
        ],

        'query' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'info',
            'days' => 7,
            'permission' => 0777,
            'tap' => [App\Logging\QueryLogFormatter::class],
        ],
    ],
];

ログを出力する

出力するを行う箇所にログ出力を行うコードを実装します。当然アクションログとクエリログでは出力されるタイミングが違うので実装する場所も違ってきます。

アクションログ

アクションログはMiddlewareにLarvelがリクエストを受け取ったタイミングでログを出力したいので、Middlewareを作成してそこでログ出力をします。また、パスワードや個人情報などの平文でログに残すべきではない内容はこのクラスでマスクを行います。

// Http/Middleware/ActionLogMiddleware.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
class ActionLogMiddleware
{
    public function handle($request, Closure $next)
    {
        $logContext = [
            'url' => $request->path(),
            'request' => $this->maskRequest($request->all()),
        ];
        Log::channel('action')->info($request->method(), $logContext);
        return $next($request);
    }

    public function maskRequest($params)
    {
            # password、password_confirmationという名目で送られてきた内容のマスクをしています。
        # 他の項目にもマスクをかける場合はここに追記します    
        array_walk_recursive($params, function (&$val, $key) {
            if (($key === 'password')||($key === 'password_confirmation')) {
                $val = '********';
            }
        });
        return $params;
    }
}

クエリログ

クエリログはSQLが実行されたタイミングでログを出力します。Laravelでこれを実現するには、クエリログ出力用のServiceProviderを作成してアプリケーションに登録を行います。

// Providers/QueryLogServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Database\Events\TransactionBeginning;
use Illuminate\Database\Events\TransactionCommitted;
use Illuminate\Database\Events\TransactionRolledBack;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;

class QueryLogServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        DB::listen(function ($query) {
            $sql = $query->sql;
            for ($i = 0; $i < count($query->bindings); $i++) {
                if (! is_object($query->bindings[$i])) {
                    $sql = preg_replace("/\?/", $query->bindings[$i], $sql, 1);
                } else {
                    $sql = preg_replace("/\?/", date_format($query->bindings[$i], 'Y-m-d'), $sql, 1);
                }
            }
            $this->writeLog($sql);
        });

        Event::listen(TransactionBeginning::class, function (TransactionBeginning $event): void {
            $this->writeLog('begin transaction');
        });

        Event::listen(TransactionCommitted::class, function (TransactionCommitted $event): void {
            $this->writeLog('commit transaction');
        });

        Event::listen(TransactionRolledBack::class, function (TransactionRolledBack $event): void {
            $this->writeLog('rollback transaction');
        });
    }

    private function writeLog($msg)
    {
        Log::channel('query')->info($msg);
    }
}
// config/app.php
<?php
return [

    ...

    'providers' => [

        ...

        \App\Providers\QueryLogServiceProvider::class

        ...
    ]
    ...

]

参考にした記事

Laravel SQLの実行クエリログを出力する
Laravel のログにリクエストごとのIDを出す

.../articles/

Articles

記事

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

monorepo管理しているNext.jsをAmplifyにデプロイしようとした際にいくつか躓く内容があったのでまとめておきます。

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

コロナ禍の影響により、リモートワークの導入をおこなっている制作会社も多く、実際に弊社でも導入しています。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトのデザインは、「この形式」という決まりはありません。ECサイトで売り上げを上げるなら、しっかりとしたコンセプトと、コンセプトを決定するまでのリサーチが必要です。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

新型コロナウイルス感染拡大の影響で、リモートワークが主流になり、弊社でも週のほとんどは各自宅で作業をしています。

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaを利用するWebデザイナーの助けになれると嬉しいです。使い方は本記事をご覧ください。

ECの構築方法、おすすめのECサービス。

ECの構築方法、おすすめのECサービス。

ファッションや家電、スーパーの買い物でさえもECサイトを利用することが当たり前になりました。加えて新型コロナウイルスの影響もあり、弊社にも「どんなプラットフォームを利用したら良いか」「どれくらいコストがかかるのか」などECに関するさまざまなご相談を頂きます。

FastAPIのスキーマクラスをOpenAPIから生成する方法

FastAPIのスキーマクラスをOpenAPIから生成する方法

PythonでAPIを構築する要件があり、フレームワークに比較的モダンなFastAPIを採用しました。FastAPIはバックエンドの開発を行えば自動でOepnApi定義を生成する機能が備わっていますが、今回はこれを使わず、事前に用意したOepnApi定義からFastAPIで利用するスキーマクラスを生成する方法を紹介します。

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel + MySQLで全文検索を実装する

とあるPythonのソースで sys.path.append としたく無かった話

とあるPythonのソースで sys.path.append としたく無かった話

とあるプロジェクトのとあるソースコードのレビューをしてた時、「ソースコードの参照がうまくいってなかったので修正しました」とレビュー依頼がきました。 ディレクトリ構造 ``` module L __init__.py L main.py L tests L __init__.py L test_main.py ``` ソースコード ``` python tests/test_main.py sys.path.append(os.path.abspath("..")) from main import fuga ``` 今まで案件でPythonに触れる機会も結構ありましたが、なんとなく使ってきた部分も多く、この書き方が良いのか悪いのか判別できなかったので、改めてPythonのモジュールのインポートに関して調べてみたのでブログにしました。普段PHPを書いている事が多くPythonに関して何も分からないので初心者向けの内容になっていると思います。

GiFT1号目新卒デザイナーの2021年振り返り

GiFT1号目新卒デザイナーの2021年振り返り

いつの間に、年末ですね。入社してもう、9ヶ月も立っていたようです。2021年の振り返りを記事にしました。

すべての記事

お問い合わせ