头图

你有没有遇到过这样的情况:代码被各种人拷来拷去,散落在不同的服务器上,它们运行着同样的代码,却各有各的脾气。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 小脚本,把这些错误信息整理整理,毕竟光收集还不够,还得方便查看,不然到时候一堆日志堆在那,和没收集有什么区别?算了,明天的事就留给明天的自己头疼吧。


苏琢玉
17 声望0 粉丝