activity 在 async/await 中丢失,因 startactivity() 返回未启动 activity,须显式调用 .start() 并用 using 确保生命周期覆盖整个异步作用域,否则 activity.current 为 null。

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-outSpan - 验证方式:在
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行为一致;不能假设“同步完成就不用管上下文”
Activity.Current 自动延续,而是显式传参 + 在每个 async 方法入口重新绑定:把 Activity.Context 作为参数传入,用 Activity.SetParentId() 恢复上下文。这听起来繁琐,但在复杂调度(如 Task.Run、线程池回调、Timer 回调)中是唯一可控手段。








