こんにちは、タカフです。
Webアプリ作る場合に後からボディブローのように効いてくるのがWebアプリ独自のログ、そうアプリケーションログです。
要所要所で埋め込んでおくことで、何か障害が発生した時にaccess_logでもなくerror_logでもなくアプリケーションログを確認することですぐにその障害の原因を突き止められることが往々にしてあります。
数年前まではApacheから提供されているlog4phpが便利でしたが、
今の時代はAWSとも相性の良いmonologの方が便利と言えます。
僕も以前こんなツイートをしました。
PHPのロガーにlog4phpをよく使うんだけど、CloudWatchLogsにもログ送ると1行になってしまい2行目以降のログ内容が消える。
その点MonologはCloudWatchLogsともうまく統合出来るようで、、、https://t.co/WYJ1D75f8Y
時代はMonologだったよ。— スズキタカフミ@マナブロガー (@kahoo365) July 19, 2019
ということで今はPHPのロガーで迷ったらとりあえずmonologを使っておけばいい状況なので、
本記事ではmonologの使い方や便利なサンプルコードを紹介していきます。
monologのインストール
composer経由でインストールが便利で確実です。
composerは事前に導入済みであるとして、以下のコマンドでmonologパッケージをvendorディレクトリ内にインストールすることが出来ます。
$ composer require monolog/monolog
composer.jsonのファイルに以下のように記述されます。
"require": {
"monolog/monolog": "^2.0"
},
これでmonologを使えます。
因みにmonologの2系はPHP7.2以上となっています。
PHP7.1以下の場合はmonologの1系を使いましょう。
monologの基本的な使い方
monologの基本的な使い方は Loggerクラスをインスタンス化して、そのインスタンスに対して出力先をセット後、ログレベルに応じてログ関数を呼んでいきます。
<?php
require_once "vendor/autoload.php";
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\NativeMailerHandler;
// 日本時間にしておかないと時刻がUTCになってしまう
date_default_timezone_set("Asia/Tokyo");
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// monolog本体をLoggerクラスから実体化する。引数でログ出力する名称を渡す。
$log = new Logger('app');
// monologの実体にハンドラーとして出力先を追加していく
// 以下は指定のパスのログファイルにWARNING以上のログなら保存する設定です。
$log->pushHandler(new StreamHandler(__DIR__ . '/path/to/your.log', Logger::WARNING));
// アプリ上で以下のような記述をすると指定のファイルにログ出力できる
$log->debug('デバッグログ');
$log->warning('ワーニングログ');
$log->error('エラーログ');
この実行結果は以下のようになります。
[2020-01-07T11:30:24.378266+09:00] app.WARNING: ワーニングログ [] []
[2020-01-07T11:30:24.385743+09:00] app.ERROR: エラーログ [] []
StreamHandler
インスタンス化のところで、WARNING以上なら出力する設定にしているのでdebugのログは出力されていないですね。
monologのログレベル
monolog のログレベルの強さは以下の通りとなっています。
下に行く程ログレベルが強いことを意味します。
ログレベル | 関数 | 主な使い道 |
---|---|---|
DEBUG | debug() | 詳細なデバッグ情報 |
INFO | info() | 興味深いイベント。例:ユーザーログイン、SQLログ。 |
NOTICE | notice() | 通常だが重要なイベント。 |
WARNING | warning() | エラーではない例外的な発生。 例:非推奨のAPIの使用、APIの不適切な使用、 必ずしも間違っているとは限らない望ましくないもの。 |
ERROR | error() | すぐに対処する必要はないが、 通常はログに記録して監視する必要があるランタイムエラー。 |
CRITICAL | critical() | クリティカルな状態。 例:アプリケーションコンポーネントが利用できない、予期しない例外。 |
ALERT | alert() | すぐに対処する必要があります。 例:Webサイト全体がダウンしている、データベースが利用できないなど。 これにより、SMSアラートがトリガーされ、起動されます。 |
EMERGENCY | emergency() | 緊急:システムは使用できません。 |
PHPのWebアプリ上で処理内容に応じてこのログレベルの関数を呼び出せば後からとっても見やすいログの出来上がりという事が出来ます。
monologの引数
monologのログ関数の第二引数では任意の配列の変数を渡してログでその中身を確認することが出来ます。
例えば、以下のようにユーザー情報を渡すことが出来ます。
$user = [
"name" => "タカフ",
"gender" => "man"
];
$log->info('ユーザー情報', $user);
そうすると、ログ上でその中身をjson形式で出力してくれます。現代的ですよね。
[2020-01-07T11:30:57.254506+09:00] app.INFO: ユーザー情報 {"name":"タカフ","gender":"man"} []
Tips. ログ出力の書式を変更する
上記までのログ出力例を見てもわかるように、デフォルトの書式ではミリ秒まで出ています。
これを好みの書式に変更することが出来ます。
LineFormatter
というクラスからログ書式インスタンスを生成し、ハンドラーにセットします。
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\NativeMailerHandler;
date_default_timezone_set("Asia/Tokyo");
// monolog本体をLoggerクラスから実体化する。引数でログ出力する名称を渡す。
$log = new Logger('app');
// ログ書式を定義する
$dateFormat = "Y-m-d H:i:s";
$output = "%datetime%|%level_name%|%message% %context%\n"; // 複数チャンネルあるなら[%channel%]も追加
$formatter = new LineFormatter($output, $dateFormat);
// ハンドラーに書式インスタンスをセットする
$streamHandler = new StreamHandler(__DIR__ . '/path/to/your.log', Logger::INFO);
$streamHandler->setFormatter($formatter);
$log->pushHandler($streamHandler);
// アプリ上で以下のような記述をしてログ保存できる
$log->info('テストログ');
すると以下のようなログになります。
2020-01-07 11:35:01|INFO|テストログ []
Tips. Exception補足時のスタックトレースをキレイに出す
例外発生時、exception変数をmonologに渡せるのですが、スタックトレースが1行で出てしまいます。
例えば以下のようなログになります。
[2020-01-07 11:51:53][ERROR]> Exception: 重大なエラー in /Users/taksuzuki/Dropbox/htdocs/kahoo.blog/test.php:79 Stack trace: #0 /Users/taksuzuki/Dropbox/htdocs/kahoo.blog/test.php(13): test_exception() #1 {main} : [] : []
Exceptionのスタックトレースをキレイに残すにはLineFormatter
のインスタンスに対してincludeStacktraces
を呼んでおきます。
以下のようなコードになります。
// monolog本体をLoggerクラスから実体化する。引数でログ出力する名称を渡す。
$log = new Logger('app');
// ログ書式を定義する
$dateFormat = "Y-m-d H:i:s";
$output = "[%datetime%][%level_name%]> %message% : %context% : %extra%\n"; // 複数チャンネルあるなら[%channel%]も追加
$formatter = new LineFormatter($output, $dateFormat);
$formatter->includeStacktraces(true); // これでスタックトレースをキレイに出せる
// ハンドラーに書式インスタンスをセットする
$streamHandler = new StreamHandler(__DIR__ . '/path/to/your.log', Logger::INFO);
$streamHandler->setFormatter($formatter);
$log->pushHandler($streamHandler);
try{
throw new Exception("重大なエラー");
}catch(Exception $exception){
// 例外発生時は$exceptionデータをそのまま渡す
$log->error($exception);
}
以下のようなログになりました。
[2020-01-07 11:53:37][ERROR]> Exception: 重大なエラー in /Users/taksuzuki/Dropbox/htdocs/kahoo.blog/test.php:79
Stack trace:
#0 /Users/taksuzuki/Dropbox/htdocs/kahoo.blog/test.php(13): test_exception()
#1 {main} : [] : []
参考ページ:https://arata.hatenadiary.com/entry/2018/03/16/212448
Tips. 渡したデータが空の場合に[]を出力しない
LineFormatterで%context%があると、ログ関数でデータを渡せてそれをログ出力出来ますが、上述の通りそのログ書き出しの第二引数でデータが空の場合は [] が出力されています。
これをなくすにはLineFormatterの生成時にパラメータで指定やればいいだけです。
LineFormatterの第4引数が $ignoreEmptyContextAndExtra
となっているのでこれをtrueで渡せば、contextやextraが空の時に何も出力しないように出来ます。
コードにすると以下の通りです。
$formatter = new LineFormatter($output, $dateFormat, false, true); // 4番目の引数をtrueにするとcontextが空の時に[]を出力しない
これで[]は出力されなくなります。
Tips. monologの便利な使い方サンプルコード
とっても便利なmonologですが、僕はロガーを呼び出す時はどこからでもすぐに且つ簡単に呼び出せるべきだと考えています。
なので以下のようなクラスを用意します。
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\NativeMailerHandler;
/**
* Description of MyLog
* @author taksuzuki
*/
class MyLog {
static $log = null; // static app logger instance
static function setup($logname = "app") {
// monolog setting
self::$log = new Logger($logname);
// ログ書式
$dateFormat = "Y-m-d H:i:s";
$output = "[%datetime%][%level_name%]> %message% : %context% : %extra%\n"; // 複数チャンネルあるなら[%channel%]も追加
$formatter = new LineFormatter($output, $dateFormat, false, true);
$formatter->includeStacktraces(true); // これでスタックトレースをキレイに出せる
// 標準出力ハンドラー追加。コマンドライン実行とかに便利。
$streamHandler = new StreamHandler('php://stdout', Logger::DEBUG);
$streamHandler->setFormatter($formatter);
self::$log->pushHandler($streamHandler);
// RotatingFileHandler追加。日単位でログファイルを残す。
$rotatingFileHandler = new RotatingFileHandler(__DIR__. "/path/to/your/log.txt", 60, Logger::DEBUG); // 60日分は残す
$rotatingFileHandler->setFilenameFormat('{filename}_{date}', 'Y-m-d'); // 月ごと
$rotatingFileHandler->setFormatter($formatter); // ログ書式
self::$log->pushHandler($rotatingFileHandler);
// NativeMailerHandler追加。アラート時にメール送信する。
$to = "通知先メールアドレス";
$from = "alert@kahoo.blog";
$nativeMailerHandler = new NativeMailerHandler($to, "【カフーブログ:エラー】", $from, Logger::ALERT);
$nativeMailerHandler->setFormatter($formatter);
self::$log->pushHandler($nativeMailerHandler);
}
}
MyLog::setup();
このクラスをどこかのライブラリディレクトリに設置して、composer.jsonのautoloadのclassmapでライブラリとして自動ロード出来るようにしておきます。
"autoload": {
"classmap": [
"path/to/libraries"
]
}
そして以下のように呼び出します。これだけです。
MyLog::$log->info("ログだよ");
このようにしておけば、このようなログ出力の最初のコードでmonologも初期化されてログ出力処理も動作してくれます。
そしてどこからでも呼び出せます。
まとめ
monologは使ってみると、今の時代のPHPアプリケーションにとてもマッチしていてめちゃくちゃ使いやすいんですよね。
ログ埋め込みはリリース後の運用に入ってから非常に重要になってきますので、設計段階で盛り込んでおきたいところです。
現場からは以上です。