不能在exceptionhandler::report()里直接用curl推送钉钉——会阻塞请求、引发雪崩;正确做法是先落盘jsonl日志,再由定时脚本异步消费并带超时、加签、去重、限流控制地推送。

不能在 ExceptionHandler::report() 里直接用 curl 推送到钉钉 —— 这会把网络 I/O 塞进请求生命周期,高并发时反而拖垮整个服务,甚至引发雪崩。
为什么 report() 里直发钉钉是危险操作
ThinkPHP 的 report() 方法运行在当前请求上下文中,一旦钉钉 Webhook 响应慢、超时或失败,就会阻塞整个响应流程。更糟的是:如果钉钉限流(每分钟最多 20 条)、网络抖动或机器人被禁用,错误日志不仅发不出去,还会让 PHP 进程卡住、连接堆积、CPU 拉满。
- 钉钉 Webhook 不保证低延迟,实测 P95 响应常超 800ms
-
curl_exec()默认无超时控制,容易 hang 住 worker - 多个请求同时触发错误 → 多个并发
curl→ 快速触达钉钉频率限制 → 后续全部 403 或静默丢弃 - 错误发生时若数据库/Redis 也异常,再加一层网络依赖 = 双重故障点
正确做法:先落盘,再异步投递
核心思路是解耦「捕获错误」和「发送通知」两个阶段。ThinkPHP 本身没提供异步钩子,必须手动补一层缓冲层。
- 重写
AppException的report()方法,只做一件事:把错误序列化为 JSON Line(JSONL)格式,追加写入临时文件,例如/tmp/think_error_queue.jsonl - 确保写入是原子的:
file_put_contents($file, $line . "\n", FILE_APPEND | LOCK_EX) - 每行一个完整错误对象,含
error_id(建议用uniqid('', true))、message、file、line、trace、context等关键字段 - 不校验钉钉可用性,不重试,不阻塞 —— 写成功即认为“已记录”
如何用定时脚本安全推送钉钉
用 crontab 每 30 秒拉起一次命令,读取并批量消费队列文件。这不是“伪异步”,而是生产环境验证过的可靠模式。
- 命令入口:定义
php think send-error-log命令,读取/tmp/think_error_queue.jsonl,逐行解析,过滤出未发送过的error_id(查/tmp/sent_errors.log) - 签名必须计算:若钉钉机器人启用了加签,需按
timestamp + "\n" + secret计算 HmacSHA256,base64 编码后再urlencode,拼到 URL 末尾 - 单次最多发 10 条(留余量防限流),失败条目记入
/tmp/send_error_retry.jsonl,下次重试 - 推送成功后,把
error_id追加写入/tmp/sent_errors.log,用file_get_contents()+strpos()判断是否已存在,避免重复告警 - 务必设置
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 3000)和CURLOPT_CONNECTTIMEOUT_MS,超时立即放弃
钉钉 Webhook 安全与格式细节
很多报错不是逻辑问题,而是输在细节上。以下几处最容易翻车:
- Webhook URL 里如果带
&实体符(比如从网页复制),curl会解析失败 —— 务必检查 URL 是纯& - 消息体必须是 UTF-8 编码的 JSON,且
Content-Type: application/json头不能少,否则钉钉返回 400 - 加签密钥(
secret)是字符串,不是 base64,别误 decode;时间戳单位是毫秒,不是秒 - 如果设置了自定义关键词(如
[ERROR]),content字段里必须显式包含该词,否则钉钉静默丢弃 - 不要用
msgtype: markdown发原始 trace —— 换行、反引号、括号都会破坏渲染,优先用text+ 简洁摘要
真正难的不是调通第一次推送,而是让这套机制在 CPU 满载、磁盘 IO 高、网络分区时依然稳定落地 —— 所有外部依赖都得设防,所有写操作都要加锁,所有网络调用都得设硬超时。临时文件路径、重试日志位置、error_id 去重方式,这些看似琐碎的点,才是线上不出事的关键。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











