傳統按日期分檔的日誌易產生龐大單一檔案,難以閱讀與維護。本文提供單例模式的滾動日誌類別,支援依檔案大小與檔案數量自動輪替,並提供多級別記錄功能。
xxx.log)達到 maxFileSize 時自動改名為 xxx_1.log,舊檔依序遞增(_1 → _2 → ...)。maxFileNum 個檔案(含當前檔),超過時刪除最舊檔案。DEBUG、MSG、ERR,可依設定層級過濾。fopen('a+b') 追加寫入,自動釋放鎖定。<?php
final class LOGS {
const LOGS_DEBUG = 0;
const LOGS_MSG = 1;
const LOGS_ERR = 2;
private $level;
private $maxFileNum;
private $maxFileSize;
private $logPath;
private $filePrefix;
private static $instance = null;
private function __construct() {}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function init($level, $maxNum, $maxSize, $logPath, $filePrefix) {
$level = intval($level);
$maxNum = intval($maxNum);
$maxSize = intval($maxSize);
if (!is_dir($logPath)) {
@mkdir($logPath, 0777, true);
}
if (!in_array($level, [self::LOGS_DEBUG, self::LOGS_MSG, self::LOGS_ERR]) ||
$maxNum <= 0 || $maxSize <= 0 || !is_dir($logPath)) {
return false;
}
$this->level = $level;
$this->maxFileNum = $maxNum;
$this->maxFileSize = $maxSize;
$this->logPath = rtrim($logPath, DIRECTORY_SEPARATOR);
$this->filePrefix = $filePrefix;
return true;
}
private function formatTime() {
$ustime = explode(" ", microtime());
return "[" . date('Y-m-d H:i:s', time()) . "." . ($ustime[0] * 1000) . "]";
}
private function rotateIfNeeded() {
$currentFile = $this->logPath . DIRECTORY_SEPARATOR . $this->filePrefix . ".log";
clearstatcache();
if (!file_exists($currentFile) || filesize($currentFile) < $this->maxFileSize) {
return;
}
// 尋找現有最大索引
$index = 1;
for (; $index < $this->maxFileNum; $index++) {
if (!file_exists($this->logPath . DIRECTORY_SEPARATOR . $this->filePrefix . "_" . $index . ".log")) {
break;
}
}
// 已達上限,從倒數第二個開始向後搬移(覆蓋最舊)
if ($index == $this->maxFileNum) {
$index--;
}
for (; $index > 1; $index--) {
$old = $this->logPath . DIRECTORY_SEPARATOR . $this->filePrefix . "_" . ($index - 1) . ".log";
$new = $this->logPath . DIRECTORY_SEPARATOR . $this->filePrefix . "_" . $index . ".log";
@rename($old, $new);
}
// 將當前檔改名為 _1.log
@rename($currentFile, $this->logPath . DIRECTORY_SEPARATOR . $this->filePrefix . "_1.log");
}
public function log($str) {
$this->rotateIfNeeded();
$path = $this->logPath . DIRECTORY_SEPARATOR . $this->filePrefix . ".log";
$fp = fopen($path, "a+b");
if ($fp) {
fwrite($fp, $str, strlen($str));
fclose($fp);
}
return true;
}
public function debug($msg, $file, $line) {
if ($this->level <= self::LOGS_DEBUG) {
$this->log($this->formatTime() . "[{$file}:{$line}] DEBUG: {$msg}\n");
}
}
public function msg($msg, $file, $line) {
if ($this->level <= self::LOGS_MSG) {
$this->log($this->formatTime() . "[{$file}:{$line}] MSG: {$msg}\n");
}
}
public function err($msg, $file, $line) {
if ($this->level <= self::LOGS_ERR) {
$this->log($this->formatTime() . "[{$file}:{$line}] ERR: {$msg}\n");
}
}
}
// 使用範例
$logs = LOGS::getInstance();
$logs->init(LOGS::LOGS_MSG, 5, 200, __DIR__, 'app'); // MSG 層級、最多 5 檔、每檔 200 bytes
$logs->msg("使用者登入", __FILE__, __LINE__);
$logs->debug("除錯訊息", __FILE__, __LINE__); // 不會寫入,因層級為 MSG
?>
持續執行上述範例,目錄下將產生:
app_4.log app_3.log app_2.log app_1.log app.log <-- 最新日誌
每個檔案不超過 200 bytes,最多 5 個檔案,自動淘汰最舊記錄。