Excalidraw 与 OpenTelemetry 链路追踪的可视化融合实践
在一次深夜排障中,团队盯着 Jaeger 界面上密密麻麻的调用链发愁:一个简单的用户请求竟横跨了十几个服务,嵌套层级深得像迷宫。虽然数据齐全,但没人能快速说清“到底哪一环最慢”。这时有人开玩笑:“要是能把这堆 trace 画成我们白板上那种手绘架构图就好了。”——这句话成了本文的起点。
将OpenTelemetry 的链路追踪能力与Excalidraw 的视觉表达力相结合,并非炫技,而是试图回答一个现实问题:当可观测性数据越来越丰富时,如何不让信息过载成为理解系统的障碍?
从 Trace 到图形:一场认知效率的重构
传统 APM 工具(如 Zipkin、Jaeger)擅长展示时间轴上的 span 分布,却往往牺牲了空间结构的直观性。开发者需要在“时间维度”和“拓扑关系”之间自行脑补映射。而 Excalidraw 提供了一个天然的中间层:它不替代后端分析系统,而是作为“认知加速器”,把冷冰冰的 trace 数据转化为一眼可读的服务拓扑草图。
设想这样一个场景:运维人员发现某接口 P99 超标,点击一键生成“最近失败请求”的 Excalidraw 拓扑图。画布上,核心路径清晰展开,异常节点以红色加粗边框标记,旁边还贴着 AI 自动生成的便签:“B 服务平均延迟 820ms,占总耗时 76%”。无需切换多个页面,根因已跃然纸上。
这种体验的背后,是两种技术范式的互补:
- OpenTelemetry解决的是“数据采集标准化”问题,确保无论 Java、Go 还是 Node.js 服务,都能输出统一格式的 trace;
- Excalidraw解决的是“信息呈现人性化”问题,用接近纸笔绘图的风格降低心理距离,尤其适合跨职能沟通。
它们的交汇点,正是现代软件协作中最稀缺的资源——共同语境。
Excalidraw 如何承载动态追踪数据
很多人认为 Excalidraw 只是个静态绘图工具,实则不然。它的开放 JSON 数据模型和可编程 API,使其完全可以作为一个轻量级的“前端图表引擎”嵌入系统。
数据结构的本质:元素即状态
Excalidraw 场景中的每个图形都是一组 JSON 对象,包含位置、类型、样式等元信息。例如一个代表微服务的矩形,其结构如下:
{ "type": "rectangle", "x": 100, "y": 200, "width": 120, "height": 60, "strokeColor": "#c92a2a", "backgroundColor": "#fff", "roughness": 2, "id": "service-user" }关键在于,这些字段完全可以由程序生成。比如strokeColor可根据对应服务的响应延迟动态计算:
function getColorByLatency(ms) { if (ms > 1000) return "#d9480f"; // 橙红:严重 if (ms > 500) return "#c92a2a"; // 红色:警告 return "#37b24d"; // 绿色:正常 }类似地,箭头连线的粗细也可以反映调用量或错误率。这样一来,一张图就不再是快照,而是运行时状态的实时投影。
动态注入:让代码“画画”
通过官方提供的updateScene接口,我们可以批量添加或更新元素。以下是一个典型封装函数:
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types"; const addServiceNode = (excalidrawAPI, name, x, y, latency) => { const color = getColorByLatency(latency); const rect: ExcalidrawElement = { type: "rectangle", version: 1, isDeleted: false, id: `service-${name}`, fillStyle: "hachure", strokeWidth: 2, roughness: 2, opacity: 100, x, y, strokeColor: color, backgroundColor: "#fff", width: 120, height: 60, seed: 1, groupIds: [], shape: null, }; const text = { type: "text", version: 1, isDeleted: false, id: `label-${name}`, x: x + 10, y: y + 20, strokeColor: "#000", text: `${name}\n(${latency}ms)`, fontSize: 14, fontFamily: 1, textAlign: "left", verticalAlign: "top", }; excalidrawAPI.updateScene({ elements: [rect, text], }); };这个函数可以被 trace 处理服务调用,输入来自 OpenTelemetry 的 span 数据,输出则是可视化的节点。整个过程完全自动化,无需人工干预。
更进一步,还可以利用 Excalidraw 的customData扩展字段绑定原始 trace 上下文:
// 在元素上附加元数据 rect.customData = { serviceName: name, traceId: "abc123...", spans: ["span-a", "span-b"] };这样,用户点击某个节点时,前端即可提取traceId并跳转至详细分析页面,实现“概览 → 深入”的无缝导航。
OpenTelemetry:构建可信的数据底座
如果说 Excalidraw 是舞台上的演员,那 OpenTelemetry 就是幕后精准计时的导演。没有可靠的数据来源,再漂亮的可视化也只是空中楼阁。
标准化采集:一次埋点,多端受益
OpenTelemetry 的最大优势在于其厂商中立性和跨语言支持。无论是 Spring Boot 应用自动注入的 Java Agent,还是 Python 的opentelemetry-instrument,都能生成符合 OTLP 协议的 trace 数据。
以一段 Python 示例为例:
from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) span_processor = BatchSpanProcessor(ConsoleSpanExporter()) trace.get_tracer_provider().add_span_processor(span_processor) def handle_request(): with tracer.start_as_current_span("handle_request") as span: span.set_attribute("http.method", "GET") span.set_attribute("http.url", "/api/v1/data") with tracer.start_as_current_span("fetch_from_database") as db_span: db_span.set_attribute("db.system", "postgresql") db_span.set_attribute("db.operation", "SELECT") import time time.sleep(0.1)这段代码记录了一个典型的请求流程,包括入口处理和数据库访问。每个 span 都携带了语义化属性(semantic conventions),如http.method、db.system,这为后续分析提供了结构化依据。
更重要的是,这些数据可以通过 OpenTelemetry Collector 统一接收并转发至多种后端——既可以存入 Jaeger 做深度分析,也能送入我们的 trace 处理服务生成 Excalidraw 图谱。
关键参数的意义不只是字段
| 参数 | 实际用途 |
|---|---|
traceId | 全局串联一次请求的所有片段 |
spanId/parentSpanId | 构建调用树,识别父子关系 |
startTimeUnixNano,endTimeUnixNano | 计算持续时间,定位瓶颈 |
attributes | 注入业务上下文,如租户 ID、环境标签 |
其中,attributes尤其值得重视。合理使用语义约定(Semantic Conventions),能让不同团队的数据具备可比性。比如所有 HTTP 客户端都设置http.status_code,那么在生成图形时就能统一用颜色标识错误状态。
架构整合:从数据到协作画布
真正的价值不在于单次绘图,而在于建立一条从“运行时行为”到“设计反馈”的闭环通道。为此,我们设计了如下架构:
[微服务集群] ↓ (OTLP) [OpenTelemetry Collector] ↓ (gRPC/HTTP) [Trace Processing Service] ←→ [LLM Gateway] (可选) ↓ (JSON Scene Data) [Excalidraw Instance (Embedded)] ↑ [User Browser]各组件职责明确:
- Collector:承担协议转换与缓冲,避免直接暴露应用到外部服务。
- Processing Service:核心逻辑所在,负责解析 trace、提取拓扑、聚合指标,并生成 Excalidraw 兼容的 JSON。
- LLM Gateway(可选):用于自然语言摘要生成。例如输入一段 trace 数据,返回:“该请求经过 Auth → Order → Payment,Payment 服务出现超时”。
- Excalidraw 实例:嵌入内部 DevOps 平台,支持查看、标注、导出。
实际工作流示例
- 用户触发
/checkout请求; - OpenTelemetry SDK 记录完整 trace;
- 后端服务监听新 trace 到达事件;
- 解析出服务列表:
auth,cart,payment; - 计算调用顺序与延迟:
auth(120ms) → cart(80ms) → payment(950ms); - 生成三个矩形节点 + 两条箭头线,payment 节点标红;
- 插入 LLM 生成注释:“支付环节显著拖慢整体性能”;
- 渲染至共享画布,团队成员即时可见。
整个过程可在秒级内完成,极大缩短“发现问题 → 共识问题”的时间窗口。
设计之外的考量:安全、性能与演进
技术整合从来不只是功能拼接,更要考虑落地细节。
数据脱敏不容妥协
生产环境的 trace 可能包含敏感信息:用户 ID、令牌、完整 SQL 语句等。在导入 Excalidraw 前必须进行清洗:
def sanitize_attributes(attrs): sensitive_keys = ["http.request.header.authorization", "user.id", "db.statement"] return {k: v for k, v in attrs.items() if k not in sensitive_keys}此外,建议仅允许授权用户访问图形生成接口,防止内部拓扑外泄。
避免画布爆炸
对于复杂 trace(>100 个 spans),直接渲染会导致视觉混乱。应采取聚合策略:
- 将同一服务的多个操作合并为单一节点;
- 使用不同图标区分服务类型(数据库、缓存、外部 API);
- 支持“聚焦模式”:默认只显示主干路径,点击展开子调用。
版本化与追溯
生成的 Excalidraw JSON 可存储至 Git,配合 CI 流程实现“架构图即代码”:
# ci.yaml - name: Generate Architecture Snapshot run: | python generate_trace_diagram.py --trace-id $TRACE_ID \ > diagrams/${SERVICE}_${TIMESTAMP}.excalidraw.json git add diagrams/ git commit -m "Update diagram for $TRACE_ID"这样,每次重大变更都有据可查,新人也能通过历史版本理解系统演化路径。
更远的未来:AI 驱动的智能协作
当前方案仍需主动触发“生成图表”动作,下一步方向是让系统变得更主动。
想象一下:
- 当监控系统检测到错误率上升,自动创建 Excalidraw 画布并@相关负责人;
- 开发者语音输入:“画出登录流程的调用链”,后台调用 LLM 解析意图,查询 trace 并渲染;
- 在画布上圈选两个服务,提问:“它们之间有没有循环依赖?”——AI 分析历史 trace 自动回答。
Excalidraw 已支持插件系统和脚本执行,结合 LLM 的推理能力,这类交互并非遥不可及。
更重要的是,这种结合正在模糊“设计文档”与“运行证据”之间的界限。过去,架构图往往是理想化的蓝图;而现在,我们可以让每一次真实流量来“绘制”它自己的路径,从而推动设计不断贴近现实。
这种从 trace 到手绘图的旅程,本质上是一场对“可理解性”的追求。在一个系统日益复杂的年代,我们需要的不仅是更多数据,更是更聪明的呈现方式。而 Excalidraw 与 OpenTelemetry 的联手,或许正是通向这一目标的一条小径——既不失严谨,又保有温度。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考