真正该触发钉钉告警的是panic/fatal错误、60秒内同错≥5次、支付回调超时等关键路径失败,或配置热更新失败等强感知事件;需用带缓冲chan统一收口异步错误并异步推送,避免阻塞和丢消息。

直接用 log 包或第三方日志库(如 zap、zerolog)本身不带告警能力,必须自己桥接错误通道和钉钉 HTTP 推送逻辑。关键不是“怎么打日志”,而是“哪些日志要升级为告警”以及“如何避免压垮钉钉接口或丢消息”。
哪些日志该走钉钉告警而不是普通日志
日志告警不是所有 ERROR 都推——那等于刷屏。真正该触发钉钉的,是满足以下任一条件的日志事件:
-
panic、fatal级别错误,或带traceID的链路级崩溃 - 同一错误在 60 秒内重复出现 ≥5 次(需聚合,否则单个 goroutine 崩溃会发 100 条)
- 业务关键路径失败:如支付回调超时、订单状态机卡死、DB 连接池耗尽
- 非错误但需强感知:如配置热更新失败、证书 72 小时后过期、某集群节点全部失联
普通 log.Error 或 zap.Error 只写文件/ES,除非显式调用告警函数,否则绝不发钉钉。
用带缓冲的 chan error 统一收口异步错误
goroutine 里出错没法 return 给主流程,defer recover() 又难覆盖所有场景。最稳的方式是让所有异步任务都往一个全局 channel “推”错误:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 声明:
var ErrCh = make(chan error, 100)—— 缓冲区太小(如 1)会导致发送阻塞,goroutine 泄漏;太大(如 10000)可能掩盖消费慢问题 - 启动独立监控 goroutine:
go func() { for err := range ErrCh { sendDingTalk(err) } }() - 每个异步任务里只做一件事:
select { case ErrCh —— 防止阻塞主逻辑 - 不要在每个 goroutine 里 new
http.Client,复用一个带Timeout: 4 * time.Second的全局 client
常见坑:panic: send on closed channel —— 说明 channel 被提前 close() 了,而 goroutine 还在往里塞;或者监控 goroutine 退出后没停写入方。
构建符合钉钉 webhook 规范的请求体
钉钉机器人要求消息必须含自定义关键词(如“告警”),且 Content-Type 必须是 application/json;charset=utf-8,否则静默丢弃:
- 用
text类型最简单,但注意字段名严格匹配:{"msgtype":"text","text":{"content":"告警: xxx"}} - 若用
markdown,标题必须以#开头,且内容中不能有未转义的**或```,否则解析失败返回400 - Webhook URL 中的
access_token是敏感信息,务必从环境变量读取,不要硬编码;URL 末尾不能有多余空格或换行 - 发送前校验:先用
curl -X POST -H "Content-Type: application/json" -d '{"msgtype":"text","text":{"content":"test"}}' "$WEBHOOK"手动测通
示例片段:
func sendDingTalk(err error) {
payload := map[string]interface{}{
"msgtype": "text",
"text": map[string]string{
"content": fmt.Sprintf("【PROD-ALERT】%v\nTraceID: %s", err, getTraceID()),
},
}
data, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", os.Getenv("DING_WEBHOOK"), bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json;charset=utf-8")
http.DefaultClient.Do(req)
}告警抑制与降噪必须手动实现
Alertmanager 有自带的抑制规则,但纯 Go 日志告警没有。如果你不处理,一个 DB 连接失败会引发下游 20 个服务连续报错,每条都发钉钉——结果就是运维半夜被 50 条重复消息叫醒。
- 按 error 类型 + 关键字段(如
host、service)做 key,用map[string]time.Time记录上次发送时间 - 每次准备推送前查表:如果
time.Since(lastSend) ,跳过本次 - 对
context.DeadlineExceeded这类高频错误,可额外加计数器,仅当 1 分钟内超 3 次才告警 - 恢复通知(resolved)不是必须的——日志系统本身不跟踪状态,要实现得额外存状态到 Redis 或本地 map
最易被忽略的一点:钉钉限流是「每分钟最多 20 条」,超频请求直接返回 429,但你的代码若没检查 resp.StatusCode,就会以为发成功了。必须显式判断并记录失败日志。

















