← 返回文章列表

Dify 企业级实验(06):可观测性体系——日志埋点与监控告警如何落地?

1. 业务场景

先讲一个我们实际遇到的场景。

客服系统线上跑着 3 个应用,偶发「回答慢、答非所问、调用失败」。半夜用户投诉,运维爬起来翻日志——日志散在三个应用里,格式还不一样,一翻两小时。

我们第一次接这类需求时,第一反应也是「出问题再翻日志呗」。真正动手才发现——翻日志是排障的下下策:日志散落、格式不一、没有贯穿 ID,定位一个请求要两个小时。可观测性不是锦上添花,是事故时的生命线,得在设计时就把观测点埋好。

生产系统跑挂了,最贵的是「定位时间」:是哪个应用、哪个节点、哪个环节出的问题。

这不是个例。任何「多应用线上运行、靠人工翻日志排障」的团队都是这个模式:应用越多,翻日志的代价越大。

2. 场景痛点

这个流程的痛点,在半夜爬起来翻日志的运维身上体现得最直接:

本质上,出问题不可怕,可怕的是「不知道哪里出了问题」

3. 方案:为什么是设计时埋点

Dify 的 code + http 节点足以搭出一套轻量可观测体系,本实验把「埋点、监控、排障」拆成三个应用。选它的理由:

这篇文章我们就用它搭一套「客服系统可观测体系」:埋点应用 + 监控应用 + 排障应用。

4. 整体架构

graph TD subgraph sub_cs["【客服应用】"] cs_start["开始"] cs_rid["生成请求ID(code)"] cs_llm["客服回答(LLM)"] cs_log["组装日志埋点(code)"] cs_write["写入统一日志(http)"] cs_end["结束"] cs_start --> cs_rid --> cs_llm --> cs_log --> cs_write --> cs_end end subgraph sub_order["【订单应用】"] o_start["开始"] o_query["查询订单(code)"] o_log["组装日志埋点(code)"] o_write["写入统一日志(http)"] o_end["结束"] o_start --> o_query --> o_log --> o_write --> o_end end subgraph sub_monitor["【监控应用】"] m_timer["定时触发"] m_pull["拉取统一日志(http)"] m_agg["聚合分析(code)"] m_thr["阈值检测(code)"] m_alert{"是否告警"} m_yes["组装告警载荷(code)"] m_webhook["Webhook 推送(http)"] m_end_alert["结束"] m_no["结束(输出健康报告)"] m_timer --> m_pull --> m_agg --> m_thr --> m_alert m_alert -- "是" --> m_yes --> m_webhook --> m_end_alert m_alert -- "否" --> m_no end subgraph sub_debug["【排障应用】"] d_start["开始(request_id)"] d_pull["拉取统一日志(http)"] d_trace["全链路 Trace(code)"] d_end["结束(诊断报告)"] d_start --> d_pull --> d_trace --> d_end end

链路很清晰:业务应用旁路埋点 → 监控应用定时聚合告警 → 排障应用按 request_id 拉全链路 Trace。埋点旁路(日志失败不影响业务)是这套体系的关键设计。

5. 模块设计

5.1 请求 ID 贯穿

请求 ID 贯穿:客服应用开始节点后接「生成请求ID」code 节点,调用方没传就自动生成:rid = request_id or ("REQ-" + str(int(time.time() * 1000))[-10:]),后续每条日志都带上。

5.2 日志埋点(旁路记录)

日志埋点(旁路记录):关键节点后接「组装日志埋点」code 节点,组装 KV 条目后 POST 到统一日志:

def main(request_id: str, query: str, answer: str) -> dict:

    import json, time

    entry = {"request_id": request_id or "", "app": "客服应用", "node": "lm_answer",

             "summary": str(answer or "")[:100], "status": "ok", "elapsed_ms": 800,

             "time": time.strftime("%Y-%m-%d %H:%M:%S")}

    return {"payload": json.dumps({"op": "append", "item": entry}, ensure_ascii=False)}

http 节点指向 http://172.19.0.50:8123/state/dify104_06_logs(KV 模拟服务,生产换 Redis/DB)。订单应用同理,其中一条把 status 置为 warn 模拟异常。

5.3 监控应用:聚合与阈值检测

监控应用聚合 code(定时触发 workflow,周一 09:00):

total = len(logs)

failed = sum(1 for e in logs if isinstance(e, dict) and e.get("status") != "ok")

fail_rate = failed * 100.0 / total if total else 0.0

latencies = [int(e.get("elapsed_ms", 0)) for e in logs if isinstance(e, dict)]

p95 = sorted(latencies)[int(len(latencies) * 0.95) - 1] if latencies else 0

阈值检测 code(告警分级逻辑就在这里):

rate = float(fail_rate or 0)

p = float(p95 or 0)

alerts = []

if rate > 5:

    alerts.append("失败率超阈值(>5%)")

if p > 20000:

    alerts.append("P95 耗时超阈值(>20s)")

level = "P1" if rate > 50 else ("P2" if alerts else "ok")

if-else 按 alert_level != "ok" 分流:告警分支组装载荷推送到 Webhook(本机用 KV /echo 模拟企业微信机器人端点),健康分支直接输出健康报告。

5.4 排障应用:全链路 Trace

排障应用 Trace code:从统一日志里按 request_id 过滤出该请求的全部节点记录,按时间排序拼成「节点 1 → 节点 2 → 节点 3(耗时/状态)」的诊断报告。

6. 运行验证

输入 预期 结果
客服应用 1 次 + 订单应用 2 次调用(其中 1 条 warn) 统一日志 3 条,request_id 贯穿 KV 收到 3 条(客服 1 + 订单 2)
监控应用触发聚合 失败率 33.3%(warn 计入)→ P2 告警推送 告警推送成功;再注入 failed 记录后失败率 50% → 再次告警
排障应用输入 request_id 输出该请求全链路 Trace 诊断报告 拉取 3 节点 Trace,定位到问题节点

环境:Dify 1.16.1(Docker Compose),模型 DeepSeek deepseek-v4-flash。4 个 DSL 均导入发布通过,Service API 验证。

7. 实战坑

现象 修复
code 节点沙箱禁写文件 写日志文件报 PermissionError(/tmp) 日志统一走 http 写入 KV 模拟服务(dify104-kv 容器,172.19.0.50:8123),生产换 Redis/DB,拓扑不变
埋点影响主流程 日志节点挂了业务也挂 埋点旁路:日志写入独立 http 节点,失败不影响业务分支(验证记录实测)
httpbin.org 本机不可达 Webhook 演示端点连不上 演示端点改用 KV /echo 模拟(实测)
工作流 http 访问内网被拦 拉日志 502/超时 本机 KV 已通过 SSRF_PROXY_ALLOW_PRIVATE_IPS=172.16.0.0/12 放行(实测经 squid 代理可达)
code 字符串状态判断 elif complete: 对字符串 "false" 恒真,分支全走错 判断必须 == "true"(交付说明实测)
告警阈值拍脑袋 误报/漏报 基线来自历史数据,先测再定阈值(102-18 测量思想)

8. 实验文档及源码获取

文章聚焦核心配置与采坑点,完整分步操作与故障注入步骤见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过