如何获取OpenTelemetry指定traceId关联的所有子Span
核心结论
OpenTelemetry 官方核心API本身不提供直接通过traceId查询关联全量子Span的内置方法。所有Span默认在结束上报后就会被SDK释放,不会在进程内存中留存关联关系,需要通过自定义组件实现采集存储,才能满足同Trace下Span查询的需求。
可落地方案
自定义SpanProcessor 进程内捕获同Trace全量Span
这是最适配服务端渲染页面性能条场景的方案,不需要依赖外部存储,直接在服务进程内完成Span收集,延迟最低。
- 实现逻辑:自定义Span处理器,在Span启动/结束时按
traceId分组将Span存入临时内存存储,等当前请求所有逻辑执行完成后,即可读取对应traceId下的全量Span列表 - 处理器实现代码:
const { SpanProcessor } = require('@opentelemetry/sdk-trace-base'); // 按traceId存储关联Span列表,配套过期机制避免内存泄漏 const traceSpanMap = new Map(); // Trace数据默认过期时间,避免无效数据长期占用内存 const TRACE_DEFAULT_EXPIRE_MS = 10 * 60 * 1000; class LocalTraceCollectProcessor extends SpanProcessor { onStart(span, parentContext) { const traceId = span.spanContext().traceId; if (!traceSpanMap.has(traceId)) { traceSpanMap.set(traceId, { spans: [], expireAt: Date.now() + TRACE_DEFAULT_EXPIRE_MS }); } traceSpanMap.get(traceId).spans.push(span); } onEnd(span) { const traceId = span.spanContext().traceId; if (traceSpanMap.has(traceId)) { // 每次有Span结束就延后过期时间,保证请求处理完成后数据留存足够读取时间 traceSpanMap.get(traceId).expireAt = Date.now() + 30 * 1000; } } shutdown() {} forceFlush() { return Promise.resolve(); } } // 定时清理过期Trace数据 setInterval(() => { const now = Date.now(); for (const [traceId, data] of traceSpanMap.entries()) { if (data.expireAt < now) traceSpanMap.delete(traceId); } }, 60 * 1000).unref(); // 对外暴露查询方法 function getSpansByTraceId(traceId) { return traceSpanMap.get(traceId)?.spans || []; }
- 注册方式:在
tracing.js初始化TracerProvider时,将自定义Processor加入处理器列表即可:
const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base'); const provider = new BasicTracerProvider(); // 保留原有上报到Collector的BatchSpanProcessor等配置 provider.addSpanProcessor(new LocalTraceCollectProcessor());
查询后端Trace存储
如果Span已经统一上报到OpenTelemetry Collector、Jaeger、Zipkin等可观测后端,可以直接调用后端提供的Trace查询接口,传入traceId拉取全量Span。但这种方案会产生额外网络请求,不适合当前请求响应周期内拼接性能条的低延迟场景。
相关问题解答
- 如何遍历Span构成的调用DAG?
拿到全量Span列表后,根据每个Span的parentSpanId字段构建父子ID映射表,从根Span(parentSpanId为空的入口Span)开始做深度/广度优先遍历,即可还原完整调用链层级。 - 为什么拿到根Span也无法直接获取子Span?
OpenTelemetry的Span设计为上下文向下透传模式,子Span创建时只会持有父Span的上下文引用,父Span本身不会维护子Span的引用列表,因此原生不支持向下遍历。 - 可以只监听指定traceId的Span吗?
可以在自定义SpanProcessor的onStart逻辑中增加过滤判断,仅将目标traceId对应的Span存入内存,其他Trace的Span直接跳过,可大幅降低内存占用。
Express场景适配示例
在路由逻辑中等待所有业务操作、外部请求执行完成后,即可读取全量Span,过滤出慢调用数据渲染到性能条:
require('./tracing.js'); const opentelemetryApi = require('@opentelemetry/api'); const express = require('express'); const app = express(); app.get('/test', async (req, res) => { // 外部HTTP请求会被HTTP插桩自动生成子Span await fetch('https://matrix-client.matrix.org/_matrix/client/versions'); const activeCtx = opentelemetryApi.context.active(); const span = opentelemetryApi.trace.getSpan(activeCtx); const traceId = span.spanContext().traceId; // 获取当前Trace下所有Span const allSpans = getSpansByTraceId(traceId); // 过滤需要展示的慢请求(示例:筛选耗时超过100ms的Span) const slowCallSpans = allSpans.filter(s => s.duration > 100); res.send(`<section><h1>Performance bar</h1> <p>Trace ID: ${traceId}, 慢请求数: ${slowCallSpans.length}</p></section> other page HTML`); }); app.listen(3000);
注意:读取Span属性时要等所有子操作执行完成、对应Span触发
onEnd回调,否则可能拿不到完整的耗时、状态等最终数据。
内容的提问来源于stack exchange,提问作者MLM
相关产品推荐
相关产品推荐

