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

投稿日:

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

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

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

作成者: shimabox

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

コメントする

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

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