如何手动关联不同进程的OpenTelemetry Trace,在Jaeger中展示为单链路?
我有两个控制台应用(App1和App2),使用System.Diagnostics.Activity生成Trace。App1会触发App2,但并非通过HTTP请求,因此无法使用请求头传递Trace信息。
在Jaeger中查看时,能看到两个服务(App1和App2)以及两个独立的Trace:App1:Step1和App2:Step2,且App2:Step2的Trace存在警告信息invalid parent span IDs=...; skipping clock skew adjustment。
App1.csproj 和 App2.csproj
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.4.0" /> </ItemGroup> </Project>
App1\Program.cs(原代码)
using System; using System.Diagnostics; using System.Threading; using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace App1 { internal class Program { static void Main(string[] args) { TracerProvider? tracer = Sdk.CreateTracerProviderBuilder() .ConfigureResource(builder => { builder.AddService(nameof(App1)); }) .AddSource(nameof(App1)) .AddOtlpExporter(cfg => { cfg.Endpoint = new Uri("http://localhost:14317"); // Jaeger container cfg.Protocol = OtlpExportProtocol.Grpc; }) .Build(); ActivitySource source = new ActivitySource(nameof(App1)); using (Activity? activity = source.StartActivity("Step1")) { Console.WriteLine($"{nameof(App1)} activity ID: {activity?.Id}"); Thread.Sleep(TimeSpan.FromSeconds(1)); // fake work } tracer?.Dispose(); } } }
App2\Program.cs(原代码)
using System; using System.Diagnostics; using System.Threading; using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace App2 { internal class Program { static void Main(string[] args) { TracerProvider? tracer = Sdk.CreateTracerProviderBuilder() .ConfigureResource(builder => { builder.AddService(nameof(App2)); }) .AddSource(nameof(App2)) .AddOtlpExporter(cfg => { cfg.Endpoint = new Uri("http://localhost:14317"); // Jaeger container cfg.Protocol = OtlpExportProtocol.Grpc; }) .Build(); ActivitySource source = new ActivitySource(nameof(App2)); using (Activity? activity = source.StartActivity("Step2")) { string parentId = "..."; // set to App1's activity.Id activity?.SetParentId(parentId); Console.WriteLine($"{nameof(App2)} activity ID: {activity?.Id}"); Console.WriteLine($"{nameof(App2)} activity parent ID: {activity?.ParentId}"); Thread.Sleep(TimeSpan.FromSeconds(1)); // fake work } tracer?.Dispose(); } } }
问题出在App2启动Activity后再设置ParentId的方式不正确:Activity启动时会自动生成新的TraceId,事后修改ParentId无法让它关联到App1的Trace。正确的做法是在启动App2的Activity时,直接传入App1的完整Trace上下文,确保两者共享同一个TraceId。
步骤1:App1传递完整的Trace上下文给App2
App1需要把Activity的TraceParent字符串(包含TraceId、SpanId、TraceFlags)传递给App2,可通过命令行参数、文件、本地消息队列等IPC方式(这里以命令行参数为例)。
修改App1的Program.cs:
using System; using System.Diagnostics; using System.IO; using System.Threading; using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace App1 { internal class Program { static void Main(string[] args) { TracerProvider? tracer = Sdk.CreateTracerProviderBuilder() .ConfigureResource(builder => { builder.AddService(nameof(App1)); }) .AddSource(nameof(App1)) .AddOtlpExporter(cfg => { cfg.Endpoint = new Uri("http://localhost:14317"); cfg.Protocol = OtlpExportProtocol.Grpc; }) .Build(); ActivitySource source = new ActivitySource(nameof(App1)); using (Activity? activity = source.StartActivity("Step1")) { Console.WriteLine($"{nameof(App1)} activity ID: {activity?.Id}"); Console.WriteLine($"{nameof(App1)} trace parent: {activity?.TraceParent}"); // 将TraceParent作为命令行参数启动App2 if (activity != null) { var app2Path = Path.GetFullPath("../../../../App2/bin/Debug/net6.0/App2.dll"); Process.Start(new ProcessStartInfo("dotnet", $"{app2Path} {activity.TraceParent}") { UseShellExecute = false, CreateNoWindow = false }); } Thread.Sleep(TimeSpan.FromSeconds(1)); } tracer?.Dispose(); } } }
步骤2:App2基于传入的Trace上下文启动Activity
App2接收传递的TraceParent字符串,解析为ActivityContext,然后在启动Activity时直接指定父上下文,这样新的Activity会复用App1的TraceId,成为同一个Trace下的子Span。
修改App2的Program.cs:
using System; using System.Diagnostics; using System.Threading; using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace App2 { internal class Program { static void Main(string[] args) { TracerProvider? tracer = Sdk.CreateTracerProviderBuilder() .ConfigureResource(builder => { builder.AddService(nameof(App2)); }) .AddSource(nameof(App2)) .AddOtlpExporter(cfg => { cfg.Endpoint = new Uri("http://localhost:14317"); cfg.Protocol = OtlpExportProtocol.Grpc; }) .Build(); ActivitySource source = new ActivitySource(nameof(App2)); ActivityContext parentContext = default; // 解析传入的TraceParent字符串 if (args.Length > 0 && ActivityContext.TryParse(args[0], out var parsedContext)) { parentContext = parsedContext; } // 启动Activity时指定父上下文 using (Activity? activity = source.StartActivity("Step2", ActivityKind.Client, parentContext)) { Console.WriteLine($"{nameof(App2)} activity ID: {activity?.Id}"); Console.WriteLine($"{nameof(App2)} activity parent ID: {activity?.ParentId}"); Console.WriteLine($"{nameof(App2)} trace ID: {activity?.TraceId}"); Thread.Sleep(TimeSpan.FromSeconds(1)); } tracer?.Dispose(); } } }
关键说明
TraceParent是W3C Trace Context标准格式的字符串,包含了关联Trace所需的全部信息(TraceId、SpanId、采样标记),比单独传递Activity.Id更完整。- 在
StartActivity时传入父上下文,会让新Activity直接继承父的TraceId,从而在Jaeger中显示为同一个Trace下的两个服务Span。 - 避免在Activity启动后调用
SetParentId,这种方式无法正确更新Trace上下文,会导致独立的Trace和Jaeger的无效父Span警告。
内容的提问来源于stack exchange,提问作者ElGordo

