Spring Cloud Sleuth 如何导出客户环境链路数据至本地Zipkin
基于自定义Sender实现链路数据落盘持久化
核心思路是替换Spring Cloud Sleuth默认的zipkinSender Bean实现,完全跳过客户环境到Zipkin服务的网络上报动作,将原本要序列化上报的Span数据直接持久化到本地文件,后续离线导入本地Zipkin即可完成链路排查,整个过程不需要改动任何业务层的@NewSpan注解逻辑,也不需要在客户环境部署额外组件。
前置适配说明
Hoxton.SR10版本的Sleuth链路上报逻辑完全基于注入的zipkinSender Bean实现,只要替换这个Bean的逻辑,即可无缝接管所有链路数据的出口,同时保留原有的Span采集、上下文传播能力,和本地开发环境的采集逻辑保持一致。
通过@ConditionalOnProperty做环境隔离:客户环境开启自定义落盘逻辑,本地开发环境保持原有的网络上报到Zipkin的逻辑,不影响日常开发体验。
完整配置实现
补全原方案中缺失的两个核心方法,完整可运行的配置类如下:
@Configuration @ConditionalOnProperty(name = "custom.sleuth.file-sender.enabled", havingValue = "true") public class SleuthFileSenderConfiguration { // 可通过配置文件注入自定义的文件存储目录 @Value("${custom.sleuth.file-sender.save-path:/data/sleuth/spans}") private String savePath; // 单文件最大大小,默认100MB @Value("${custom.sleuth.file-sender.max-file-size:104857600}") private long maxFileSize; @Bean("zipkinSender") public Sender zipkinFileSender() { return new Sender() { @Override public Encoding encoding() { // 保持和默认Sender一致的JSON编码格式,保证Span结构和Zipkin要求完全兼容 return Encoding.JSON; } @Override public int messageMaxBytes() { // 无限制批量发送,减少文件IO次数 return Integer.MAX_VALUE; } @Override public int messageSizeInBytes(List<byte[]> list) { // 无限制批量大小 return Integer.MAX_VALUE; } @Override public Call<Void> sendSpans(List<byte[]> spans) { // 1. 转换为Zipkin可直接导入的JSON数组格式 String spanJsonContent = convertByteArrayToZipkinFormat(spans); // 2. 持久化到本地文件 saveToLocalFile(spanJsonContent); // 3. 返回成功的回调,避免Sleuth触发重试逻辑 return new Call.Base<>() { @Override protected Void doExecute() { return null; } @Override protected void doEnqueue(Callback<Void> callback) { callback.onSuccess(this, null); } }; } }; } /** * 将批量Span的字节数组转换为Zipkin兼容的JSON数组格式 */ private String convertByteArrayToZipkinFormat(List<byte[]> spans) { StringBuilder jsonBuilder = new StringBuilder("["); for (int i = 0; i < spans.size(); i++) { String spanStr = new String(spans.get(i), StandardCharsets.UTF_8); jsonBuilder.append(spanStr); // 非最后一个元素追加逗号分隔 if (i != spans.size() -1) { jsonBuilder.append(","); } } jsonBuilder.append("]"); return jsonBuilder.toString(); } /** * 将Span JSON内容持久化到本地文件,支持滚动切割避免单文件过大 */ private void saveToLocalFile(String content) { // 确保存储目录存在 File targetDir = new File(savePath); if (!targetDir.exists()) { targetDir.mkdirs(); } // 查找当前可写入的文件,若超过最大大小则生成新文件 File currentFile = getCurrentWritableFile(targetDir); // 追加模式写入,加锁保证并发写入不混乱 synchronized (this) { try (FileWriter writer = new FileWriter(currentFile, true)) { writer.write(content); writer.flush(); } catch (IOException e) { // 写入失败只打日志,不抛出异常影响业务流程 e.printStackTrace(); } } } /** * 获取当前可写入的文件,实现按大小滚动 */ private File getCurrentWritableFile(File dir) { // 文件名格式:span-时间戳-序号.json String fileNamePrefix = "span-" + LocalDate.now().toString(); File[] existFiles = dir.listFiles((f, name) -> name.startsWith(fileNamePrefix) && name.endsWith(".json")); // 无当日文件则新建 if (existFiles == null || existFiles.length == 0) { return new File(dir, fileNamePrefix + "-001.json"); } // 找最后一个文件判断大小,没超过阈值则继续写入 File lastFile = Arrays.stream(existFiles).max(Comparator.comparingLong(File::length)).get(); if (lastFile.length() < maxFileSize) { return lastFile; } // 超过阈值则生成新序号的文件 int nextIndex = existFiles.length +1; return new File(dir, fileNamePrefix + "-" + String.format("%03d", nextIndex) + ".json"); } }
离线导入使用流程
- 问题排查时,直接从客户环境的存储目录(默认
/data/sleuth/spans/)拷贝所有生成的JSON文件到本地 - 本地启动Zipkin服务后,调用Zipkin的
/api/v2/spans接口,将每个文件的完整内容作为POST请求的Body传入,Zipkin会自动完成Span的解析、链路拼接 - 打开本地Zipkin的Web UI即可查看完整的链路信息,和本地开发环境的使用体验完全一致
注意事项
- 该实现没有额外依赖,原项目只要已经引入
spring-cloud-starter-zipkin,所有需要的Sender、Encoding、Call类都已经在现有依赖中,不需要额外引入第三方库 - 文件写入是追加模式,且通过异步线程触发,不会阻塞业务主流程的响应时间
- 可根据客户环境的磁盘容量,调整文件保存路径、单文件大小、旧文件自动清理策略,避免长期运行占用过多磁盘空间
- 不需要修改任何业务代码,原有的
@NewSpan注解的自定义Span逻辑会被完整采集并落盘
内容的提问来源于stack exchange,提问作者Rokko_11
相关产品推荐
相关产品推荐

