現在、個人的にちょっとした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();
で登録されるからなんだろうな〜なんて思っていますが、深く追えていません。。)
今後も勉強を続けて理解する事が出来たら、また追記したいと思います。