Node.js中Axios调用生成的OpenTelemetry Span无法与父Span关联
OpenTelemetry手动埋点中Axios Span无法关联父Span的问题
环境与依赖
- Node.js v18.16.1
- TypeScript
- 核心依赖:
"@opentelemetry/api": "^1.3.0", "@opentelemetry/auto-instrumentations-node": "^0.38.0", "@opentelemetry/context-async-hooks": "^1.15.0", "@opentelemetry/instrumentation": "^0.41.0", "@opentelemetry/instrumentation-http": "^0.35.1", "@opentelemetry/resources": "^1.9.1", "@opentelemetry/sdk-metrics": "^1.9.1", "@opentelemetry/sdk-node": "^0.35.1", "@opentelemetry/sdk-trace-base": "^1.9.1", "@opentelemetry/sdk-trace-node": "^1.9.1", "@opentelemetry/semantic-conventions": "^1.9.1", "axios": "^1.4.0"
配置代码
otel/tracing.js
const opentelemetry = require('@opentelemetry/api') const { Resource } = require("@opentelemetry/resources"); const { SemanticResourceAttributes } = require("@opentelemetry/semantic-conventions"); const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node"); const { registerInstrumentations } = require("@opentelemetry/instrumentation"); const { ConsoleSpanExporter, SimpleSpanProcessor} = require("@opentelemetry/sdk-trace-base"); const { AsyncHooksContextManager } = require('@opentelemetry/context-async-hooks'); const { HttpInstrumentation, } = require("@opentelemetry/instrumentation-http"); const contextManager = new AsyncHooksContextManager(); contextManager.enable() opentelemetry.context.setGlobalContextManager(contextManager) registerInstrumentations({ instrumentations: [new HttpInstrumentation()], }); const resource = Resource.default().merge( new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'poc-otel-tracing', [SemanticResourceAttributes.SERVICE_VERSION]: "0.1.0", }) ); const provider = new NodeTracerProvider({ resource: resource }); const exporter = new ConsoleSpanExporter(); const processor = new SimpleSpanProcessor(exporter); provider.addSpanProcessor(processor); provider.register(); // process.on('exit', function() { // void exporter.shutdown() // });
src/index.ts
import axios from "axios"; import opentelemetry, {Span} from "@opentelemetry/api"; const tracer = opentelemetry.trace.getTracer('poc-otel-tracing'); const API_BASE_URL = 'https://jsonplaceholder.typicode.com/posts'; const func = async() => { const rootSpan = tracer.startSpan('root-span') await apiCall(rootSpan) rootSpan.end() } const apiCall = async (parentSpan: Span) => { const ctx = opentelemetry.trace.setSpan(opentelemetry.context.active(), parentSpan) const childSpan = tracer.startSpan('api-call', undefined, ctx) try { await axios.get(API_BASE_URL); } catch (e) { childSpan.recordException(e); } childSpan.end() } void func()
运行命令
npm run build && node --require "./otel/tracing.js" dist/index.js
问题现象
控制台输出的Span中,Axios生成的HTTPS GET Span拥有独立的traceId,未与api-call Span形成父子关联:
{ traceId: '15b261f7332feb1fdc79cff5a280529b', parentId: undefined, traceState: undefined, name: 'HTTPS GET', id: '477643c3a9536749', kind: 2, timestamp: 1690206903281000, duration: 128485, attributes: { 'http.url': 'https://jsonplaceholder.typicode.com/posts', 'http.method': 'GET', 'http.target': '/posts', 'net.peer.name': 'jsonplaceholder.typicode.com', 'http.host': 'jsonplaceholder.typicode.com:443', 'net.peer.ip': '172.64.162.6', 'net.peer.port': 443, 'http.status_code': 200, 'http.status_text': 'OK', 'http.flavor': '1.1', 'net.transport': 'ip_tcp' }, status: { code: 0 }, events: [], links: [] } { traceId: 'ba35dbc454728c2bbc5b30a435dec3bb', parentId: 'a3897743fb82276c', traceState: undefined, name: 'api-call', id: 'ec3d00cb4ad95d26', kind: 0, timestamp: 1690206903278000, duration: 136158, attributes: {}, status: { code: 0 }, events: [], links: [] } { traceId: 'ba35dbc454728c2bbc5b30a435dec3bb', parentId: undefined, traceState: undefined, name: 'root-span', id: 'a3897743fb82276c', kind: 0, timestamp: 1690206903278000, duration: 136633, attributes: {}, status: { code: 0 }, events: [], links: [] }
预期结果
HTTPS GET Span应继承api-call Span的上下文,属于同一Trace,且parentId指向api-call的Span ID:
{ traceId: 'ba35dbc454728c2bbc5b30a435dec3bb', parentId: 'ec3d00cb4ad95d26', traceState: undefined, name: 'HTTPS GET', id: '477643c3a9536749', kind: 2, timestamp: 1690206903281000, duration: 128485, attributes: { 'http.url': 'https://jsonplaceholder.typicode.com/posts', 'http.method': 'GET', 'http.target': '/posts', 'net.peer.name': 'jsonplaceholder.typicode.com', 'http.host': 'jsonplaceholder.typicode.com:443', 'net.peer.ip': '172.64.162.6', 'net.peer.port': 443, 'http.status_code': 200, 'http.status_text': 'OK', 'http.flavor': '1.1', 'net.transport': 'ip_tcp' }, status: { code: 0 }, events: [], links: [] } { traceId: 'ba35dbc454728c2bbc5b30a435dec3bb', parentId: 'a3897743fb82276c', traceState: undefined, name: 'api-call', id: 'ec3d00cb4ad95d26', kind: 0, timestamp: 1690206903278000, duration: 136158, attributes: {}, status: { code: 0 }, events: [], links: [] } { traceId: 'ba35dbc454728c2bbc5b30a435dec3bb', parentId: undefined, traceState: undefined, name: 'root-span', id: 'a3897743fb82276c', kind: 0, timestamp: 1690206903278000, duration: 136633, attributes: {}, status: { code: 0 }, events: [], links: [] }
问题原因与解决方法
原因
代码中仅创建了childSpan,但未将其上下文绑定到Axios调用的异步执行流中。OpenTelemetry的AsyncHooksContextManager需要显式将Span上下文关联到当前操作,否则HTTP instrumentation无法识别当前Span,会生成独立的Trace。
解决方法
有两种常用方式将Span上下文绑定到异步操作:
方法1:使用tracer.startActiveSpan(推荐)
startActiveSpan会自动将创建的Span设置为当前上下文的活跃Span,无需手动绑定:
const apiCall = async (parentSpan: Span) => { const ctx = opentelemetry.trace.setSpan(opentelemetry.context.active(), parentSpan); await tracer.startActiveSpan('api-call', {}, ctx, async (childSpan) => { try { await axios.get(API_BASE_URL); } catch (e) { childSpan.recordException(e); } finally { childSpan.end(); } }); }
方法2:使用opentelemetry.context.with
手动将Span上下文绑定到Axios调用的执行上下文:
const apiCall = async (parentSpan: Span) => { const parentCtx = opentelemetry.trace.setSpan(opentelemetry.context.active(), parentSpan); const childSpan = tracer.startSpan('api-call', undefined, parentCtx); // 创建包含childSpan的上下文 const childCtx = opentelemetry.trace.setSpan(parentCtx, childSpan); try { // 在childCtx的上下文中执行Axios调用 await opentelemetry.context.with(childCtx, async () => { await axios.get(API_BASE_URL); }); } catch (e) { childSpan.recordException(e); } finally { childSpan.end(); } }
修改后重新运行,Axios生成的HTTPS GET Span会正确关联api-call Span,属于同一Trace。
内容的提问来源于stack exchange,提问作者Liviu Gheorghe
相关产品推荐
相关产品推荐

