Lumenでログを出力する(その2)

投稿日:

現在、個人的にちょっとした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(); で登録されるからなんだろうな〜なんて思っていますが、深く追えていません。。)

今後も勉強を続けて理解する事が出来たら、また追記したいと思います。

投稿日:
カテゴリー: PHP タグ:

作成者: shimabox

Web系のプログラマをやっています。 なるべく楽しく生きていきたい。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください