You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 06:37:02