你有没有遇到过这样的情况:代码被各种人拷来拷去,散落在不同的服务器上,它们运行着同样的代码,却各有各的脾气。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 小脚本,把这些错误信息整理整理,毕竟光收集还不够,还得方便查看,不然到时候一堆日志堆在那,和没收集有什么区别?算了,明天的事就留给明天的自己头疼吧。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。