首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 單例模式日誌類:自動分檔、錯誤分級、資源節省

     編輯:PHP 相關知識

前言

生產環境應用程式離不開日誌記錄。優秀的日誌類應具備:單例模式避免重複開啟檔案控制代碼、自動分檔防止單一檔案過大、錯誤分級區分一般事件與嚴重錯誤、效能優化減少 I/O 開銷。本文實作一個生產就緒的 Log 類別,並說明設計考量。

完整類別代碼

<?php
/**
 * 單例模式日誌處理類
 * 功能:自動分檔、支援多級別、延遲開啟檔案、執行結束自動關閉
 */
class Log {
    // 單例實例
    private static ?self $instance = null;

    // 檔案控制代碼(靜態,跨實例共享)
    private static ?resource $handle = null;

    // 設定參數(從常數或建構子注入)
    private bool $enabled;
    private string $dir;
    private int $max_size;      // 位元組
    private string $prefix;     // 檔案前綴,如 'log_'

    // 私有建構子,防止外部實例化
    private function __construct() {
        // 這裡假設專案定義了常數,實際專案請改為配置檔注入
        $this->enabled   = defined('LOG_SWITCH')   ? LOG_SWITCH   : true;
        $this->dir       = defined('LOG_FILE_PATH') ? LOG_FILE_PATH : __DIR__ . '/logs/';
        $this->max_size  = defined('LOG_MAX_LEN')   ? LOG_MAX_LEN   : 10 * 1024 * 1024; // 10MB
        $this->prefix    = 'log_';

        // 確保目錄存在
        if (!is_dir($this->dir)) {
            @mkdir($this->dir, 0755, true);
        }
    }

    // 取得單例
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    // 禁止克隆與反序列化
    private function __clone() {}
    public function __wakeup() {
        throw new Exception("不能反序列化單例");
    }

    /**
     * 寫入日誌
     * @param int    $type 0=一般事件(THING), 1=錯誤(ERROR), 2=警告(WARN), 3=除錯(DEBUG)
     * @param string $msg  日誌訊息
     * @param string $time 時間字串,預設當前時間
     */
    public function log(int $type, string $msg, string $time = ''): void {
        if (!$this->enabled) return;

        $time = $time ?: date('Y-m-d H:i:s');
        $prefix = match($type) {
            1 => 'ERROR',
            2 => 'WARN',
            3 => 'DEBUG',
            default => 'INFO',
        };

        $line = "[$time] [$prefix] $msg" . PHP_EOL;

        // 延遲開啟檔案
        if (self::$handle === null) {
            $this->openLogFile();
        }

        // 寫入前檢查檔案大小,超過則輪替
        if (ftell(self::$handle) + strlen($line) > $this->max_size) {
            fclose(self::$handle);
            $this->openLogFile();
        }

        fwrite(self::$handle, $line);
    }

    // 開啟/建立最新日誌檔
    private function openLogFile(): void {
        $suffix = $this->getNextSuffix();
        $filename = $this->dir . $this->prefix . $suffix . '.log';
        self::$handle = fopen($filename, 'a'); // 追加模式
        if (self::$handle === false) {
            throw new RuntimeException("無法開啟日誌檔案: $filename");
        }
    }

    // 取得下一個可用後綴號(掃描現有檔案)
    private function getNextSuffix(): int {
        $max = 0;
        if ($dh = opendir($this->dir)) {
            while (($file = readdir($dh)) !== false) {
                if ($file === '.' || $file === '..') continue;
                if (preg_match('/^' . preg_quote($this->prefix, '/') . '(\d+)\.log$/', $file, $m)) {
                    $max = max($max, (int)$m[1]);
                }
            }
            closedir($dh);
        }

        // 檢查最新檔案是否已超過大小限制
        $latest_file = $this->dir . $this->prefix . $max . '.log';
        if (file_exists($latest_file) && filesize($latest_file) >= $this->max_size) {
            $max++;
        }
        return $max;
    }

    // 關閉檔案控制代碼(可在腳本結束手動呼叫,或註冊 shutdown_function)
    public function close(): void {
        if (self::$handle !== null) {
            fclose(self::$handle);
            self::$handle = null;
        }
    }

    // 解構子自動關閉
    public function __destruct() {
        $this->close();
    }
}

// 註冊關閉函數確保資源釋放
register_shutdown_function([Log::getInstance(), 'close']);
?>

使用範例

<?php
require_once 'Log.php';

// 取得單例
$logger = Log::getInstance();

// 記錄一般事件
$logger->log(0, '使用者登入成功', '2024-01-15 10:30:00');

// 記錄錯誤
try {
    // 業務邏輯...
} catch (Exception $e) {
    $logger->log(1, '資料庫連線失敗: ' . $e->getMessage());
}

// 記錄警告
$logger->log(2, 'API 回應逾時,重試中...');

// 記錄除錯資訊(正式環境可關閉)
$logger->log(3, 'SQL 查詢耗時: 120ms', '2024-01-15 10:30:05');

// 程式結束前會自動關閉,也可手動呼叫
// $logger->close();
?>

設計亮點解析

1. 單例模式節省資源

全程只建立一個 Log 實例與一個檔案控制代碼(self::$handle),避免多次 fopen/fclose 開銷,亦避免並發寫入導致檔案損毀。

2. 自動分檔(Rolling)

  • 掃描目錄找出最大後綴號 log_N.log。
  • 寫入前檢查 ftell() 當前指標位置 + 即將寫入長度是否超過 $max_size。
  • 超過則關閉舊檔、建立 log_{N+1}.log 繼續寫入。

3. 延遲開啟檔案

只有首次呼叫 log() 時才 fopen,若程式執行期間完全未寫日誌,則不佔用檔案描述符。

4. 多級別支援

使用 match 表達式(PHP 8.0+)對應級別前綴,易於擴充(如加入 NOTICE、CRITICAL)。

5. 安全關閉機制

  • 提供 close() 公開方法。
  • 解構子 __destruct() 自動關閉。
  • 註冊 register_shutdown_function 確保即使發生致命錯誤也能釋放資源。

待優化項目(留待擴充)

  1. 最大檔案數量限制:目前無上限,長期運行會產生大量檔案,建議加入 $max_files 參數,超過時刪除最舊檔案。
  2. 非同步寫入:高併發場景下,可將日誌推入記憶體佇列,由背景 Worker 批次寫入磁碟(或使用 Monolog + Redis/AMQP)。
  3. 結構化日誌:輸出 JSON 格式,便於 ELK、Loki 等日誌平台收集分析。
  4. 上下文資訊:自動附加 Request ID、User ID、IP、Trace ID 等。

總結

本文實作的 Log 類別展示了單例模式在資源管理上的優勢,結合自動分檔、多級別、延遲開啟與安全關閉,滿足中小型專案的核心日誌需求。若專案規模增大,建議遷移至成熟函式庫(如 Monolog),獲得更豐富的 Handler、Formatter、Processor 生態系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved