海岛奇兵草花平台
367.43 MB · 2025-11-13
最近我把自己常用的一套错误上报逻辑封装成了一个 Composer 包,叫 hejunjie/lazylog。
功能很简单也很实用:安全地写本地日志 + 把异常信息上报到远端(支持同步/异步) 。本文讲讲为什么我要做这个库、实现思路、在不同运行环境下如何选择(以及我推荐的优化方案)。
先讲个背景。之前我写了一个 Go 项目 —— oh-shit-logger,目标是把不同语言、不同项目里的错误集中收集到一个地方。Go 做服务天然快、部署也简单: GitHub Actions自动打包,我只要把包丢主机上一键启动就好了。
但上线后有朋友问:
这是个很合理的问题。网络 I/O 的确有成本,但异常本身在多数系统里不是那种持续不断、高频率的事件(如果异常多到经常并发,那系统可能已经在出问题了)。
与其空谈“会不会慢”,我更愿意把常用做法封装一下,直接给出一个实战好用的方案——于是 hejunjie/lazylog 诞生了。
lazylog 的核心思路很简单:
proc_open() 或 exec() fork 出一个 PHP CLI 子进程来发送 HTTP POST,不阻塞主进程。适用于 PHP-FPM、一次性 CLI 脚本等短生命周期环境。我把这些行为都封装在一个很小的包里:composer require hejunjie/lazylog,在任何项目里都能快速复用。
PHP 没有内置线程(除非用扩展),但我们可以通过子进程实现“非阻塞式”的上报:
proc_open():启动子进程并可拿到 stdin/stdout/stderr,控制能力强;但会创建管道资源,需要注意关闭管道以免资源泄露。exec():简单粗暴,把命令交给 shell 去做 fork,父进程可立即返回(命令后面加 &)。语义上更轻量,但控制能力弱。两者的本质都是 fork 一个新进程去跑 PHP CLI,然后子进程读取临时文件(或者接收传参)、发 POST、删临时文件、退出。主进程不会等子进程走完就返回给用户,所以对用户体验几乎零影响。
优点:实现简单、跨平台、即插即用;适合错误信息本身不高频的场景。
缺点:在“极高并发”场景下(比如每秒上千条错误)会比较吃资源,子进程启动和网络请求仍然有成本。
这是个重要的实践问题:在常驻内存框架中,我更推荐用同步上报或队列,而不是频繁 fork 子进程。
原因很直观:
我在包里同时提供了 reportSync()(同步上报)和 reportAsync()(伪异步上报),并提供 Logger::formatThrowable() 帮你把异常转成纯数据结构,方便推队列或序列化。
本地写日志
Logger::write('/var/logs', 'error/app.log', 'Task Failed', ['msg' => 'something wrong']);
短生命周期场景(异步上报)
try {
// ...
} catch (Throwable $e) {
Logger::reportAsync($e, 'https://your-collector/collect', 'my-project');
}
常驻框架(推荐同步或队列)
try {
// ...
} catch (Throwable $e) {
// 同步上报(简单、直接)
Logger::reportSync($e, 'https://your-collector/collect', 'my-project');
// 或者:转成数组,投递队列,由 Worker 负责上报(推荐)
$payload = Logger::formatThrowable($e, 'my-project');
Queue::push('error_report', $payload);
}
有人担心“网络 I/O 会把 PHP 卡死”。我的观点是:
总之:衡量利弊后选择适合你业务的方式。lazylog 提供了两端(sync/async)以及格式化功能,方便你按需设计。
我把它做成 composer 包的原因很直接:我希望 快速把 PHP 项目的错误上报到我自己的 Go 服务(oh-shit-logger) ,而不是每个项目都重复造轮子。把常用逻辑抽出来,项目里 composer require hejunjie/lazylog 就能统一上报方式——既省事又稳妥。
如果你想快速了解这个项目:Zread 解析文档
reportAsync() 很方便,能保证主流程不被阻塞。reportSync() 或推队列再上报。