你有没有遇到过这样的情况:代码被各种人拷来拷去,散落在不同的服务器上,它们运行着同样的代码,却各有各的脾气。A 服务器风平浪静,B 服务器炸成烟花,C 服务器似乎活着但又不太对劲……而你,每天都在面对来自四面八方的“XX功能炸了”“接口500了”“部署完直接寄了”的灵魂拷问。
最离谱的是,它们都会从你这同步最新的代码,但到底是代码问题还是服务器环境问题,你根本没办法第一时间知道。于是,问题就变成了:如何把这些分散的错误日志规范地收集起来,好让我在别人冲进来质问之前,提前找到问题所在?
于是,我不情不愿地搞了个日志收集方案,顺便写了个 Golang 脚本来专门接收远程日志。虽然我并不想管这些破事,但现实就是,我要是再不解决,估计下次见到我,老板已经换了个人。
先把实现好的仓库放在这里:点击前往GitHub
在开发PHP工具包时,我需要一个满足以下特性的日志组件:
// 日志处理器
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;
}
通过接口隔离了「日志处理」与「格式转换」两个关注点,为后续扩展打下基础。
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);
}
}
通过注入不同的格式化器实现格式策略:
// 文本格式化
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')
]);
maxFileSize
控制单个文件大小file.log.1
递增命名方式,避免覆盖历史日志Content-Type: application/json
场景:添加企业微信通知
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 小脚本,把这些错误信息整理整理,毕竟光收集还不够,还得方便查看,不然到时候一堆日志堆在那,和没收集有什么区别?算了,明天的事就留给明天的自己头疼吧。