【第5回】PHPで使えるカスタマイズ性の高いログ出力パッケージ Monolog

【第5回】Composerを使ってお手軽アプリケーション開発

本連載ではComposerで公開されているパッケージの中から、フレームワークを問わず汎用的に使えるライブラリをサンプルコードと共に紹介します。今回は「Monolog」という高機能なロギングができるパッケージを紹介します。

ロギングとMonolog

あなたのアプリケーションはログを残していますか?

単にログと言われると新米のPHPエンジニアなら、エラーログやアクセスログのようなログファイルを連想する人もいれば、error_reportingやerror_logのようなphp.iniの設定(ディレクティブ)を連想する人もいると思います。

開発環境ではエラーを画面に表示して確認しているが、本番環境ではエラーを画面に表示しないように設定を変えているだけで、ファイルには残していなかったり、ログファイルはあってもファイルの中身を見たことがないという人もいるかもしれません。

本番環境のログはアプリケーションを安定稼働するために必要不可欠なものであり、特にアプリケーションログではユーザーの行動を把握することができます。例えば、ユーザーが予期しない行動を行った際の不具合や今後起こりうるシステム障害のヒントがログに残っている場合があります。

ログはファイルに残すもの?

ログの保存先と言えば、Linuxでは/var/log配下のファイルというイメージを持っているかもしれませんが、ログは必ずしもファイルに残すものではありません。例えば、データベースのテーブルに記録したり、アプリケーションからメールでログを送信することもあります。最近では、Fluentdなどでログを収集してログ収集用のサーバで一括管理したり、API経由でSlackにログを残すということもあるでしょう。

Monologでできること

Monologを使うと、アプリケーションのログをファイルはもちろん、データベースや様々なWebサービスに記録することができ、ログレベルで分類して、すべてのログを記録する出力先とエラーレベルの高いログのみ記録する出力先など複数の出力先を指定することもできます。

多くのPHPフレームワークで使われており、LaravelやFuelPHPでは、標準のロギングライブラリとして使用されていたり、SymfonyやCakePHPでも設定を変更したり、追加のパッケージをインストールすることによりロギングライブラリとして使うことができます。

Monologの導入方法

インストールは他のライブラリ同様

composer require monolog/monolog

を実行するか、composer.jsonに下記を記載して、composer updateを実行します。

{
    "require": {
        "monolog/monolog": "^1.24"
     }
}
2019/02/06時点の最新バージョンは1.24.0です。

基本的な使い方と概念

サンプルコード

Monologを使うためには、「チャンネル (Channel)」と「ハンドラー (Handler)」という2つの概念を理解する必要があるのですが、イメージをつかむために、説明よりも先にはサンプルコードから見てみましょう。

<?php
require_once __DIR__ . '/vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

try {
    // accountチャンネル(Loggerインスタンス)の作成
    $channel = new Logger('account');
    // ハンドラーの作成
    $handler = new StreamHandler(__DIR__ . '/sample.log');
    $channel->pushHandler($handler);
} catch (\Exception $e) {
    die($e->getMessage());
}

// ログ出力
$channel->info('新しいアカウントが作成されました');
$channel->warning('画像認証に5回連続で間違えたユーザーがいます');
$channel->error('DBエラーで新しいアカウントの作成に失敗しました');

出力結果

上記コードの結果(sample.log)は下記のように、[時間] [チャンネル名].[ログレベル]: [ログメッセージ]という出力になっています。

[2019-02-06 10:20:19] account.INFO: 新しいアカウントが作成されました [] []
[2019-02-06 10:20:19] account.WARNING: 画像認証に5回連続で間違えたユーザーがいます [] []
[2019-02-06 10:20:19] account.ERROR: DBエラーで新しいアカウントの作成に失敗しました [] []

動作イメージができたところで、「チャンネル (Channel)」と「ハンドラー (Handler)」という概念について説明していきます。

チャンネル

チャンネルはログの種類を定義するもので、Loggerクラスのインスタンスが該当します。Loggerインスタンスの生成時の第1引数がチャンネル名になり、ログの種類ごとに異なる場所や方法でログを出力するために使われます。また、ログを出力するためには、チャンネルに最低でも1つのハンドラーを紐付ける必要があります。

上記の例ではアカウント関連のログを出力するためのaccountチャンネルを生成しています。チャンネルの数は決まってないため、DB関連のログを出力するdbチャンネルやリクエストのログを出力するrequestチャンネルなどアプリケーションに必要な数だけ作成することができます。

ハンドラー

ハンドラーはログの出力先を定義するインスタンスで、\Monolog\Handler\HandlerInterfaceのインスタンス(上記コードではStreamHandler)が該当します。

「ロギングとMonolog」の説明でログはファイル以外にも残すことができるという話をしましたが、ファイルやDBなどログの出力先ごとに記録方法はまったく異なるため、出力方法にあったハンドラーインスタンスを生成して、チャンネルと紐付ける必要があるということです。もちろん、HandlerInterfaceを実装すればハンドラーを自作することも可能です

上記の例ではログをsample.logというファイルに出力するために、StreamHandlerを使用しています。

StreamHandlerは第1引数にストリームを指定しますが、string型でパスを指定するとそのパスをfopenで開き、ストリームを生成してくれます。

複数のチャンネルやハンドラーを使用した例

次に複数のチャンネルと複数のハンドラーを使用したサンプルコードを見てみましょう。チャンネルはアカウント関連のログを出力するaccountチャンネルとDB処理のログを出力するdbチャンネルの2つがあり、ハンドラーはそれぞれのチャンネルごとにログを出力するハンドラー($accountHandler, $dbHandler)と両方のチャンネルのログを出力するハンドラー($allHandler)の3つを使います。

アカウント追加の処理をサンプルとして取り上げます。簡素化のためにデータ追加には実際にはあり得ないですが生のSQL文を使用し、$pdoにはPDOのインスタンスが格納されているとします。

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

/* ログ準備 チャンネルの生成 */
try {
    // accountチャンネルの作成
    $accountChannel = new Logger('account');
    // dbチャンネルの作成
    $dbChannel = new Logger('db');

    // accountのログのみを出力するハンドラーの作成
    $accountHandler = new StreamHandler(__DIR__ . '/account.log');
    // dbのログのみを出力するハンドラーの作成
    $dbHandler = new StreamHandler(__DIR__ . '/db.log');
    // accountとdb両方のログを出力するハンドラーの作成
    $allHandler = new StreamHandler(__DIR__ . '/all.log');

    // チャンネルとハンドラーの紐付け
    $accountChannel->pushHandler($accountHandler);
    $accountChannel->pushHandler($allHandler);
    $dbChannel->pushHandler($dbHandler);
    $dbChannel->pushHandler($allHandler);
} catch (\Exception $e) {
    die($e->getMessage());
}


/* ログ出力箇所 */
// サンプルのためSQL文を直接記載しています。アプリケーションで使用する場合はSQLインジェクション対策をしてください。
$sql = 'INSERT INTO accounts (id, name, email, password) VALUES (1, "テストユーザー", "example@technoledge.net", "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd")';
try {
    $dbChannel->debug($sql);
    $pdo->exec($sql);
} catch (\Exception $e) {
    $accountChannel->error('ユーザー登録失敗');
    $dbChannel->error($e->getMessage());
    exit;
}
$accountChannel->info('ユーザー登録成功');

出力結果

ここではcatコマンドを用いて、出力結果を確認してみます。

$ cat account.log
[2019-02-06 11:46:26] account.INFO: ユーザー登録成功 [] []
$ cat db.log
[2019-02-06 11:46:26] db.DEBUG: INSERT INTO accounts (id, name, email, password) VALUES (1, "テストユーザー", "example@technoledge.net", "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd") [] []
$ cat all.log
[2019-02-06 11:46:26] db.DEBUG: INSERT INTO accounts (id, name, email, password) VALUES (1, "テストユーザー", "example@technoledge.net", "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd") [] []
[2019-02-06 11:46:26] account.INFO: ユーザー登録成功 [] []

$accountHandlerの出力先であるaccount.logや$dbHandlerの出力先であるdb.logにはそれぞれ片方のチャンネルのログが出力され、$allHandlerの出力先である、all.logには両方のチャンネルのログが出力されました。このように1つのアプリケーションで複数のチャンネルやハンドラーを使い分けることで、より確認がしやすいログ構成にすることができます。

まとめ

今回のサンプルではログをファイルに書き込む例のみ取り上げましたが、いろいろなログの出力先に対応できるようMonologのパッケージには50種類近くものハンドラーが同梱されています。
https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md

また、上記で紹介したチャンネルやハンドラーの概念以外に、ログに関連する追加データを付与してメッセージと一緒に書き出すためのプロセッサーやログの出力フォーマットを設定するフォーマッターという概念があります。
これらを使いこなせるようになると、より快適なログ管理とシステムの安定運用を保つことができます。