現在、個人的にちょっとしたAPIをLumenで実装しています。
Lumenオシャンティ!! って感じで意気揚々とやっていたのですが、ログ周りに関して 前回 のやり方だと困った場面に遭遇しました。
そこで今回は、何に困ったのか、最終的にどうしたのかを綴ってみたいと思います。
※ Lumenのバージョンは前回と一緒です
何をしたかったのか
まず、前回何をしたかったのかというと
すべてのログを好きなHandler(Monolog)でコントロールしたかった
というものがありました。
※ 最初、ログの出し方すらつまずきましたが
そこで以下の方法を前回はとったわけです。
(前回は例として、NativeMailerHandler も使いましたが今回は省略します)
app/bootstrap.php
を修正
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | <?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 ; |
で、使う側はこんな感じにします。
1 2 | $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
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <?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 へは何も書き出されていません。
1 2 3 4 5 | try { $logger = app( 'log' ); } catch (Exception $ex ) { throw $e ; // throw the original exception } |
多分、こんな感じで直接コアを弄っちゃうか、子クラスなどでオーバーライドすれば上手くいく気がするのですが、それだとなんだか負けた気がします。
※ コアを弄るのはそもそもあれですし
そこで他の方法を探る旅に出まして、最終的に以下の実装を行いました。
最終的にどうしたのか
app/bootstrap.php
を以下のように修正しました。
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | <?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 の生成時にそのクラスを指定する
上記を行う必要があります。
1 2 3 | $app = new Laravel\Lumen\Application /* ここを変える */ ( realpath (__DIR__. '/../' ) ); |
こんな感じです。
なお、独自のロガーを別途定義したい場合は、前回書いた通り
1 2 3 | $app ->singleton( 'hogeLog' , function () { // Handlerを定義して、返す }); |
こんな感じでやればいいかと思います。
最後に
そもそもソースにこう書いています。
1 2 3 4 5 6 7 | /* |-------------------------------------------------------------------------- | Define a callback to be used to configure Monolog |-------------------------------------------------------------------------- */ $app ->configureMonologUsing( function ( $monolog ) { }); |
(Google翻訳によると) Monologの設定に使用されるコールバックを定義します。
(最初から書いてあるやんけ!!)
でも、なぜこれで全てがまかり通るのか正直言って分かっていません。
(多分、$app->registerLogBindings()
で、'Psr\Log\LoggerInterface'
が singleton();
で登録されるからなんだろうな〜なんて思っていますが、深く追えていません。。)
今後も勉強を続けて理解する事が出来たら、また追記したいと思います。