Help us understand the problem. What is going on with this article?

Laravel5.6 での ログ設定について

More than 1 year has passed since last update.

はじめに

Laravel5.6 でログの仕様が変わり(Laravel 5.6 Release & 5.5 機能差分メモ)、Laravel5.5 までの手法が使えなくなりましたので、Laravel5.6 での手法についての覚書です。

バージョン 5.6.3 時点でのバグ

「ルーティングのグループ単位でログを分割したい」といった時にミドルウェアで新しいログハンドラを追加したりしたいと思っても正常には動作しません。

Monolog のインスタンスを取得し、変更を行ったとしても Monolog のインスタンスを保持している Illuminate\Log\Logger インスタンスそのものがキャッシュされていないため、次回インスタンス呼び出し時(ログ出力毎)に再度新しいインスタンスが作成されてしまうためです。

[5.6] Fix cache for loggers」で修正されたものが既にマージされていますので 5.6.4 のリリースではバグが直っているはずです。

コンフィグファイル

config/logging.php が追加されており、Laravel5.6 へのアップグレード時にはファイルの追加が必須です。
合わせて config/app.php にあった loglog_max_fileslog_levellog_channel といったログ関係の設定は使用されません。

カスタム Monolog 設定

Laravel5.5 までは bootstrap/app.php 内でログの拡張を行っていましたが、Laravel5.6 では後述するチャンネルに拡張用のクラスをセットして拡張を行います。

Monolog インスタンスへのアクセス

Laravel5.5 までは $monolog = Log::getMonolog(); といった記述でMonologインスタンスを取得していましたが、Laravel5.6 では getMonolog() というメソッドは廃止されています。

デフォルトチャンネルの Monolog インスタンスへのアクセス

$monolog = Log::getLogger();

特定のチャンネルの Monolog インスタンスへのアクセス

$monolog = Log::channel(チャンネル名)->getLogger();

ログの出力

Laravel ではログの出力の記述方法が複数あります。

Log ファサードを使用した記述

// 「\」を入れないで使うには下記の一文を入れておくこと
use Illuminate\Support\Facades\Log;

// ログファサードでは「Illuminate\Log\LogManager」のインスタンスを取得する

// DEBUG
Log::debug($message, $context);

// INFO
Log::info($message, $context);

// NOTICE
Log::notice($message, $context);

// WARNING
Log::warning($message, $context);

// ERROR
Log::error($message, $context);

// ERROR
Log::error($message, $context);

// CRITICAL
Log::critical($message, $context);

// ALERT
Log::alert($message, $context);

// EMERGENCY
Log::emergency($message, $context);

helper の logger() を使用した記述

// 「logger()」は引数を指定しないと「Illuminate\Log\LogManager」のインスタンスを返す

// DEBUG
logger()->debug($message, $context);

// INFO
logger()->info($message, $context);

// NOTICE
logger()->notice($message, $context);

// WARNING
logger()->warning($message, $context);

// ERROR
logger()->error($message, $context);

// CRITICAL
logger()->critical($message, $context);

// ALERT
logger()->alert($message, $context);

// EMERGENCY
logger()->emergency($message, $context);

helper で logs() を使用した記述

// チャンネル名を指定しない場合は「Illuminate\Log\LogManager」のインスタンスを返す
// チャンネル名を指定した場合は「Illuminate\Log\Logger」のインスタンスを返す

// DEBUG
logs(チャンネル名)->debug($message, $context);

// INFO
logs(チャンネル名)->info($message, $context);

// NOTICE
logs(チャンネル名)->notice($message, $context);

// WARNING
logs(チャンネル名)->warning($message, $context);

// ERROR
logs(チャンネル名)->error($message, $context);

// CRITICAL
logs(チャンネル名)->critical($message, $context);

// ALERT
logs(チャンネル名)->alert($message, $context);

// EMERGENCY
logs(チャンネル名)->emergency($message, $context);

helper を使用した記述(ログの出力場所が関数の中になるのであまりお勧めではない)

// デフォルトのチャンネルに書き込む

// DEBUG
logger($message, $context);

// INFO
info($message, $context);

デフォルトチャンネル以外のチャンネルでのログ出力

Illuminate\Log\LogManager のインスタンスからチャンネルを指定して Illuminate\Log\Logger のインスタンスを取得し debug() 等のメソッドを実行します。
Illuminate\Log\LogManager のインスタンスで debug() 等のメソッドを実行した場合にはデフォルトのチャンネルに対してメソッドが実行されます。

Log::channel('slack')->info('Something happened!');

また、複数チャンネルでログ出力を行いたい場合は下記のようにstack を使用することで、それぞれのチャンネルでログ出力を行うことができます。

Log::stack(['single', 'slack'])->info('Something happened!');

チャンネル

config/logging.phpchannels キーの中に各チャンネルを登録していきます。

各チャンネルは driver キーでドライバを指定します。ドライバは何か特別なクラスがあるわけではなく、customstack 以外は Monolog の Handler を指定しているだけです。
ですので、各チャンネルの driver 以外のキーは一部を除き Handler を初期化するときに必要なパラメータになります。

デフォルト以外にも多くの Handler が存在しますので、デフォルトには無い Handler を使用するには custom で専用のクラスを作成して使用するか、Log::extend() でオリジナルのドライバを作成して使用します。

Handlers, Formatters and Processors

レベルの指定

レベルの指定は文字列で行います。

level Monologのレベル
debug Monolog::DEBUG
info Monolog::INFO
notice Monolog::NOTICE
warning Monolog::WARNING
error Monolog::ERROR
critical Monolog::CRITICAL
alert Monolog::EMERGENCY

レベルの使い分け参考

各ドライバの仕様

handler の引数の指定以外に 'name' => 'examle', とすることでログのフォーマットの %channel% で出力される値を変更することができます。
指定のない場合は config/app.phpenv の値が %channel% に出力されます。

single

単一のログファイルを出力
'driver' => 'single',StreamHandler を指定します。

Handler の引数 対応するキー キーに指定する型 備考
$stream path string 出力するファイルのフルパス
$level level string 出力するログレベル
$bubble 無し -
$filePermission 無し -
$useLocking 無し -

daily

日付毎のログファイルを出力
'driver' => 'daily',RotatingFileHandler を指定します。
「ファイル名-2018-02-15.log」といった感じでファイル名の後ろに日付が付きます。

Handler の引数 対応するキー キーに指定する型 備考
$filename path string 出力するファイルのフルパス
$maxFiles days integer 出力したログファイルを残す数(指定のない場合は7ファイル)
$level level string 出力するログレベル
bubble 無し -
$filePermission 無し -
$useLocking 無し -

slack

Slack にログを出力
'driver' => 'slack',SlackWebhookHandler を指定します。

Handler の引数 対応するキー キーに指定する型 備考
$webhookUrl url string Slack の Webhook URL
$channel channel string 出力先のチャンネル名(指定のない場合は null)
$username username string ユーザ名(指定のない場合は laravel)
$useAttachment attachment bool メッセージを添付ファイルとしてSlackに追加するかどうか(指定のない場合は true)
$iconEmoji emoji string 使用する絵文字の名前(指定のない場合は :boom:)
$useShortAttachment short bool 添付ファイルとして Slack に追加された context/extra を短いスタイルにするかどうか(指定のない場合は false)
$includeContextAndExtra context bool 添付ファイルに context/extra を含めるかどうか
$level level string 出力するログレベル
$bubble 無し -
$excludeFields 無し -

syslog

syslog にログを出力
'driver' => 'syslog',SyslogHandler を指定します。

Handler の引数 対応するキー キーに指定する型 備考
$ident app.name string config/app.phpname を使用
$facility facility string facility を指定(指定のない場合は定数 LOG_USER)
$level level string 出力するログレベル
$bubble 無し -
$logopts 無し -

errorlog

errorlog() を使用してログを出力
'driver' => 'errorlog',ErrorLogHandler を指定します。

Handler の引数 対応するキー キーに指定する型 備考
$messageType type integer エラーの発生場所を指定(指定のない場合は ErrorLogHandler::OPERATING_SYSTEM)
$level level string 出力するログレベル
$bubble 無し -
$expandNewlines 無し -

custom

自身で Monolog のインスタンスを作成します。
'driver' => 'custom', で指定します。

キー キーに指定する型 備考
via string App\Logging\CreateCustomLogger::class といった感じで拡張用のクラスを指定
その他 mixed 拡張用クラスに渡す引数に指定するものを自由に指定

'driver' => 'custom', の例

常は info レベルでログを出力するが error レベル以上で debug レベルのログも出力する例

config/logging.php
'channels' => [
    'fingers' => [
        'driver'     => 'custom',
        'via'        => App\Logging\FingersCrossedLogger::class,
        'path'       => storage_path('logs/laravel.log'),
        'level'      => 'debug', // 指定したハンドラで出力するログレベル
        'activation' => 'error', // このログレベル以上で指定したハンドラで出力するレベルのログを出力する
        'pass'       => 'info', // このログレベル以上は常に出力する
    ],
],
app/Logging/LogDriverAbstract.php
<?php

namespace App\Logging;

use Illuminate\Log\Logger;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\HandlerInterface;
use Monolog\Logger as Monolog;
use Psr\Log\LoggerInterface;

abstract class LogDriverAbstract
{
    /**
     * The Log levels.
     *
     * @var array
     */
    protected $levels = [
        'debug'     => Monolog::DEBUG,
        'info'      => Monolog::INFO,
        'notice'    => Monolog::NOTICE,
        'warning'   => Monolog::WARNING,
        'error'     => Monolog::ERROR,
        'critical'  => Monolog::CRITICAL,
        'alert'     => Monolog::ALERT,
        'emergency' => Monolog::EMERGENCY,
    ];

    /**
     * Apply the configured taps for the logger.
     *
     * @param array                    $config
     * @param \Psr\Log\LoggerInterface $logger
     *
     * @return \Psr\Log\LoggerInterface
     */
    protected function tap(array $config, LoggerInterface $logger)
    {
        foreach ($config['tap'] ?? [] as $tap) {
            list($class, $arguments) = $this->parseTap($tap);

            app()->make($class)->__invoke($logger, ...explode(',', $arguments));
        }

        return $logger;
    }

    /**
     * Parse the given tap class string into a class name and arguments string.
     *
     * @param string $tap
     *
     * @return array
     */
    protected function parseTap($tap)
    {
        return Str::contains($tap, ':') ? explode(':', $tap, 2) : [$tap, ''];
    }

    /**
     * Prepare the handler for usage by Monolog.
     *
     * @param \Monolog\Handler\HandlerInterface $handler
     *
     * @return \Monolog\Handler\HandlerInterface
     */
    protected function prepareHandler(HandlerInterface $handler)
    {
        return $handler->setFormatter($this->formatter());
    }

    /**
     * Get a Monolog formatter instance.
     *
     * @return \Monolog\Formatter\FormatterInterface
     */
    protected function formatter()
    {
        return tap(new LineFormatter(null, null, true, true), function ($formatter) {
            $formatter->includeStacktraces();
        });
    }

    /**
     * Extract the log channel from the given configuration.
     *
     * @param array $config
     *
     * @return string
     */
    protected function parseChannel(array $config)
    {
        if (!isset($config['name'])) {
            return app()->bound('env') ? app()->environment() : 'production';
        }

        return $config['name'];
    }

    /**
     * Parse the string level into a Monolog constant.
     *
     * @param array $config
     *
     * @return int
     *
     * @throws \InvalidArgumentException
     */
    protected function level(array $config)
    {
        $level = $config['level'] ?? 'debug';

        if (isset($this->levels[$level])) {
            return $this->levels[$level];
        }

        throw new InvalidArgumentException('Invalid log level.');
    }
}

app/Logging/FingersCrossedLogger.php
<?php

namespace App\Logging;

use Monolog\Logger;

class FingersCrossedLogger extends LogDriverAbstract
{
    /**
     * Create a custom Monolog instance.
     *
     * @param  array  $config config/logging.php で指定した fingers 以下のものを取得できる
     * @return \Monolog\Logger
     */
    public function __invoke(array $config)
    {
        // StreamHandler を生成
        $handler = $this->prepareHandler(
            new StreamHandler($config['path'], $this->level($config))
        );

        // ログに出力するフォーマット
        $format = '[%datetime% %channel%.%level_name%] %message% [%context%] [%extra%]';

        // StreamHandler にフォーマッタをセット
        $handler->setFormatter(
            tap(new LineFormatter($format, null, true, true), function ($formatter) {
                $formatter->includeStacktraces();
            })
        );

        // Monolog のインスタンスを生成して返す
        return new Logger($this->parseChannel($config), [
            new FingersCrossedHandler(
                $handler,
                $config['activation'] ?? null,
                0,
                true,
                true,
                $config['pass'] ?? null
            )
        ]);
    }
}

stack

複数のチャンネルのハンドラをまとめます。
'driver' => 'stack', で指定します。

キー キーに指定する型 備考
channels array ['syslog', 'slack'] といった感じでチャンネルを指定

ドライバの拡張

tap キーに拡張用のクラスを指定することでチャンネルの Monolog のインスタンスを拡張することができます。
tap キーは配列で複数のクラスを登録できるので、拡張したい項目ごとに複数に分けていると管理しやすくなるでしょう。

拡張用クラスの引数

拡張用のクラスにはデフォルトで Monolog のインスタンスが引数として渡されますが、tap のクラス名を「クラス名:引数,引数,引数」といったようにクラス名の後ろに : を付け、その後ろに渡したい引数を , 区切りで入力していくとクラスの引数として渡すことができます。

extra フィールドをログに出力する拡張例

daily でログファイルを出力するので日付は「時:分:秒.ミリ秒」で出力し、それぞれの extra フィールドも出力してみます。

config/logging.php
return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'days' => 7,
            'tap' => [
                App\Logging\LoggerCustomize::class . ':' . implode(',', [
                    '[%datetime% %channel%.%level_name%] %message% %context% [%extra.class%::%extra.function%(%extra.line%)【ip:%extra.ip%】]'.PHP_EOL, // ログのフォーマット
                    'H:i:s.v' // ログフォーマットの %datetime% に出力する日付のフォーマット
                ]),
            ],
        ],

        ...
    ],
];
app/Logging/LoggerCustomize.php
<?php

namespace App\Logging;

use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\WebProcessor;

class LoggerCustomize
{
    /**
     * Customize the given Monolog instance.
     *
     * @param  \Monolog\Logger  $monolog
     * @param  string           $logFormat  config/logging.php で指定した最初の引数
     * @param  string           $dateFormat config/logging.php で指定した2番目の引数
     *
     * @return void
     */
    public function __invoke(Logger $monolog, string $logFormat, string $dateFormat): void
    {
        // 新しいフォーマッタを生成
        $formatter = new LineFormatter($logFormat, $dateFormat, true, true);

        // クラス名等を extra フィールドに挿入するプロセッサを生成
        $ip = new IntrospectionProcessor(Logger::DEBUG, ['Illuminate\\']);

        // IPアドレス等を extra フィールドに挿入するプロセッサを生成
        $wp = new WebProcessor();

        foreach ($monolog->getHandlers() as $handler) {
            // 各ログハンドラにフォーマッタとプロセッサを設定
            $handler->setFormatter($formatter);
            $handler->pushProcessor($ip);
            $handler->pushProcessor($wp);
        }
    }
}

ミドルウェアでデフォルトのチャンネルにログハンドラを追加する例

アプリケーションの"api"ルート定義のミドルウェア設定でハンドラ追加用のミドルウェアを指定してログハンドラを追加してみます。
ここで追加したログハンドラで作成されるファイルは「2018-02-15_api.log」といった日付毎のファイルで INFO レベルでログ出力します。

app/Middleware/AddLogHandler.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Log;
use Monolog\Handler\StreamHandler;

class AddLogHandler
{
    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure                 $next
     * @param string                   $logName  ログに使用する名前
     * @param string                   $logLevel ログ出力のレベル
     *
     * @return mixed
     */
    public function handle($request, Closure $next, string $logName = '', string $logLevel = 'debug')
    {
        // デフォルトチャンネルのMonolog インスタンスを取得
        $logger = Log::getLogger();

        // ログの保存先
        $filePath = storage_path('logs').DIRECTORY_SEPARATOR.date('Y-m-d_').$logName.'.log';

        // 新しい StreamHandler を生成
        $handler = new StreamHandler($filePath, $logLevel);

        // Handler を追加
        $logger->pushHandler($handler);

        return $next($request);
    }
}

app/Http/Kernel.php
    protected $routeMiddleware = [
        'auth'          => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic'    => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings'      => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can'           => \Illuminate\Auth\Middleware\Authorize::class,
        'guest'         => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle'      => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'log'           => \App\Http\Middleware\AddLogHandler::class, // ログに Handler を追加するミドルウェア
    ];
app/Providers/RouteServiceProvider.php
    protected function mapApiRoutes()
    {
        Route::prefix('api')
             ->middleware('log:api,info', 'api') // ログに Handler を追加するミドルウェアに引数をつけて指定
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }

最後に

Illuminate\Log\Logger のインスタンスはチャンネルが呼び出されるときに生成されるので、複数のチャンネルを config/logging.php に記述しておいて、本番や開発ごとに stack のドライバでチャンネルを指定する感じでしょうか。
フォーマットはどのチャンネルでも同じでしょうから tap でまとめて指定するのを想定しいるのかな。
Laravel5.5 までよりは柔軟に運用できそうでいい感じ:relaxed:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away