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