Dozzle 告警与 Webhook 通知实战指南:基于表达式引擎的容器日志、指标与事件监控
【免费下载链接】dozzleRealtime log viewer for containers. Supports Docker, Swarm and K8s.项目地址: https://gitcode.com/GitHub_Trending/do/dozzle
Dozzle 是一个支持 Docker、Swarm 与 Kubernetes 的实时容器日志查看器,其内置的告警系统允许你针对容器日志内容、CPU/内存指标和 Docker 生命周期事件自定义规则,并通过 Webhook(Slack、Discord、ntfy 或自定义端点)即时送达通知。本指南以 docs/fr/guide/alerts-and-webhooks.md 为主线,结合internal/notification后端实现与前端表单代码,完整讲解告警规则的三种类型、表达式语法、目的地配置、持久化要求,以及从表达式编译到消息分发的内部处理链路,读完即可在生产环境落地一套可复用的容器监控告警方案。
告警机制总览:规则永远留在自托管实例上
Dozzle 可以同时监听三种数据源,并在你描述的任意条件满足时发出通知:
| 告警类型 | 触发依据 | 典型用途 |
|---|---|---|
| 日志告警 | 日志消息匹配指定模式 | 5xx 错误、程序异常堆栈 |
| 指标告警 | CPU / 内存使用率越过阈值 | 容器 CPU 超过 90% |
| 事件告警 | Docker 上报的容器生命周期事件 | OOM 被杀、容器变为 unhealthy |
每条告警都同时绑定两个表达式:容器表达式(选择要监控哪些容器)与触发表达式(决定什么条件下触发),二者都使用 expr-lang/expr 表达式语法,并且全部在 Dozzle 自己的实例上本地求值——日志、指标和事件不会离开你的主机。
一个值得强调的架构事实是:告警规则与目的地配置持久化在容器的/data目录中,internal/notification/persist.go 中定义了两个默认路径:
const ( DefaultNotificationConfigPath = "./data/notifications.yml" DefaultCloudConfigPath = "./data/cloud.yml" )如果你希望配置在容器重启、镜像升级后仍然保留,就必须把/data以卷的形式挂载出来。
数据卷挂载与配置持久化
docker run 方式:
docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/data:/data -p 8080:8080 amir20/dozzle:latestdocker-compose 方式:
services: dozzle: image: amir20/dozzle:latest volumes: - /var/run/docker.sock:/var/run/docker.sock - /path/to/data:/data ports: - 8080:8080从源码层面看,/data目录的写入由Persister统一负责:每次你在界面上保存改动,persist.go 都会把当前内存中的Config(包含Subscriptions与Dispatchers)以 YAML 格式写回notifications.yml;服务启动时再通过Load()读回并应用。因此notifications.yml就是你告警体系的"地面真相",你也可以在仓库的 e2e 测试数据 e2e/data/notifications.yml 中看到该文件的示例结构。cloud.yml则单独保存 Dozzle Cloud 的 API Key 与元数据(persist.go),云端目的地不会被混入notifications.yml。
配置通知目的地(Webhook 与 Dozzle Cloud)
在创建任何告警之前,你需要先在Notifications页面点击Add destination配置至少一个通知目的地。
Webhook 目的地
Webhook 会向指定 URL 发送 HTTP POST 请求。Dozzle 为最常见的服务内置了现成的 payload 模板,这些模板直接定义在前端源码 assets/components/notifications/payloadTemplates.ts 中:
- Slack:使用 Slack 的 blocks + markdown 结构,正文为
*{{ .Container.Name }}*加{{ .Detail }},底部 context 区展示 Host 与 Image 信息; - Discord:适配 Discord Webhook API 的
content+embeds结构,包含 Host、Image 两个内联字段; - ntfy:推送主题默认为
dozzle-{{ .Container.HostName }},标题为容器名、正文为{{ .Detail }}; - Custom:通用的
{ container, message }JSON 结构,完全开放给你改造。
除了选模板,你还可以用 Go 的text/template语法编写完全自定义的 payload。渲染器在 internal/notification/dispatcher/webhook.go 中实现:若模板整体是合法 JSON,则先解析成 JSON 结构、再逐个把字符串字段中的{{ }}占位符解析渲染后重新序列化,从而保证日志消息里的引号、花括号等特殊字符被正确 JSON 转义;若模板不是合法 JSON,则退化为对整段文本执行 Go 模板。
自定义模板中可用的变量如下:
| 变量 | 说明 |
|---|---|
{{.Detail}} | 摘要内容(日志消息或指标数值) |
{{.Container.Name}} | 容器名称 |
{{.Container.Image}} | 容器镜像 |
{{.Container.HostName}} | Docker 主机名 |
{{.Container.State}} | 容器状态 |
{{.Log.Message}} | 日志消息正文 |
{{.Log.Level}} | 日志级别 |
{{.Log.Timestamp}} | 日志时间戳 |
{{.Log.Stream}} | 日志流类型(stdout/stderr) |
{{.Stat.CPUPercent}} | CPU 使用百分比 |
{{.Stat.MemoryPercent}} | 内存使用百分比 |
{{.Stat.MemoryUsage}} | 内存使用量(字节) |
{{.Subscription.Name}} | 告警规则名称 |
这些字段与后端数据结构一一对应,可对照 types/notification.go 中的NotificationContainer与NotificationLog定义核实。另外注意目的地还支持自定义 HTTP Header(DispatcherConfig.Headers,见 internal/notification/types.go),可用于携带鉴权令牌。
保存前务必使用Test按钮验证 Webhook 是否可用。该按钮调用后端的SendTest(webhook.go):它会真实发出一次 POST 请求,2xx 视为成功,非 2xx 会返回状态码与错误信息,方便你排查 URL、鉴权或模板问题。
关于安全性,webhook.go 的实现细节值得说明:
- 仅允许
http与https两种 URL 协议; - 自定义
DialContext会先解析目标 IP 并拒绝回环地址、链路本地地址、组播地址及0.0.0.0/8等存在 SSRF 风险的地址段(包括 6to4、NAT64、Teredo 等 IPv6 过渡地址中内嵌的 IPv4);RFC1918 私有网段被有意放行,因为自托管 Webhook(如内网 Mattermost、Home Assistant)通常就部署在私有网络中; - HTTP 客户端设置 10 秒超时(
Timeout: 10 * time.Second)。
Dozzle Cloud 目的地
已绑定 Cloud 的实例会自动获得Dozzle Cloud目的地,无需逐项配置。与裸 Webhook 不同,Cloud 端会将重复故障聚合为一条通知、生成事件摘要,并分发到邮箱、Telegram、Discord、Slack、ntfy 以及浏览器推送,同时支持静音与移动端渠道。云端的配置(API Key、前缀、过期时间)单独保存在cloud.yml中(persist.go)。完整能力见 Dozzle Cloud 指南。
创建一条告警:三步表单与容器表达式
在Notifications页面点击Add alert进入创建流程。前端表单 assets/components/notifications/AlertForm.vue 把创建过程组织为三个步骤:
- 选择告警类型(log / metric / event);
- 编写容器过滤表达式(决定监听哪些容器);
- 编写触发表达式(随类型不同分别渲染 LogAlertFields.vue、MetricAlertFields.vue 或 EventAlertFields.vue)。
容器表达式
容器表达式负责圈定要监控的容器,可用属性如下:
| 属性 | 类型 | 示例 |
|---|---|---|
name | 字符串 | name contains "api" |
image | 字符串 | image == "nginx:latest" |
state | 字符串 | state == "running" |
health | 字符串 | health == "unhealthy" |
hostName | 字符串 | hostName == "prod-host" |
labels | map | labels["env"] == "production" |
对照后端 types/notification.go,NotificationContainer实际还额外暴露了id与hostId两个字段,需要按容器 ID 精确匹配时也可使用。
条件之间用&&(与)、||(或)、!(非)组合:
name contains "api" && labels["env"] == "production"表单内置了实时校验与自动补全(ExpressionField),并在你输入时实时预览当前命中的容器数量与名称(见 AlertForm.vue 的匹配结果提示),确认无误后再保存。
日志告警(Log Alerts)
日志表达式
日志表达式过滤触发告警的日志消息,可用属性:
| 属性 | 类型 | 示例 |
|---|---|---|
message | 字符串/map | message contains "error" |
level | 字符串 | level == "error" |
stream | 字符串 | stream == "stderr" |
type | 字符串 | type == "complex" |
对于 JSON 格式的日志,可用点号访问嵌套字段:
message.status >= 500 && message.path contains "/api"支持的字符串操作符包括contains、startsWith、endsWith和matches(正则)。
这里有一个重要的实现细节:message字段之所以既可能是字符串也可能是 map,是因为后端在 types.go 的extractMessage中做了归一化——简单日志原样返回字符串;多片段(分组)日志按行拼接为单个字符串;JSON/对象日志则把OrderedMap转换为普通 map 以兼容 expr 求值。另外在 processing.go 中,Dozzle 会主动跳过自己容器(镜像名含amir20/dozzle)的日志,避免告警-日志-告警的反馈循环。
日志告警示例
监控生产环境所有容器的错误日志:
Container: labels["env"] == "production" Log: level == "error"监控 API 容器的 HTTP 5xx 错误:
Container: name contains "api" Log: message.status >= 500监控特定镜像的所有 stderr 输出:
Container: image startsWith "myapp/" Log: stream == "stderr"监控生产环境 API 的慢响应:
Container: name contains "api" && labels["env"] == "production" Log: message.duration > 5000 && message.path contains "/api"用正则监控认证失败(大小写不敏感):
Container: name contains "auth" || name contains "gateway" Log: message matches "(?i)(unauthorized|forbidden|invalid token)"在 internal/notification/processing.go 的日志处理链路中,每一条命中表达式的日志都会立即触发一次通知(日志告警没有冷却窗口,由表达式本身的精确度来控制消息量),Detail字段会带上完整的日志消息或 JSON 序列化结果(formatLogMessage)。
指标告警(Metric Alerts)
指标告警在容器 CPU 或内存使用率越过阈值时触发。触发表达式作用于在滑动窗口内采样的统计值的平滑均值上,从而避免短暂尖峰造成误报。
指标表达式
可用属性:
| 属性 | 类型 | 说明 |
|---|---|---|
cpu | 数值 | CPU 使用百分比(0–100),与界面显示一致 |
memory | 数值 | 内存使用百分比(0–100) |
memoryUsage | 数值 | 内存使用量(字节) |
cpu与界面上数值一致这点有源码保证:processing.go 会先用容器的CPULimit(无限制时回退到宿主机核心数)对原始 per-core 百分比做归一化,得到 0–100 的整体负载百分比。此外,NotificationStat还额外暴露了mounts字段(挂载点磁盘占用),支持如any(mounts, .usedPercent >= 85)的表达式(types/notification.go),可用于磁盘空间告警。
采样窗口与冷却时间
- 采样窗口(Sample Window):表达式求值前参与平均的统计秒数。窗口越长越平滑、短则响应更快。代码实现见 types.go:默认 15 秒,合法范围被钳制在 1–300 秒。
- 冷却时间(Cooldown):同一容器两次连续触发之间的最小间隔秒数,用于避免容器长期超标时告警刷屏。代码默认值为 300 秒,钳制在 0–3600 秒(types.go)。
指标告警还有一个"缓冲确认"机制:RecordMetricSample(types.go)为每个容器维护一个环形缓冲,只有当窗口内至少 80% 的采样点都命中表达式时才真正触发;窗口为 1 秒时则退化为即时判断。也就是说,"持续超标"才会告警,一次偶然的毛刺不会打扰你。
指标告警示例
监控生产环境容器的高 CPU:
Container: labels["env"] == "production" Metric: cpu > 90监控特定服务的内存压力:
Container: name contains "api" Metric: memory > 85监控绝对内存用量(1 GiB):
Container: name == "postgres" Metric: memoryUsage > 1073741824触发后通知的Detail字段会附带实时数值,格式为CPU: 87.3%, Memory: 45.1%(见 processing.go)。
事件告警(Event Alerts)
事件告警直接监听 Docker 上报的容器生命周期事件,非常适合在不分析日志的前提下捕捉崩溃、OOM 杀死与健康状态变化。
事件表达式
可用属性:
| 属性 | 类型 | 说明 |
|---|---|---|
name | 字符串 | 事件名称(见下方列表) |
actorId | 字符串 | Docker actor 标识(通常是容器 ID) |
attributes | map | Docker 事件属性(随事件类型而异) |
timestamp | 时间 | 事件发生时刻 |
常见的 Docker 事件名包括start、stop、die、kill、oom、restart、destroy和health_status。从实现层面看,事件监听器在 internal/notification/event_listener.go 内部维护了一个事件白名单(start、stop、die、restart、health_status、oom、kill),白名单之外的事件会被直接丢弃,这可以理解为可编写规则的稳定事件子集。
对于health_status事件,Docker 原生上报的名称形如health_status: healthy/health_status: unhealthy。Dozzle 在normalizeEvent(event_listener.go)中把事件名归一化为裸的health_status,并把健康状态写入attributes["healthStatus"],因此你可以直接写:
name == "health_status" && attributes["healthStatus"] == "unhealthy"事件告警示例
生产环境容器停止时告警:
Container: labels["env"] == "production" Event: name == "die"OOM 被杀时告警:
Container: true Event: name == "oom"容器变为 unhealthy 时告警:
Container: true Event: name == "health_status" && attributes["healthStatus"] == "unhealthy"监控异常退出(排除正常停机):
退出码 0(成功)、130(SIGINT)、143(SIGTERM)、137(SIGKILL)通常出现在docker stop、Ctrl+C 或更新换代的正常流程中,需排除以免噪音;真正的错误退出(1、2、125 等)始终触发:
Container: name contains "worker" Event: name == "die" && !(attributes["exitCode"] in ["0", "130", "143", "137"])后端还会把退出码翻译成可读的信号名:describeExitCode(processing.go)维护了 129=SIGHUP、130=SIGINT、131=SIGQUIT、134=SIGABRT、137=SIGKILL、139=SIGSEGV、143=SIGTERM 的映射,因此die事件的Detail会呈现为Container event: die (exit code 137, SIGKILL)这样的信息。特别地,137 常被误读为 OOM,而真正的 OOM 会以独立的oom事件上报——代码注释中对此有明确解释。
事件告警同样受冷却时间控制,同一容器在冷却窗口内不会重复触发(processing.go)。
告警管理:启用、统计与删除
在 Notifications 页面,每条告警都支持:
- 启用/停用:无需删除即可临时关停某条规则;
- 编辑:随时修改表达式与绑定的目的地;
- 查看统计:包括累计触发次数、命中的容器集合、最近一次触发时间;
- 删除:移除不再需要的规则。
这些运行期统计在后端有对应的数据结构支撑:Subscription上的TriggerCount(原子计数器)、LastTriggeredAt(最近触发时间指针)与TriggeredContainerIDs(触发过的容器 ID 集合)都是不落盘的运行时字段(internal/notification/types.go)。并且当主节点向 Agent 同步配置时,HandleNotificationConfig会保留已有订阅的触发统计,只替换表达式与目的地,避免每次同步都清零历史数据(internal/notification/config.go)。
从表达式到通知:内部处理链路
了解底层原理有助于你写出更高效、更准确的规则。完整链路可分为三层,全部位于 internal/notification 目录:
第一层:表达式编译。每次创建或加载规则时,CompileExpressions(types.go)会用expr-lang/expr把容器表达式、日志表达式、指标表达式、事件表达式分别编译为预编译的vm.Program,并绑定对应的环境类型(NotificationContainer/NotificationLog/NotificationStat/NotificationEvent)。编译失败(如语法错误)会在保存时立即报错,而不是等到触发时才暴露。运行时MatchesContainer、MatchesLog等则对编译产物求值,类型不匹配(例如对 JSON 日志写字符串操作)会被安全地视为不匹配并记录调试日志。
第二层:数据监听。三个监听器并行订阅三类数据源:
ContainerLogListener(log_listener.go):遍历所有 client 的容器列表,只对匹配容器表达式的容器建立日志流(StreamLogs),并支持容器动态启停时增量增删流;ContainerStatsListener(stats_listener.go):订阅每秒统计流,并通过 5 秒 TTL 缓存解析容器与主机信息;ContainerEventListener(event_listener.go):订阅 Docker 事件流,先归一化health_status,再过滤白名单与 Dozzle 自身容器。
第三层:匹配与分发。processing.go 中的三条处理循环(processLogEvents/processStatEvents/processDockerEvents)逐一检查订阅的容器表达式与触发表达式,命中后更新统计并构造types.Notification投递给订阅绑定的目的地。分发过程由信号量限流(防止积压时雪崩),单次发送带 30 秒超时(processing.go),失败会记录错误日志但不会重试。
一句话总结这条链路:规则编译为字节码 → 监听器按容器表达式定向采集数据 → 处理器按触发表达式实时求值 → 命中后通过目的地分发。理解了这条链路,你就能根据实际负载合理选择采样窗口与冷却时间,也能理解为什么规则必须保存在本地/data——因为整个求值过程都发生在你的实例上,日志与指标从不外流。
至此,你已经掌握了 Dozzle 告警系统的全部核心:从挂载/data持久化规则、配置 Slack/Discord/ntfy 等 Webhook 目的地,到编写容器、日志、指标、事件四类表达式,再到理解底层编译与分发机制。你可以直接从上述示例复制规则开始,再逐步调整采样窗口与冷却参数,让告警噪音与漏报之间达到最佳平衡。
【免费下载链接】dozzleRealtime log viewer for containers. Supports Docker, Swarm and K8s.项目地址: https://gitcode.com/GitHub_Trending/do/dozzle
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考