はじめに
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
にあった log
、log_max_files
、log_level
、log_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.php
の channels
キーの中に各チャンネルを登録していきます。
各チャンネルは driver
キーでドライバを指定します。ドライバは何か特別なクラスがあるわけではなく、custom
と stack
以外は 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.php
の env
の値が %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.php の name を使用 |
$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
レベルのログも出力する例
'channels' => [
'fingers' => [
'driver' => 'custom',
'via' => App\Logging\FingersCrossedLogger::class,
'path' => storage_path('logs/laravel.log'),
'level' => 'debug', // 指定したハンドラで出力するログレベル
'activation' => 'error', // このログレベル以上で指定したハンドラで出力するレベルのログを出力する
'pass' => 'info', // このログレベル以上は常に出力する
],
],
<?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.');
}
}
<?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 フィールドも出力してみます。
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% に出力する日付のフォーマット
]),
],
],
...
],
];
<?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 レベルでログ出力します。
<?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);
}
}
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 を追加するミドルウェア
];
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 までよりは柔軟に運用できそうでいい感じ