一个负责「决定要不要半夜叫醒你」的系统,最大的恐惧是变成黑盒。这篇讲 WebhookWise 怎么把可观测性做成第一等公民——从 OTLP 出口、Alloy 收集、指标/追踪/日志/事件/信号/剖析六类信号,到一个会让「没人看的指标」直接 CI 失败的离线契约。
架构和整体设计见前一篇《WebhookWise:把吵闹的告警做成一个小型 AIOps 控制面》,这篇只聚焦可观测性。
一、为什么可观测性是第一等公民
当系统由 AI 来决定「这条告警该不该转发」时,最怕的一句话是:「它为什么没发?」——如果答不上来,On-Call 工程师迟早不信任它、绕过它。
所以 WebhookWise 的原则是:没有不留痕迹的拦截,也没有不明不白的故障。每一个决策点、每一次投递、每一段耗时,都要可追溯、可解释、可干预。
实现上它是 OTel-first、纯 OTLP 出口的:应用自己不暴露 /metrics 端点、不 tail 文件进 Loki、也不依赖 prometheus_client——所有指标、追踪、日志都通过 OpenTelemetry 走 OTLP 出去,由采集器分发。三个角色(API、Worker、Scheduler)共享同一套 resource(service.name/namespace/version、deployment.environment),schema 版本钉死在 1.41.0,语义规范升级要走显式的 schema 迁移。
二、遥测管道:从应用到大盘
1 | API / Worker / Scheduler ──OTLP(gRPC/HTTP)──▶ Alloy ──┬─▶ Prometheus (指标, 带 exemplar) |
各组件的角色:
- Alloy:中心采集器,OTLP gRPC :4317 / HTTP :4318 收流,按信号分发到各后端;把 resource 属性摊平成 Prometheus 安全的标签;开启 exemplar 转发(这是「延迟采样点 → 一键跳 trace」的基础)。
- Prometheus:指标库,开了 native-histograms 和 exemplar-storage。
- Tempo:追踪库,还跑 metrics-generator 生成 service graph 和 span metrics 回写 Prometheus。
- Loki:结构化日志。
- Pyroscope:持续剖析。这是唯一不走 OTLP 的一条路——用 Pyroscope SDK 直推(Python 的 profile 导出目前经 Pyroscope 比经 OTel profiles 更成熟)。
- Beyla:eBPF 自动埋点,零改代码地给 API 容器补上 HTTP/SQL/Redis 的 span 和指标。
- Grafana:面板 + 数据源,全部 provision 化。
一个很实用的效果:Grafana 里数据源做了互相关联——Prometheus 的 exemplar 指向 Tempo,Tempo 的 trace 能跳到 Loki 日志(filterByTraceID)和 Pyroscope 火焰图,Loki 的 derivedFields 又能从日志里的 trace_id 跳回 Tempo。指标尖峰 → 具体 trace → 那条日志 → 那段 CPU 火焰图,一路点下去,不用手动拼查询。
三、异步链路里的一条连贯 trace
WebhookWise 是「接收即入队、异步处理」的,最容易断链的就是 trace:请求在 FastAPI 边界进来,丢进 Redis Stream,由 TaskIQ worker 另一个进程消费。
做法是手动透传 W3C traceparent:入队时 inject_trace_headers() 把当前上下文注入任务头(外加一个业务 X-Request-Id),worker 侧 trace_context_from_headers() 再把它接回来。于是在 Tempo 里看到的是一条连贯的调用链:webhook.parse → dedup → analyze → noise(管道各阶段的 span 名)→ worker.webhook_process_task → GenAI 的 chat span → 转发/outbox span,跨进程也不断。
管道每个阶段的 span 和它的指标是同一处代码发出来的(一个 _instrument_step 包装器同时 set span 属性 + 记 webhook.pipeline.step.duration),所以 span 和 metric 永远对得上、不会漂。LLM 调用的 span 挂的是 OTel GenAI 语义属性(gen_ai.system/request.model/usage.input_tokens…),但prompt 和 completion 正文刻意不进遥测。
四、结构化日志 + 日志↔追踪关联
日志是结构化 JSON(OTel 日志数据模型:severity_text/severity_number/body/资源属性/exception.*),走 QueueHandler 异步写、不占热路径。每条日志由一个过滤器自动注入当前 span 的 trace_id / span_id / trace_flags(没有 span 时用一个合成 id 兜底,这样 OTel 关掉时日志仍能按请求串起来)。
一个刻意的设计:高基数的东西不进 Loki 标签。trace_id/span_id/request_id/webhook_event_id/url 这些都留在日志正文里(靠 Grafana derived fields 查),Loki 标签只留一小撮低基数的(severity/event.name/signal.name/webhook.source/…)。高基数标签是拖垮 Loki 的头号原因,这条被写进了契约检查(见第六节)。
五、六类信号:不止指标/追踪/日志
除了三大件,WebhookWise 还显式建模了事件(events)、信号(signals)、剖析(profiles):
- 事件
emit_event(name, ...):一次性的业务里程碑,同时三路发出——给当前 span 加一个 span event、写一条结构化日志、并把observability.events{event.name}计数 +1。比如webhook.analysis.completed、webhook.storm.suppressed。 - 信号
record_signal(name, state, ...):状态迁移,低基数。比如record_signal("circuit_breaker", "open", ...)、record_signal("webhook.task", "completed"/"error"/"suppressed")。 - 剖析:Pyroscope 持续采样,开了 span-profile 关联,能从一条慢 trace 直接看到当时的 CPU 火焰图。
这样「一件事发生了」既能在指标上聚合(多少次)、又能在某条 trace 上定位(这一条)、还能在日志里看到上下文——三个视角同源。
六、杀手锏:一个会让「没人看的指标」CI 失败的离线契约
这是我认为 WebhookWise 可观测性最值得说的一点,也是它区别于「随便挂个 OTel」的地方。
有一条铁律:不允许存在没有消费方的指标——每个 metric 都必须被某个 dashboard 面板、告警规则、SLO 或 preset 查询消费,否则它就不该存在。指标的价值在于被用,不在于数量。
这条铁律不是靠自觉,是靠一个离线契约工具在 CI 里强制的(webhookwise_observe.py contract,不需要连任何后端):
- 它 AST 解析
metrics.py,抽出每一个Counter/Gauge/Histogram(name, ..., unit=...)定义,按 OTel→Prometheus 规则展开成实际的 series 名(Counter→_total、Histogram→_bucket/_count/_sum、Gauge→_ratio等,还带单位后缀s→_seconds、By→_bytes); - 再扫出所有被两张 Grafana dashboard、
alerts.yml的录制/告警规则、以及 preset 目录引用的指标; - 定义了但没被任何一方引用的指标 → CI 直接挂。反向也查:dashboard/告警引用了一个没定义的指标,同样挂。
除了指标消费闭环,同一个契约还跑另外几项(都离线):
- loki 标签基数守卫:Alloy 的 Loki 标签里出现
trace_id/webhook_event_id/url这类高基数键就失败; - 禁敏感标签:Loki 标签里出现
token/secret/password/authorization/cookie/prompt就失败; - 结构化日志强制:全仓不许出现裸
extra={,必须走统一的log_extra()helper; - schema 版本一致性:
1.41.0要在 env/compose/docs 里保持一致; - 无陈旧遥测名 / PromQL 语法平衡 等。
效果:可观测性配置和代码一起被 CI 管住了。加一个指标却忘了上大盘?挂。给 Loki 加了个高基数标签?挂。日志里手滑写了个敏感字段当标签?挂。可观测性不再是「上线后慢慢补」的二等公民。
七、告警与自我监控(含一个 dogfooding 细节)
大盘之外,Prometheus 侧有成套的多窗口 SLO 燃尽率告警(API 可用性、ingress、处理、转发投递各有 fast-burn / slow-burn),AI 侧有降级率、错误、p95 延迟告警,基础设施侧有队列积压、死信、DB 连接池近满、Redis 不可用、熔断器打开、outbox 积压过久等。每条告警都带 runbook 注解,很多直接指向 webhookwise_observe.py runbook <AlertName> 这个命令,把「收到告警→该查哪些 PromQL/Loki/Tempo」固化下来。
一个有意思的闭环:Alertmanager 触发的告警,会回推给 WebhookWise 自己的 /v1/webhook/alertmanager——它吃自己的狗粮,用自己这套告警中枢去处理自己的告警。
八、想自己跑一遍
这套可观测性栈有一个可本地拉起的学习实验室(一个独立的 docker compose 项目:Alloy + Prometheus + Tempo + Loki + Pyroscope + Beyla + Grafana + Alertmanager + k6),带分步教程(指标 / 日志与追踪 / 剖析 / RUM 与压测):
https://github.com/itswl/WebhookWise/blob/main/docs/operations/observability/local-lab/README.md
小结
WebhookWise 的可观测性不是「挂个 OTel 收 CPU 内存」,而是:
- 纯 OTLP 出口 + Alloy 分发,六类信号(指标/追踪/日志/事件/信号/剖析)同源;
- 异步链路一条连贯 trace,指标↔追踪↔日志↔剖析 一键互跳;
- 高基数守卫 + 敏感字段禁入,让 Loki 不被拖垮、日志不泄密;
- 一个离线契约把「没人看的指标」挡在 CI 门外,让可观测性和代码一起被管住。
一个聪明的告警大脑,还得配一套坚固、透明的神经系统——每个动作都可被追溯、可被解释、可被干预。