你有沒有遇到過這樣的情況:代碼被各種人拷來拷去,散落在不同的服務器上,它們運行着同樣的代碼,卻各有各的脾氣。A 服務器風平浪靜,B 服務器炸成煙花,C 服務器似乎活着但又不太對勁……而你,每天都在面對來自四面八方的“XX功能炸了”“接口500了”“部署完直接寄了”的靈魂拷問。
最離譜的是,它們都會從你這同步最新的代碼,但到底是代碼問題還是服務器環境問題,你根本沒辦法第一時間知道。於是,問題就變成了:如何把這些分散的錯誤日誌規範地收集起來,好讓我在別人衝進來質問之前,提前找到問題所在?
於是,我不情不願地搞了個日誌收集方案,順便寫了個 Golang 腳本來專門接收遠程日誌。雖然我並不想管這些破事,但現實就是,我要是再不解決,估計下次見到我,老闆已經換了個人。
先把實現好的倉庫放在這裏:點擊前往GitHub
需求分析與設計目標
在開發PHP工具包時,我需要一個滿足以下特性的日誌組件:
- 多輸出渠道:同時支持文件、控制枱、遠程API等輸出方式
- 格式解耦:允許不同輸出使用不同格式(如開發環境用文本,生產環境用JSON)
- 低耦合擴展:新增處理器或格式化器時無需修改現有代碼
核心架構設計
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. 異常處理原則
- 靜默失敗:單個處理器異常不影響其他處理器執行
- 開發友好:控制枱處理器直接輸出原始錯誤信息
- 生產安全:文件處理器避免拋出致命錯誤
擴展實踐示例
場景:添加企業微信通知
- 實現新處理器:
class WeChatHandler implements LogHandlerInterface {
public function handle(...) {
$markdown = "## {$title}\n**級別**: {$level}\n".$this->formatContext($context);
$this->sendToWeChat($markdown);
}
}
- 組合使用:
$logger = new Logger([
new FileHandler(...),
new WeChatHandler(WEBHOOK_URL),
]);
$logger->log(...)
模式應用總結
-
責任鏈模式的價值:
- 符合單一職責原則:每個處理器只關注自己的輸出方式
- 動態組合:運行時自由搭配不同處理器
- 可擴展性:新增處理器無需修改核心邏輯
-
策略模式的優勢:
- 格式轉換與業務邏輯解耦
- 支持不同場景的格式策略快速切換
- 便於進行格式驗證和單元測試
這種設計模式組合特別適合需要靈活擴展的日誌系統,在保持核心穩定的同時,為各種定製需求留出了足夠的擴展空間。
反正這玩意兒是搞完了。現在項目的日誌終於變得清爽了一點,該輸出到文件的就乖乖寫文件,該打印到控制枱的就老實滾屏,至於那些緊急的、可能導致我或者老闆跑路的錯誤,就直接遠程通知到我的服務器上。這樣一來,我至少能在被質問之前,先假裝冷靜地説:“哦,這個問題我已經在看了。”
今天先這樣,日誌收集算是有個着落了。明天再搞個 Go 小腳本,把這些錯誤信息整理整理,畢竟光收集還不夠,還得方便查看,不然到時候一堆日誌堆在那,和沒收集有什麼區別?算了,明天的事就留給明天的自己頭疼吧。