[コピペでOK] PHPでログを出力する

# はじめに

PHPでログを出力するLoggerを作成しました。
PHPでは、標準のエラーログや便利なライブラリがありますが、
ここでは、ライブラリを使用しません。
コピペですぐに使えるものを目指しています。

機能としては下記の通りです。

- ログメッセージを出力する
- ログレベルの指定
- ログローテート
- 出力先の指定

## ErrorLog

PHPには、「ErrorLog」という実行時エラーをメッセージに出力する機能があります。
これは、try-catchで補足できないエラーもあります。
今回の作成したLoggerクラスでは対象外です。

詳細は下記の通りです。

[PHPでのログ出力　まとめ](https://qiita.com/iwason/items/8dc9f62b4118186cf2df)

## Monolog

PHPには、「Monolog」というログ出力のライブラリがあります。
Laravelなどの有名なPHPフレームワークで採用されています。
今回の作成したLoggerクラスでは対象外です。

詳細は下記の通りです。

[コード改善に役立ちそうなPHPライブラリ・ツール](https://qiita.com/ryu22e/items/bc87f2106e5db463462a)

# 実行環境

今回実行した環境は下記の通りです。

```Markdown:実行環境
PHP: 7.3.11-0ubuntu0.19.10.3
```

# 使い方

使い方は下記の通りです。

```php
$log = Logger::getInstance();
$log->error('error log.');
$log->warn('warn log.');
$log->info('info log.');
$log->debug('debug log.');
```

getInstanceメソッドで、Loggerインスタンスを生成して
各ログレベルのメソッドを呼び出します。

出力されるログは下記の通りです。

```log
[2020-04-06 23:35:33.506][835][ERROR] error log.
[2020-04-06 23:35:33.521][835][WARN] warn log.
[2020-04-06 23:35:33.525][835][INFO] info log.
[2020-04-06 23:35:33.545][835][DEBUG] debug log.
```

「835」この数字は、プロセスIDを表示しています。

# Loggerの実装

ConfigクラスでLoggerに関する設定を行います。

```php:config.php
<?php
/**
 * 設定クラス
 */
class Config {
    const IS_LOGFILE = true; // ログファイル出力フラグ true=出力あり/false=なし
    const LOG_LEVEL = 3; // ログレベル 0=ERROR/1=WARN/2=INFO/3=DEBUG
    const LOGDIR_PATH = './logs/'; // ログファイル出力ディレクトリ
    const LOGFILE_NAME = 'console'; // ログファイル名
    const LOGFILE_MAXSIZE = 10485760; // ログファイル最大サイズ（Byte）
    const LOGFILE_PERIOD = 30; // ログ保存期間（日）
}

?>
``` 

Loggerクラスの実装は下記の通りです。

```php:logger.php
<?php
/**
 * ログ
 */
class Logger {

    // ログレベル
    const LOG_LEVEL_ERROR = 0;
    const LOG_LEVEL_WARN = 1;
    const LOG_LEVEL_INFO = 2;
    const LOG_LEVEL_DEBUG = 3;

    private static $singleton;

    /**
     * インスタンスを生成する
     */
    public static function getInstance()
    {
        if (!isset(self::$singleton)) {
            self::$singleton = new Logger();
        }
        return self::$singleton;
    }

    /**
     * コンストラクタ
     */
    private function __construct() {
    }

    /**
     * ERRORレベルのログ出力する
     * @param string $msg メッセージ
     */
    public function error($msg) {
        if(self::LOG_LEVEL_ERROR <= Config::LOG_LEVEL) {
            $this->out('ERROR', $msg);
        }
    }

    /**
     * WARNレベルのログ出力する
     * @param string $msg メッセージ
     */
    public function warn($msg) {
        if(self::LOG_LEVEL_WARN <= Config::LOG_LEVEL) {
            $this->out('WARN', $msg);
        }
    }

    /**
     * INFOレベルのログ出力する
     * @param string $msg メッセージ
     */
    public function info($msg) {
        if(self::LOG_LEVEL_INFO <= Config::LOG_LEVEL) {
            $this->out('INFO', $msg);
        }
    }

    /**
     * DEBUGレベルのログ出力する
     * @param string $msg メッセージ
     */
    public function debug($msg) {
        if(self::LOG_LEVEL_DEBUG <= Config::LOG_LEVEL) {
            $this->out('DEBUG', $msg);
        }
    }

    /**
     * ログ出力する
     * @param string $level ログレベル
     * @param string $msg メッセージ
     */
    private function out($level, $msg) {
        if(Config::IS_LOGFILE) {

            $pid = getmypid();
            $time = $this->getTime();
            $logMessage = "[{$time}][{$pid}][{$level}] " . rtrim($msg) . "\n";
            $logFilePath = Config::LOGDIR_PATH . Config::LOGFILE_NAME . '.log';

            $result = file_put_contents($logFilePath, $logMessage, FILE_APPEND | LOCK_EX);
            if(!$result) {
                error_log('LogUtil::out error_log ERROR', 0);
            }

            if(Config::LOGFILE_MAXSIZE < filesize($logFilePath)) {
                // ファイルサイズを超えた場合、リネームしてgz圧縮する
                $oldPath = Config::LOGDIR_PATH . Config::LOGFILE_NAME . '_' . date('YmdHis');
                $oldLogFilePath = $oldPath . '.log';
                rename($logFilePath, $oldLogFilePath);
                $gz = gzopen($oldPath . '.gz', 'w9');
                if($gz) {
                    gzwrite($gz, file_get_contents($oldLogFilePath));
                    $isClose = gzclose($gz);
                    if($isClose) {
                        unlink($oldLogFilePath);
                    } else {
                        error_log("gzclose ERROR.", 0);
                    }
                } else {
                    error_log("gzopen ERROR.", 0);
                }

                // 古いログファイルを削除する
                $retentionDate = new DateTime();
                $retentionDate->modify('-' . Config::LOGFILE_PERIOD . ' day');
                if ($dh = opendir(Config::LOGDIR_PATH)) {
                    while (($fileName = readdir($dh)) !== false) {
                        $pm = preg_match("/" . preg_quote(Config::LOGFILE_NAME) . "_(\d{14}).*\.gz/", $fileName, $matches);
                        if($pm === 1) {
                            $logCreatedDate = DateTime::createFromFormat('YmdHis', $matches[1]);
                            if($logCreatedDate < $retentionDate) {
                                unlink(Config::LOGDIR_PATH . '/' . $fileName);
                            }
                        }
                    }
                    closedir($dh);
                }
            }
        }
    }

    /**
     * 現在時刻を取得する
     * @return string 現在時刻
     */
    private function getTime() {
        $miTime = explode('.',microtime(true));
        $msec = str_pad(substr($miTime[1], 0, 3) , 3, "0");
        $time = date('Y-m-d H:i:s', $miTime[0]) . '.' .$msec;
        return $time;
    }
}
```

# さいごに

ソースコードをGitHubに公開しています。

[ソースファイルはこちら](https://github.com/yunbow/php-logger)

以上です。