Spring Insight 里如何把 Span 画成瀑布时间线

Spring Insight 里如何把 Span 画成瀑布时间线

作者:苏渡苇

项目地址github.com/iweidujiang...(感谢 Star !)

点开一行,扁平列表还画不出快慢

上一篇把链路列表收成一行摘要:入口、墙钟耗时、整行有没有 ERROR。

点进去之后,人真正想看的是 哪一段把时间吃掉了 。接口给回来的仍是一堆扁平 Span,有 startTimedurationMsparentSpanId,但没有现成的左右坐标,也没有现成的缩进。

详情页把这堆数据交给前端的 buildTraceTimeline,算出每条的深度、相对整段链路的偏移、条宽,再画成瀑布。Server 不预计算图形,只保证同一 traceId 的 Span 按开始时间排好、以快照交出去。

这篇就顺着这条线:明细怎么捞,树怎么深,耗时时间条怎么落在时间轴上,橙边那条关键路径又是怎么爬出来的。

详情接口只负责把这条 Trace 捞齐

点列表某一行,浏览器再打一次:

text 复制代码
GET /api/v1/ui/traces/{traceId}

后端是 TraceSpanPersistenceService.getTraceById:加锁扫内存,留下 traceId 相同的记录,按 startTime 升序,再 snapshot() 一份交出去。没有索引,也没有在 Server 里算深度。

java 复制代码
return spans.stream()
        .filter(s -> traceId.equals(s.getTraceId()))
        .sorted(Comparator.comparing(s -> n(s.getStartTime())))
        .map(TraceSpan::snapshot)
        .collect(Collectors.toList());

页面顶部那几个数------总耗时、Span 数、涉及服务、异常条数------都是前端拿这份列表算的,不是列表接口里那行摘要原样搬下来。两边算法接近,数据源都是同一批 Span。

缩进靠 parentSpanId 往上爬

瀑布左侧的服务名会往里缩,一层父子缩一档,CSS 是 12 + depth * 16 像素。depth 不是接口字段,是前端用 parentSpanId 算的。

computeDepths 先按 spanId 建一张 Map,再对每个节点往父节点递归:找到父就 +1,父不在这批数据里、或根本没有 parent,深度就是 0。顺手用一个 visiting 集合防环,链路数据若把父子指成圈,直接停在 0,避免把浏览器递归炸了。

ts 复制代码
const depthOf = (spanId: string, visiting: Set<string>): number => {
  if (depthCache.has(spanId)) return depthCache.get(spanId)!
  if (visiting.has(spanId)) return 0
  visiting.add(spanId)
  const span = byId.get(spanId)
  if (!span || !span.parentSpanId || !byId.has(span.parentSpanId)) {
    depthCache.set(spanId, 0)
    return 0
  }
  const d = depthOf(span.parentSpanId, visiting) + 1
  depthCache.set(spanId, d)
  return d
}

跨服务时,下游进程如果没把上游的 spanId 当成 parent 传过来,这批里就会出现好几个深度为 0 的根,树会摊平。那不是画图算错,是采集时上下文没接上。

耗时条的位置:相对最早开始的偏移

时间轴的零点是这批 Span 里最早的 startTime,右端是最晚的结束。缺 durationMs 就用 endTime - startTime 补,还补不上就先记 1 毫秒,避免宽度算成 0 看不见。

每条相对零点的偏移、自身耗时,再除以整段墙钟,变成百分比:

ts 复制代码
offsetMs = startTime - minStart
offsetPct = offsetMs / totalDurationMs * 100
widthPct  = durationMs / totalDurationMs * 100

画的时候 leftwidth 吃这两个百分比,条太窄时宽度会保一个下限,免得 1ms 的调用缩成一条缝。颜色按 serviceName 轮换,同一服务同一色;异常条改成红色。轴上的刻度是把总时长切成 5 段,用 formatDuration 显示成 ms / s

这里的总时长和上一篇列表里的 durationMs 是同一思路:maxEnd - minStart,不是把子 Span 耗时加总。并行的两条 Feign 并排出现在轴上,而不是首尾相接。

橙边那条:从最晚结束的叶子爬回根

页面提示里有一句:橙边是关键路径,也就是 最晚结束的那条路径 。实现在 computeCriticalPathIds

先统计每个 spanId 有几个孩子,孩子数为 0 的当叶子。在叶子里挑结束时间最晚的那一个,再沿着 parentSpanId 一路爬到根,路上的节点打上 isOnCriticalPath。没有叶子时,退回结束最晚的任意一条再爬。爬的时候用 guard 防环,避免坏数据转圈。

这条路径回答的是:整段墙钟被谁拖到最后。它不一定是耗时最长的单条 Span------一个很短的尾部调用,只要结束得最晚,也会把祖先一起涂上橙边。红条是异常,橙边是时间,两套标记可以叠在同一条上。

时间线看完,慢在哪一段就有数了

详情接口只交扁平 Span,瀑布上的缩进、条的左右位置、关键路径,都是浏览器里 buildTraceTimeline 算出来的。深度跟 parentSpanId 走,偏移和条宽跟最早开始、最晚结束走,橙边从最晚结束的叶子爬回根。

看图时有两处容易误会。树摊平了,多半是跨进程没把 parent 带上,不是 CSS 没缩进;橙边不是自动等于最慢的那一条,它标的是把整段请求拖到结束的那条链。

最后:欢迎围观 Spring Insight

Spring Insight 是给 Spring Boot / Spring Cloud 用的轻量监测工具:业务服务加 spring-insight-agent-starter 上报,旁边跑一个 insight-server,控制台就能看服务拓扑和链路详情。

仓库在这:

🔗 GitHubgithub.com/iweidujiang...

用得上的话欢迎 Star,遇到问题直接开 Issue。

相关推荐
卷无止境1 小时前
从终端里长出来的 IDE,oh-my-pi 到底是个什么东西
人工智能·后端
万物智能1 小时前
PWM散热风扇设置—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
前端·后端·算法
用户8181870627461 小时前
第29章 死信队列与延迟消息实战
java·后端
专业程序开发源1 小时前
springboot旅游推荐系统82074-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·php·课程设计·旅游
步行cgn1 小时前
Spring 注入值中含有特殊符号的处理详解
java·后端·spring
user_admin_god1 小时前
第 11 篇:实践三 —— 表单 / 合同字段抽取
java·人工智能·spring boot·语言模型
未秃头的程序猿1 小时前
全链路追踪落地一年:从翻日志到秒级定位,我们都做了什么
java·后端·面试
回家路上绕了弯1 小时前
ZCode 入门教程:从打开项目到完成一次 Java 代码修改
后端
薛定谔的算法2 小时前
M03:面向对象编程(OOP)
后端