Spring Insight 里如何对 Span 进行清洗

Spring Insight 里如何对 Span 进行清洗

作者:苏渡苇

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

本文要讲什么

上一篇把存储讲完了:saveTraceSpans 负责 snapshot、淘汰、可选刷盘,控制台查的也是那份内存列表。

数据并不是 POST 进来就往列表里扔,Controller 先交给 TraceSpanCollectorService,校验过了再清洗、补全,最后才调用存储。

Agent 这边字段有时会空着------服务名只写在批次信封上、耗时没算、statusCode 没填------直接入库的话,后面按服务聚合、按状态筛选都会对不上。

这篇要讲的就是门口这一步:一批 Span 怎样被判定有效,缺的字段怎样补上,以及为什么整批失败和单条跳过不是一回事。

一、先把问题说清楚

上报体对应的是 CollectorRequest,和 Agent 里的 TraceBatchReport 字段对齐:serviceNameserviceInstancebatchIdspans

HttpInsightBatchSink 组包时,服务名写在批次这一层,每条 Span 里也可能再带一份;Server 不能假定两条都有,更不能假定 durationMsspanKindstatusCode 都填好了。

所以收集服务要干三件事:这批能不能收、每条缺什么补什么、补完再交给上一篇那个存储。

代码在此:io.github.iweidujiang.springinsight.collector.service.TraceSpanCollectorService

二、整批先过一道门

processBatchRequest 一进来就问 request.isValid(),不通过就返回错误,Controller 映射成 400,列表里不会出现这批数据。

isValid() 看的是硬条件:spans 不能空,每一条都得有 traceIdspanId,缺一个整批退回。

java 复制代码
public boolean isValid() {
    if (spans == null || spans.isEmpty()) {
        return false;
    }
    for (int i = 0; i < spans.size(); i++) {
        TraceSpan span = spans.get(i);
        if (span.getTraceId() == null || span.getTraceId().trim().isEmpty()) {
            return false;
        }
        if (span.getSpanId() == null || span.getSpanId().trim().isEmpty()) {
            return false;
        }
    }
    return true;
}

这和存储里 缺 ID 就 skip看起来像重复,层级却不一样:收集服务面对的是一次 HTTP 批次,宁可整批发回来,也不让半残数据进清洗;存储那层是入库兜底,防止还有别的入口把脏对象塞进来。

Controller 上还有 @ValidserviceNameserviceInstance@NotBlankspans@NotNull,框架校验比 isValid() 更早,信封都缺的请求到不了收集服务。

过了这道门才会去改计数器、开 StopWatch、进入清洗,成功则 202,处理异常则 500。batchId 只进日志摘要,Server 目前没有按它去重,同一批重发一次就会再存一遍。

三、缺的字段就地补上

清洗在 cleanAndEnrichSpans 里,按条处理:Span 上没有服务名或实例名,就用批次信封上的顶上,然后再走 cleanAndEnrichSingleSpan

单条补全大致是这几件事:

spanKind 空着就写成 INTERNAL HTTP 进站一般是 SERVER,Feign 出站是 CLIENT,补不上的先给个内部类型,后面按 kind 筛选时至少不是 null

statusCode 空着就猜。 先看 tags 里的 http.status_code,2xx 写成 OK;没有 HTTP 码再看 success,为 true 也写成 OK;两条都没有,落成 ERROR。字段不全会被标成失败,这是当前实现,不是业务真的抛了异常。

durationMs 空着就减一下。 同时有 startTimeendTime 时,用结束减开始填进去,拓扑平均耗时、链路列表的 duration 都靠这个数。

tags 保证不是 null,再挂两个收集器自己的标签: collector.process.timecollector.version。后者现在写死成 1.0,跟项目版本号不是一回事,当处理痕迹看就行。

java 复制代码
if (span.getDurationMs() == null && span.getStartTime() != null && span.getEndTime() != null) {
    span.setDurationMs(span.getEndTime() - span.getStartTime());
}
if (span.getTags() == null) {
    span.setTags(new HashMap<>());
}
span.getTags().put("collector.process.time", Instant.now().toString());
span.getTags().put("collector.version", "1.0");

这些改动打在请求体里的原对象上,上一篇说的 snapshot() 发生在这之后,所以内存里留下的是补全后的拷贝,不是 Agent 刚发过来的那份引用。

单条清洗若抛异常,这条跳过、其余继续,和 isValid() 的整批失败是两条路:ID 这种硬字段整批拦,补全时的意外只丢掉出事的那一条。

四、和「入库时再判断」的区别

可以对照着看:

收集服务 存储
看到的是 一次 HTTP 批次 已经洗过的列表
traceId / spanId 整批 400 单条 skip
缺服务名、耗时、状态 补上再往下走 不再补,按现有字段存
失败时 可以整批拒绝 只决定进不进列表

收集放在存储前面,是因为 这批像不像监测数据列表怎么淘汰 」不是同一个问题。补字段、打 collector.* 标签、记成功失败条数,都不该塞进带锁的 ArrayList 里去做。

存储只认补完的结果:有 ID 就 snapshot 进去,超上限就挤旧的。两边各管一段,后面换文件还是换库,清洗规则不用跟着搬。

五、自己写收集入口时可以用到的

  1. 信封和明细分开补。 批次上有服务名,Span 上也可能有;明细空了用信封,明细有了别覆盖,跨服务一批里混着不同 serviceName 时才不会全被改成上报进程的名字。
  2. 硬校验和软补全分开。 没有 traceId 的批次没有清洗的意义;耗时没算、状态没填,可以猜,不该把整批打回。
  3. 猜状态要有默认值,也要知道默认值很凶。 现在缺 statusCode、又没有 HTTP 码、success 也不是 true,就会落成 ERROR,控制台错误分析会被这些「空字段」抬高。
  4. 先改原对象,入库再拷贝。 补全和 snapshot 分两步,收集侧不用自己 new 一份,存储侧也不用知道字段是谁填的。
  5. 计数器和写入分开。 AtomicLong 记收到、成功、失败,和列表内容不是同一件事;列表被挤掉旧数据之后,计数不会回滚。

六、小结

  • POST 进来之后先过 CollectorRequest.isValid()spans 为空或任何一条缺 traceId / spanId,整批不会入库。
  • 过门之后 cleanAndEnrichSpans 用批次上的服务名补空字段,再补 spanKindstatusCodedurationMs 和 tags。
  • 单条清洗出错只跳过这一条;存储里缺 ID 再 skip 一次,属于入库兜底。
  • 收集服务改的是请求里的原对象,snapshot() 在这之后,控制台看到的是补全后的拷贝。

数据放下之后,控制台还得把「谁调用了谁」画成图,边不是前端编的,是 Span 上的 remoteService 聚出来的。

下一篇预告:看拓扑的边是怎么从一批 CLIENT Span 里抠出来的,为什么只有 Feign 出站这类带远端服务名的记录才会变成箭头。

最后:欢迎围观 Spring Insight

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

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

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

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

相关推荐
find1star1 小时前
LeetCode 54:螺旋矩阵——用四个边界模拟矩阵收缩
java·算法·leetcode·边缘计算·学习方法
Yeniden1 小时前
Java 后端从零到企业级:第1篇 Java 基础语法——变量、数据类型与运算符
java·开发语言·apache
邪修king1 小时前
Linux系统篇(二十三) 基础 IO:从“文件”到“文件描述符”,彻底理解重定向
android·java·linux
WeiXin_DZbishe1 小时前
基于springboot大学生提问箱系统-计算机毕设【课程设计】72593
javascript·vue.js·spring boot·vscode·python·node.js·php
云雀衔光1 小时前
MCP + 应用生成:让 AI 直接产出可交互的应用
java·人工智能·测试工具·microsoft·交互·ai编程
Moment1 小时前
为什么市面上的 coding agent 大多数都基于Nodejs?
前端·后端·面试
Nuanyt2 小时前
JVM常见核心知识梳理02 类加载 字节码技术 双亲委派 Java内存模型 JMM 并发底层 volatile synchronized 常见排障与调优工具
java·开发语言·jvm
Shan12052 小时前
经典算法题示例与详解:飞地的数量(二)
java·数据结构·算法
一嘴一个橘子2 小时前
java - redis 缓存雪崩
java