Spring Insight 里如何把 Span 收成一条链路

Spring Insight 里如何把 Span 收成一条链路

作者:苏渡苇

项目地址https://github.com/iweidujiang/spring-insight(感谢 Star !)

一次下单接口,在链路追踪只占列表一行?

上一篇把拓扑讲完了:图上的箭头来自 remoteService,按 服务对 归边。

链路页要回答的是另一件事------一次请求从头到尾经过了哪些调用

如果把内存里的 Span 直接摊成表格,同一条 Trace 会占很多行,入口、总耗时、成没成功都看不清。

控制台现在的做法是:同一 traceId 收成一行摘要,点进去才展开那些 Span。

这篇要讲的就是这行摘要:根服务、入口操作、总耗时、是否有错误,是怎么从扁平记录里抠出来的。

列表要的是摘要,不是把 Span 全铺出来

一次跨服务调用会留下多条 Span:Gateway 进站、Gateway 出站、order 进站、order 调 user、order 调 product......它们共享同一个 traceIdparentSpanId 把树串起来。

列表如果按 Span 展示,刷新一次能刷出几十上百行,其中很多是同一次下单。打开页面的人想找的通常是:刚才那次请求慢不慢、有没有 ERROR、入口是哪个服务。

所以列表接口返回的不是 Span 数组,而是 Trace 摘要:

字段 含义
traceId 这一次请求的 ID
serviceName 根 Span 所在服务,当作入口
operationName 根 Span 的操作名
startTime 这棵树里最早的开始时间
durationMs 最晚结束减去最早开始,整段墙钟时间
spanCount / serviceCount 这条 Trace 里有多少条 Span、经过几个服务
hasError 其中任意一条失败,整行就算 ERROR

聚合代码在此:TraceSpanPersistenceService.getRecentTraceSummaries

查询接口:GET /api/v1/ui/traces/recent

进站 SERVER、JDBC 只要挂了同一个 traceId,都会进 spanCount,并不要求带 remoteService。拓扑那张图会丢掉没有远端名的记录,列表这边不会。

用 record 把同一条 Trace 累在一起

扫描仍在那把锁里做,按 traceId 放进 LinkedHashMap,每条 Trace 一个累加器:

java 复制代码
record Acc(
        String traceId,
        long minStart,
        long maxEnd,
        int spanCount,
        boolean hasError,
        String rootService,
        String rootOperation,
        LinkedHashSet<String> services
) {}

record 是 Java 16 正式引入的一种紧凑型类声明 ,专为纯数据载体设计,可参考我之前的这篇介绍:record + sealed class = Java 的"类型乐高"!

这里用它,是因为 Acc 只扛一组数字和字符串,不需要再手写 getter、equalshashCode。取字段写成 acc.traceId()acc.minStart(),不像普通 JavaBean 那样 getXxx()。Insight 要求 JDK 21,这类写法可以直接用。

碰到一条 Span,先用时间窗口切掉过旧的,再更新这几个量:

  • minStart / maxEnd:整段链路的起止,缺 endTime 时用 startTime + durationMs 顶上
  • spanCount:条数加一
  • services:出现过的 serviceName 去重
  • hasError:任意一条 statusCode 为 ERROR,或 success == false,整行打上错误
  • 根节点:parentSpanId 为空的那条,记下它的服务名和操作名,当作入口
java 复制代码
boolean root = s.getParentSpanId() == null || s.getParentSpanId().isBlank();

总耗时不是把每条 Span 的 durationMs 加起来------子调用是嵌套、并行都有,加总会虚高。这里用 maxEnd - minStart,表示这次请求从最早埋点到最晚结束跨了多久。

扫完之后再过滤、排序:按 startTime 倒序,截到 limit。根服务如果始终没找到(整棵树都带了 parent),就退回这条 Trace 里出现过的第一个服务名,避免列表上入口是空白。

筛选项对着接口参数来

接口参数和页面上的筛选项是对齐的:

text 复制代码
GET /api/v1/ui/traces/recent
    ?hours=24
    &limit=100
    &service=sca-order
    &status=error
    &q=create
    &minDurationMs=500

service 看的是这条 Trace 有没有经过 该服务,不是只匹配入口。

order 调了 user,按 sca-user 筛,这次下单仍会留下。status=error|ok 看的是整行的 hasError

关键字会拿 Trace ID、入口操作、入口服务、以及路过的服务名做忽略大小写包含。

minDurationMs 用的是上面算出来的墙钟耗时,仪表盘跳慢请求也走这个参数。

这些条件都在内存里聚完后过滤,没有单独的索引。前端把当前筛选写进地址栏,复制 URL 能打开同一组条件;点某一行再请求 GET /api/v1/ui/traces/{traceId},那才是这条 Trace 下的 Span 明细,瀑布图怎么画放在下一篇。

收成一行之后,还要看清哪一段慢

链路列表把同一 traceId 收成一行:入口来自没有父节点的根 Span,总耗时是整段墙钟,任意一条失败整行就是 ERROR。接口是 GET /api/v1/ui/traces/recent,筛选在聚完之后做。

看列表时有两处容易对不上预期。总耗时不是子 Span 耗时之和,并行调用加起来会比墙钟更夸张;按服务筛选是 经过该服务,不是只显示以它为入口的请求。根找不到时会拿出现过的第一个服务名顶上,跨进程如果 parent 没带上,入口可能不准。

点进某一行之后,还要把这些 Span 按开始时间和父子关系排成瀑布,才能看出哪一段把时间吃掉了。

下集预告:详情页的时间线怎么把扁平 Span 画成调用树,深度、偏移和条宽分别从哪几个字段来。

最后:欢迎围观 Spring Insight

如果你对轻量监测、Spring Boot Starter、链路埋点感兴趣,欢迎看看这个还在打磨的小项目:

🔗 GitHubhttps://github.com/iweidujiang/spring-insight

当前形态很朴素:业务服务加 spring-insight-agent-starter,旁边单独跑 insight-server 看拓扑和链路。能力有限,代码也还糙,适合当练手和对照。

你的 Star 是对我最大的鼓励。

相关推荐
熊猫猫猫猫猫猫2 小时前
Lambda Fusion:面向企业级应用与 AI 智能体开发的开源微服务框架
spring boot
周先生FullStack2 小时前
Mac + 容器本地 MySQL/Redis 环境搭建与网络原理实战
java·spring boot·微服务·容器·架构·个人开发
边境悍匪2 小时前
蜗牛学苑 Java 智能体学习 Day44|Vue 前端项目搭建 思维导图复盘
java·开发语言·spring boot·学习·阿里云
摇滚侠3 小时前
《Spring Boot 3:高级与架构设计》第 1 章 元编程与元信息 个人理解 1
java·spring boot·笔记·后端
RuoyiOffice3 小时前
SpringBoot3+Vue3 流程抄送已读:打开详情消红点,待办与知会怎么分开
spring boot·vue3·flowable·spring boot 3·ruoyi office·流程抄送·已读回执
程序猿乐锅4 小时前
【黑马点评 | 第四篇】Redis缓存雪崩
java·数据库·spring boot·redis·缓存
小尹哥-程序员4 小时前
第4集:让AI学会看文档:Spring AI RAG入门实战
java·人工智能·spring
张小凡vip4 小时前
Java Web 设置 Session 超时时间:如何让 Session 永不失效
java·spring boot