PHP
laravel
monolog
laravel5.6

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: