博客 / 詳情

返回

基於責任鏈與策略模式的輕量級PHP日誌庫設計

你有沒有遇到過這樣的情況:代碼被各種人拷來拷去,散落在不同的服務器上,它們運行着同樣的代碼,卻各有各的脾氣。A 服務器風平浪靜,B 服務器炸成煙花,C 服務器似乎活着但又不太對勁……而你,每天都在面對來自四面八方的“XX功能炸了”“接口500了”“部署完直接寄了”的靈魂拷問。

最離譜的是,它們都會從你這同步最新的代碼,但到底是代碼問題還是服務器環境問題,你根本沒辦法第一時間知道。於是,問題就變成了:如何把這些分散的錯誤日誌規範地收集起來,好讓我在別人衝進來質問之前,提前找到問題所在?

於是,我不情不願地搞了個日誌收集方案,順便寫了個 Golang 腳本來專門接收遠程日誌。雖然我並不想管這些破事,但現實就是,我要是再不解決,估計下次見到我,老闆已經換了個人。

先把實現好的倉庫放在這裏:點擊前往GitHub

需求分析與設計目標

在開發PHP工具包時,我需要一個滿足以下特性的日誌組件:

  1. 多輸出渠道:同時支持文件、控制枱、遠程API等輸出方式
  2. 格式解耦:允許不同輸出使用不同格式(如開發環境用文本,生產環境用JSON)
  3. 低耦合擴展:新增處理器或格式化器時無需修改現有代碼

核心架構設計

1. 接口先行:定義規範

// 日誌處理器
interface LogHandlerInterface {
    public function handle(
        string $level, 
        string $title,
        string $message,
        array $context = []
    ): void;
}

// 日誌格式化
interface LogFormatterInterface {
    public function format(
        string $level,
        string $message,
        array $context = []
    ): string;
}

通過接口隔離了「日誌處理」與「格式轉換」兩個關注點,為後續擴展打下基礎。

2. 責任鏈模式實現多路輸出

class Logger {
    private array $handlers;

    public function __construct(array $handlers) {
        $this->handlers = $handlers;
    }

    public function log(...$params) {
        foreach ($this->handlers as $handler) {
            $handler->handle(...$params);
        }
    }
}

每個處理器獨立處理日誌,形成處理流水線。典型處理器實現:

文件處理器核心邏輯:

class FileHandler implements LogHandlerInterface {
    // 自動滾動日誌文件
    private function rotateLogFiles(string $logFile) {
        $index = 1;
        while (file_exists("{$logFile}.{$index}")) {
            $index++;
        }
        rename($logFile, "{$logFile}.{$index}");
    }
}

遠程API處理器:

class RemoteApiHandler implements LogHandlerInterface {
    public function handle(...$params) {
        // 實際應使用異步HTTP客户端
        HttpClient::post($this->endpoint, $formattedData);
    }
}

3. 策略模式實現格式切換

通過注入不同的格式化器實現格式策略:

// 文本格式化
class DefaultFormatter implements LogFormatterInterface {
    public function format(...) {
        return "[{$time}] {$level}: {$message} " . json_encode($context);
    }
}

// JSON格式化
class JsonFormatter implements LogFormatterInterface {
    public function format(...) {
        return json_encode([
            'timestamp' => microtime(true),
            'level' => $level,
            // ...其他字段
        ]);
    }
}

在處理器中組合使用:

$logger = new Logger([
    new FileHandler('app.log'),
    new ConsoleHandler(),
    new RemoteApiHandler('https://log-server.com/api')
]);

關鍵實現細節

1. 文件處理優化

  • 自動分割:通過maxFileSize​控制單個文件大小
  • 滾動策略:採用file.log.1​遞增命名方式,避免覆蓋歷史日誌
  • 目錄創建:在首次寫入時自動創建日誌目錄

2. 遠程傳輸設計

  • 格式要求:強制使用JSON格式確保數據可解析
  • 頭信息配置:預設Content-Type: application/json
  • 解耦網絡層:將具體HTTP實現隔離在處理器之外

3. 異常處理原則

  • 靜默失敗:單個處理器異常不影響其他處理器執行
  • 開發友好:控制枱處理器直接輸出原始錯誤信息
  • 生產安全:文件處理器避免拋出致命錯誤

擴展實踐示例

場景:添加企業微信通知

  1. 實現新處理器:
class WeChatHandler implements LogHandlerInterface {
    public function handle(...) {
        $markdown = "## {$title}\n**級別**: {$level}\n".$this->formatContext($context);
        $this->sendToWeChat($markdown);
    }
}
  1. 組合使用:
$logger = new Logger([
    new FileHandler(...),
    new WeChatHandler(WEBHOOK_URL),
]);

$logger->log(...)

模式應用總結

  1. 責任鏈模式的價值

    • 符合單一職責原則:每個處理器只關注自己的輸出方式
    • 動態組合:運行時自由搭配不同處理器
    • 可擴展性:新增處理器無需修改核心邏輯
  2. 策略模式的優勢

    • 格式轉換與業務邏輯解耦
    • 支持不同場景的格式策略快速切換
    • 便於進行格式驗證和單元測試

這種設計模式組合特別適合需要靈活擴展的日誌系統,在保持核心穩定的同時,為各種定製需求留出了足夠的擴展空間。

反正這玩意兒是搞完了。現在項目的日誌終於變得清爽了一點,該輸出到文件的就乖乖寫文件,該打印到控制枱的就老實滾屏,至於那些緊急的、可能導致我或者老闆跑路的錯誤,就直接遠程通知到我的服務器上。這樣一來,我至少能在被質問之前,先假裝冷靜地説:“哦,這個問題我已經在看了。”

今天先這樣,日誌收集算是有個着落了。明天再搞個 Go 小腳本,把這些錯誤信息整理整理,畢竟光收集還不夠,還得方便查看,不然到時候一堆日誌堆在那,和沒收集有什麼區別?算了,明天的事就留給明天的自己頭疼吧。

user avatar jianqiangdepaobuxie 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.