c# ActivitySource 和 OpenTelemetry 在异步代码中的上下文传递

来源:这里教程网 时间:2026-02-21 17:42:47 作者:

ActivitySource 创建的 Activity 在 async/await 中为什么会丢失?

因为

Activity
依赖
AsyncLocal<activity></activity>
实现上下文传递,而 .NET 的异步执行流(如
Task
ValueTask
)默认会捕获并还原
AsyncLocal
值——但前提是
Activity
必须在进入异步边界前被显式启动并设置为当前 Activity。如果只调用
StartActivity()
但没调用
SetParentId()
或没正确处理父级上下文,后续
await
后的代码里
Activity.Current
就是
null

常见错误:在
async Task
方法里直接
var activity = source.StartActivity("work")
,然后立刻
await
,之后再想用
Activity.Current
记录日志或指标 → 此时已为
null
根本原因:OpenTelemetry SDK 默认不自动将新
Activity
设为当前上下文;必须显式调用
activity?.Start(); activity?.SetParentId(...)
,或更稳妥地用
using var activity = source.StartActivity(...)
+ 确保其生命周期覆盖整个异步作用域
注意
ActivitySource.StartActivity()
返回的是未启动的
Activity
,需手动
.Start()
才真正进入上下文栈

如何让 OpenTelemetry 正确注入和提取 W3C TraceContext?

异步调用跨服务(如 HTTP 调用)时,Trace ID 和 Span ID 必须通过 HTTP Header(

traceparent
/
tracestate
)传播。OpenTelemetry .NET SDK 默认启用 W3C 格式,但前提是
ActivitySource
创建的
Activity
已正确启动且设为当前上下文,否则
HttpClient
HttpMessageHandler
集成无法读取
Activity.Current
并注入 Header。

确保
Activity
在发起 HTTP 请求前已启动并设为当前:例如
using var activity = source.StartActivity("http-out"); activity?.Start();
检查是否注册了
AddHttpClientInstrumentation()
:它依赖
DiagnosticSource
监听
System.Net.Http
事件,若未启用则不会自动注入/提取 trace context
手动注入场景(如自定义 HTTP 客户端):用
OpenTelemetry.Context.Propagation.HttpTraceContext.Inject(...)
,传入
Activity.Current?.Context
,否则注入空 context

为什么 await 之后 Activity.Current 是 null,但 SpanBuilder 仍能生成子 Span?

因为 OpenTelemetry 的

Tracer
(如
Sdk.CreateTracerProviderBuilder()
构建的)在创建
ISpan
时,会尝试从
Activity.Current
获取父级上下文;但如果
Activity.Current == null
,它会 fallback 到“无父级”的 root span —— 这看起来像“还能工作”,实则是丢失了调用链,所有 Span 都变成孤立根节点。

典型现象:Jaeger 或 Zipkin 中看到一堆同名、同时间戳、无父子关系的
http-out
Span
验证方式:在
await
后加
Console.WriteLine(Activity.Current?.Id)
,输出
null
即确认上下文断裂
修复关键:不要依赖“Span 自动找父级”,而要确保
Activity
生命周期贯穿整个 async 方法体,推荐用
using
块包裹
StartActivity()
调用,并在
await
前完成
.Start()
using var activity = MyActivitySource.StartActivity("process-item");
activity?.Start(); // 必须调用!否则 Activity.Current 不生效
await DoWorkAsync(); // 此处 Activity.Current 仍有效
// 后续操作可安全访问
activity?.AddTag("processed", true);
activity?.SetStatus(Status.Ok);

AsyncLocal 和 Activity.Current 的行为差异容易被忽略

Activity.Current
AsyncLocal<activity></activity>
的封装属性,但它只在
Activity
.Start()
后才写入
AsyncLocal
。而
ActivitySource.StartActivity()
返回的对象默认是
IsAllDataRequested == false
且未启动,此时即使赋值给局部变量,也不会影响
Activity.Current

错误写法:
var activity = source.StartActivity("x"); await Task.Delay(1); activity?.Start();
await
期间
Activity.Current
为空
正确顺序:先
.Start()
,再
await
,且
activity
对象生命周期必须跨越
await
特别注意
ValueTask
:它可能同步完成,也可能异步,但
AsyncLocal
行为一致;不能假设“同步完成就不用管上下文”
跨 async 方法传递 Activity 最可靠的方式不是靠
Activity.Current
自动延续,而是显式传参 + 在每个 async 方法入口重新绑定:把
Activity.Context
作为参数传入,用
Activity.SetParentId()
恢复上下文。这听起来繁琐,但在复杂调度(如
Task.Run
、线程池回调、Timer 回调)中是唯一可控手段。

相关推荐