概念定义
span 链接是从一个 span 指向另一个有因果关系但并非其父 span 的引用。父子边刻画嵌套的同步调用;链接刻画多对多或延迟的关系——一个批量消费者的单个 span 由上百个生产者 span 引起,或稍后被先前请求触发的后续任务。链接让链路图能表达这些连接,而不必强加错误的层级。
为什么重要
当因果关系被强行塞进并不契合的树里,链路就会误导人:批量消费者看起来只属于某一个任选的生产者,其余九十九个则不可见。链接保留了真实的多对多关系,使链路检索能跨越异步边界回答“是什么导致了这项任务”或“这次请求最终触发了什么”。
工程实践
在批量、延迟或重试而非嵌套执行之处添加链接,并在链接上携带消息 id、trace id 等标识,使其在传输中得以保留。直接的同步调用仍用父子边,让树保持可读。队列的生产者与消费者要一并埋点,并确保重新入队的流程重新附上原有链接,而不是开启一条因果关系被割裂的新链路。
代码示例
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
const sdk = new NodeSDK({
serviceName: 'orders-service',
traceExporter: new OTLPTraceExporter({
url: process.env.OTLP_ENDPOINT ?? 'http://collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
await sdk.start();
const tracer = trace.getTracer('orders', '1.0.0');
async function processOrder(orderId: string): Promise<void> {
return tracer.startActiveSpan('order.process', async (span) => {
span.setAttribute('order.id', orderId);
try {
await tracer.startActiveSpan('order.validate', async (child) => {
await new Promise((r) => setTimeout(r, 10));
child.setAttribute('validation.rules', 12);
child.end();
});
await tracer.startActiveSpan('order.charge', async (child) => {
await new Promise((r) => setTimeout(r, 25));
child.recordException(new Error('card declined simulated'));
child.setStatus({ code: SpanStatusCode.ERROR });
child.end();
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
} finally {
span.end();
}
});
}
await processOrder('ord-7');
await sdk.shutdown();
const HEX = { trace: 32, span: 16 };
export function parseTraceparent(value) {
if (typeof value !== 'string') throw new TypeError('traceparent must be a string');
const parts = value.trim().split('-');
if (parts.length !== 4) throw new SyntaxError('traceparent must have 4 fields');
const [version, traceId, spanId, flags] = parts;
if (!/^[0-9a-f]{2}$/.test(version)) throw new SyntaxError('invalid version: ' + version);
if (version === 'ff') throw new SyntaxError('version ff is invalid');
if (!new RegExp('^[0-9a-f]{' + HEX.trace + '}$').test(traceId)) {
throw new SyntaxError('invalid trace id: ' + traceId);
}
if (traceId === '0'.repeat(HEX.trace)) throw new SyntaxError('trace id must not be all zeros');
if (!new RegExp('^[0-9a-f]{' + HEX.span + '}$').test(spanId)) {
throw new SyntaxError('invalid span id: ' + spanId);
}
if (spanId === '0'.repeat(HEX.span)) throw new SyntaxError('span id must not be all zeros');
if (!/^[0-9a-f]{2}$/.test(flags)) throw new SyntaxError('invalid flags: ' + flags);
return { version, traceId, spanId, sampled: (parseInt(flags, 16) & 1) === 1 };
}