如何让HttpClient共用同一TraceId,将10个请求合并为单Trace多Span
我实现了一个IHostedService并通过定时器执行任务,代码如下:
_timer = new Timer(RunAsync, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
在RunAsync方法中,我通过依赖注入创建Service实例,循环10次每次创建作用域获取Service并调用ExecuteAsync:
public async void RunAsync(object? state) { for (int i = 0; i < 10; i++) { using var scope = services.CreateScope(); var service = scope.ServiceProvider.GetRequiredService<Service>(); await service.ExecuteAsync(); } }
Service的ExecuteAsync方法中使用HttpClient发起Get请求:
public class Service { private readonly HttpClient httpClient; public Service(HttpClient httpClient) { this.httpClient = httpClient; } public async Task ExecuteAsync() { var res = await httpClient.GetAsync("url"); } }
已配置OpenTelemetry的HttpClient链路追踪:
builder.Services.AddOpenTelemetry() .WithTracing(config =>{ config.AddHttpClientInstrumentation() });
目前在Grafana中可看到10个独立Trace(每个Trace包含1个Span),请问如何设置让这10个请求共用一个TraceId,形成1个Trace包含10个Span以查看总耗时?
问题原因
当前每个HttpClient请求生成独立Trace的核心原因是:每次循环创建作用域后,ExecuteAsync中的请求没有继承父级追踪上下文,OpenTelemetry会自动为无父上下文的操作生成新的TraceId。要实现共用TraceId,需要手动创建一个根Span,让所有子请求都在这个根Span的上下文下执行。
具体步骤
1. 定义全局ActivitySource
在项目中定义一个全局的ActivitySource,用于创建自定义追踪Span:
// 放在项目公共类中,比如Constants.cs public static class TelemetryConstants { public static readonly ActivitySource BatchRequestActivitySource = new ActivitySource("MyApp.HostedService.BatchRequests"); }
2. 修改OpenTelemetry配置
将自定义的ActivitySource添加到OpenTelemetry的追踪配置中,确保它能被采集:
builder.Services.AddOpenTelemetry() .WithTracing(config => { config.AddHttpClientInstrumentation() .AddSource(TelemetryConstants.BatchRequestActivitySource.Name); // 注册自定义ActivitySource });
3. 重构HostedService的RunAsync方法
将async void改为async Task(避免异步回调的线程问题),并在方法内创建根Span,让所有子请求继承其上下文:
public class MyHostedService : IHostedService, IDisposable { private Timer? _timer; private readonly IServiceProvider _services; public MyHostedService(IServiceProvider services) { _services = services; } public Task StartAsync(CancellationToken cancellationToken) { // 使用异步回调的Timer重载 _timer = new Timer(async _ => await RunBatchRequests(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); return Task.CompletedTask; } // 重构为async Task,避免async void的潜在问题 private async Task RunBatchRequests() { // 创建根Span,作为整个批量任务的入口 using var rootActivity = TelemetryConstants.BatchRequestActivitySource.StartActivity( "BatchHttpClientRequests", ActivityKind.Internal); // 如果追踪被禁用,直接执行逻辑 if (rootActivity == null) { await ExecuteBatchRequests(); return; } // 在根Span的上下文下执行所有子请求 await ExecuteBatchRequests(); } private async Task ExecuteBatchRequests() { for (int i = 0; i < 10; i++) { // 可选:为每个子请求创建单独的子Span,方便查看单个请求耗时 using var childActivity = TelemetryConstants.BatchRequestActivitySource.StartActivity( $"Request_{i+1}", ActivityKind.Client, Activity.Current?.Context); using var scope = _services.CreateScope(); var service = scope.ServiceProvider.GetRequiredService<Service>(); await service.ExecuteAsync(); } } public Task StopAsync(CancellationToken cancellationToken) { _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public void Dispose() { _timer?.Dispose(); } }
效果说明
修改完成后,在Grafana中会看到一个包含**1个根Span + 10个HttpClient请求Span + 10个自定义子Span(可选)**的Trace,所有操作共用同一个TraceId。根Span的耗时就是整个批量任务的总耗时,子Span可以单独查看每个请求的执行时间,方便进行性能分析。
内容的提问来源于stack exchange,提问作者aisy

