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

# はじめに
Laravel5.6 でログの仕様が変わり(<a href="https://qiita.com/a_yasui/items/553c46015e320d6a574d#logging" target="_blank">Laravel 5.6 Release & 5.5 機能差分メモ</a>)、Laravel5.5 までの手法が使えなくなりましたので、Laravel5.6 での手法についての覚書です。

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

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

「[[5.6] Fix cache for loggers](https://github.com/laravel/framework/pull/23118)」で修正されたものが既にマージされていますので 5.6.4 のリリースではバグが直っているはずです。

# コンフィグファイル
[`config/logging.php`](https://github.com/laravel/laravel/blob/develop/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 インスタンスへのアクセス

```php
$monolog = Log::getLogger();
```

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

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

# ログの出力

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


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

```php
// 「\」を入れないで使うには下記の一文を入れておくこと
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()` を使用した記述

```php
// 「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()` を使用した記述

```php
// チャンネル名を指定しない場合は「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 を使用した記述(ログの出力場所が関数の中になるのであまりお勧めではない)

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

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

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

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

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

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

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

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

# チャンネル
[`config/logging.php`](https://github.com/laravel/laravel/blob/develop/config/logging.php) の `channels` キーの中に各チャンネルを登録していきます。

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

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

[Handlers, Formatters and Processors](https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md)

## レベルの指定

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

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

### レベルの使い分け参考

 - [ログレベルちゃんと使い分けてますか？](http://engineering.otobank.co.jp/entry/2016/09/20/181756)
 - [Yahoo!ショッピングにおけるログ設計と監視](https://techblog.yahoo.co.jp/web/shopping/yahoo_1/)

## 各ドライバの仕様

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` レベルのログも出力する例

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

```php: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.');
    }
}

```

```php: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 フィールドも出力してみます。

```php: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% に出力する日付のフォーマット
                ]),
            ],
        ],

        ...
    ],
];
```

```php: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 レベルでログ出力します。

```php: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);
    }
}

```

```php: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 を追加するミドルウェア
    ];
```

```php: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: