現在、個人的にちょっとしたAPIをLumenで実装しています。
Lumenオシャンティ!! って感じで意気揚々とやっていたのですが、ログ周りに関して 前回 のやり方だと困った場面に遭遇しました。
そこで今回は、何に困ったのか、最終的にどうしたのかを綴ってみたいと思います。
※ Lumenのバージョンは前回と一緒です
何をしたかったのか
まず、前回何をしたかったのかというと
すべてのログを好きなHandler(Monolog)でコントロールしたかった
というものがありました。
※ 最初、ログの出し方すらつまずきましたが
そこで以下の方法を前回はとったわけです。
(前回は例として、NativeMailerHandler も使いましたが今回は省略します)
app/bootstrap.php を修正
<?php
require_once __DIR__.'/../vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
try {
(new Dotenv\Dotenv(__DIR__.'/../'))->load();
} catch (Dotenv\Exception\InvalidPathException $e) {
//
}
// ~ 略 ~
$app = new Laravel\Lumen\Application(
realpath(__DIR__.'/../')
);
// $app->withFacades();
// $app->withEloquent();
/*
|--------------------------------------------------------------------------
| Register Container Bindings
|--------------------------------------------------------------------------
|
| Now we will register a few bindings in the service container. We will
| register the exception handler and the console kernel. You may add
| your own bindings here if you like or you can make another file.
|
*/
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
// これ
$app->singleton('log', function() {
$handlers[] = (
new RotatingFileHandler(
storage_path('logs/app.log') // ログファイル名を変える
)
)->setFormatter(new LineFormatter(null, null, true, true));
// ログのchannel名 を app にする
return new Logger('app', $handlers);
});
// ~ 略 ~
return $app;
で、使う側はこんな感じにします。
$logger = app('log');
$logger->info('Hello Lumen!!');
こうすることによって任意の場所、好きなHandlerでログ(storage/logs/app-{Y-m-d}.log)を出すことが出来ました。
困ったこと
で、何に困ったかというと例外が発生した際です。
例外が発生した際にログを確認すると、見事にデフォルトの storage/logs/lumen.log が出力されているではあーりませんか。
※ storage/logs/app-{Y-m-d}.log にも書き出されていない
え、うそでしょ?マジで?誰がこのログ吐いているの?と、普通のエンジニアならそうなるわけで、えぇ、ソースを追ってみました。
※ 最初に追っとけよ
例外時デフォルトのログを吐く犯人
で、ソースを追ってみたところデフォルトのログを吐く犯人は Laravel\Lumen\Exceptions\Handler::report(); だというのが分かりました。
※ 実際には子クラスである、App\Exceptions\Handler が呼び出しています
vendor\laravel\lumen-framework\src\Exceptions\Handler.php
class Handler implements ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [];
/**
* Report or log an exception.
*
* @param \Exception $e
* @return void
*/
public function report(Exception $e)
{
if ($this->shouldntReport($e)) {
return;
}
try {
$logger = app('Psr\Log\LoggerInterface');
} catch (Exception $ex) {
throw $e; // throw the original exception
}
$logger->error($e);
}
// ~ 略 ~
まぁ、これを見ると、ははぁーーん、app('Psr\Log\LoggerInterface'); ね。こいつが犯人ね。
となるわけです。
で、最初どうしたかというと、
app/bootstrap.php
<?php
// ~ 略 ~
$app->singleton('log', function() {
$handlers[] = (
new RotatingFileHandler(
storage_path('logs/app.log') // ログファイル名を変えてみる
)
)->setFormatter(new LineFormatter(null, null, true, true));
// ログのchannel名 を app にしてみる
return new Logger('app', $handlers);
});
// 追加 全部のログをコントロールしたいならこうしちゃえー
$app->singleton('Psr\Log\LoggerInterface', function() {
$handlers[] = (
new RotatingFileHandler(
storage_path('logs/app.log')
)
)->setFormatter(new LineFormatter(null, null, true, true));
return new Logger('app', $handlers);
});
// ~ 略 ~
return $app;
こうしたわけなんですね。
but, これでも例外が発生した場合、lumen.log が見事に吐き出されました。
もちろん、storage/logs/app-{Y-m-d}.log へは何も書き出されていません。
try {
$logger = app('log');
} catch (Exception $ex) {
throw $e; // throw the original exception
}
多分、こんな感じで直接コアを弄っちゃうか、子クラスなどでオーバーライドすれば上手くいく気がするのですが、それだとなんだか負けた気がします。
※ コアを弄るのはそもそもあれですし
そこで他の方法を探る旅に出まして、最終的に以下の実装を行いました。
最終的にどうしたのか
app/bootstrap.php を以下のように修正しました。
<?php
require_once __DIR__.'/../vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
try {
(new Dotenv\Dotenv(__DIR__.'/../'))->load();
} catch (Dotenv\Exception\InvalidPathException $e) {
//
}
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| Here we will load the environment and create the application instance
| that serves as the central piece of this framework. We'll use this
| application as an "IoC" container and router for this framework.
|
*/
$app = new Laravel\Lumen\Application(
realpath(__DIR__.'/../')
);
// $app->withFacades();
// $app->withEloquent();
/*
|--------------------------------------------------------------------------
| Register Container Bindings
|--------------------------------------------------------------------------
|
| Now we will register a few bindings in the service container. We will
| register the exception handler and the console kernel. You may add
| your own bindings here if you like or you can make another file.
|
*/
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
/*
|--------------------------------------------------------------------------
| Register Middleware
|--------------------------------------------------------------------------
|
| Next, we will register the middleware with the application. These can
| be global middleware that run before and after each request into a
| route or middleware that'll be assigned to some specific routes.
|
*/
// $app->middleware([
// App\Http\Middleware\ExampleMiddleware::class
// ]);
// $app->routeMiddleware([
// 'auth' => App\Http\Middleware\Authenticate::class,
// ]);
/*
|--------------------------------------------------------------------------
| Register Service Providers
|--------------------------------------------------------------------------
|
| Here we will register all of the application's service providers which
| are used to bind services into the container. Service providers are
| totally optional, so you are not required to uncomment this line.
|
*/
// $app->register(App\Providers\AppServiceProvider::class);
// $app->register(App\Providers\AuthServiceProvider::class);
// $app->register(App\Providers\EventServiceProvider::class);
/*
|--------------------------------------------------------------------------
| Define a callback to be used to configure Monolog
|--------------------------------------------------------------------------
*/
// これ
$app->configureMonologUsing(function($monolog) {
$handlers[] = (
new RotatingFileHandler(
storage_path('logs/app.log')
)
)->setFormatter(new LineFormatter(null, null, true, true));
$monolog->setHandlers($handlers);
return $monolog;
});
/*
|--------------------------------------------------------------------------
| Load The Application Routes
|--------------------------------------------------------------------------
|
| Next we will include the routes file so that they can all be added to
| the application. This will provide all of the URLs the application
| can respond to, as well as the controllers that may handle them.
|
*/
$app->group(['namespace' => 'App\Http\Controllers'], function ($app) {
require __DIR__.'/../routes/web.php';
});
return $app;
はい。これだけです。
$app->configureMonologUsing(); で、MonologのHandlerを定義してあげるだけです。
これで、app('log'); でも app('Psr\Log\LoggerInterface'); でも、上記の$app->configureMonologUsing(); でセットしたHandlerが呼ばれます。
例外時にもきちんと storage/logs/app-{Y-m-d}.log へ出力されます。
ただ、これだとloggerのchannel名がデフォルトの ‘lumen’ のままというささいな問題点があります。
そこも絶対変えたいという方は、
Laravel\Lumen\Applicationの子クラスを作る- 子クラスで
registerLogBindings()をオーバーライド- Loggerを作って返す
app/bootstrap.phpで $app の生成時にそのクラスを指定する
上記を行う必要があります。
$app = new Laravel\Lumen\Application /* ここを変える */(
realpath(__DIR__.'/../')
);
こんな感じです。
なお、独自のロガーを別途定義したい場合は、前回書いた通り
$app->singleton('hogeLog', function() {
// Handlerを定義して、返す
});
こんな感じでやればいいかと思います。
最後に
そもそもソースにこう書いています。
/*
|--------------------------------------------------------------------------
| Define a callback to be used to configure Monolog
|--------------------------------------------------------------------------
*/
$app->configureMonologUsing(function($monolog) {
});
(Google翻訳によると) Monologの設定に使用されるコールバックを定義します。
(最初から書いてあるやんけ!!)
でも、なぜこれで全てがまかり通るのか正直言って分かっていません。
(多分、$app->registerLogBindings() で、'Psr\Log\LoggerInterface'が singleton(); で登録されるからなんだろうな〜なんて思っていますが、深く追えていません。。)
今後も勉強を続けて理解する事が出来たら、また追記したいと思います。