You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何手动关联不同进程的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 02:07:15