.net opentelemetry collector jaeger

说明

可以使用jaeger的all in one直接采集数据而不经过collector。
但是为了整体架构的可扩展性、可修改性,可以通过collector这个组件灵活扩展,后续只需要增加
新的export就可以发送到其他服务,如Prometheus等。
如果考虑到整体的部署成本和可维护性,根据团队人员配置情况可以考虑直接采集到jaeger而不经过collector。
直接用all in one时就不用修改端口,直接部署即可。(jaeger all in on默认的4317/4318端口和collector端口重复)

部署 Jaeger(all-in-one)

内存存储,测试可以临时使用,后参考下文的持久化存储

docker run -d --name jaeger \
  --restart=always \
  -p 16686:16686 \
  -p 14317:4317 \
  -p 14318:4318 \
  -e COLLECTOR_OTLP_ENABLED=true \
  -e COLLECTOR_OTLP_GRPC_HOST_PORT=0.0.0.0:4317 \
  jaegertracing/all-in-one:latest

端口说明

  • 16686:Jaeger UI(浏览器访问)
  • 14317:映射到容器内 4317(OTLP gRPC)
  • 14318:映射到容器内 4318(OTLP HTTP)

验证 Jaeger 启动成功:

docker logs jaeger --tail 10

应该看到 OTLP gRPC listening on 0.0.0.0:4317 等信息。


持久化存储

持久化存储到内置的badger存储中,后续可以考虑改成存储到es或OpenSearch中。
需要注意data路径的权限问题。

sudo docker run -d --name jaeger \
  --restart=always \
  -p 16686:16686 \
  -p 14317:4317 \
  -p 14318:4318 \
  -e SPAN_STORAGE_TYPE=badger \
  -e BADGER_EPHEMERAL=false \
  -e BADGER_DIRECTORY_VALUE=/badger/data \
  -e BADGER_DIRECTORY_KEY=/badger/key \
  -e COLLECTOR_OTLP_ENABLED=true \
  -e COLLECTOR_OTLP_GRPC_HOST_PORT=0.0.0.0:4317 \
  -e COLLECTOR_OTLP_HTTP_HOST_PORT=0.0.0.0:4318 \
  -e BADGER_SPAN_STORE_TTL=336h \
  -v /server/dev/jaeger/data:/badger \
  jaegertracing/all-in-one:latest

部署 OpenTelemetry Collector

创建 Collector 配置文件

~/otel-demo/otel-collector-config.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024

exporters:
  debug:
    verbosity: detailed

  otlp/jaeger:
    endpoint: "<宿主机IP>:14317"   # 替换为服务器实际 IP
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug, otlp/jaeger]

注意endpoint 必须填写宿主机 IP(例如 192.168.1.100),不能写 localhost127.0.0.1,因为 Collector 容器内的 localhost 指向自身,不是宿主机。端口使用映射后的 14317

启动 Collector

docker run -d --name otel-collector \
  --restart=always \
  -p 4317:4317 -p 4318:4318 \
  -v $(pwd)/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml \
  otel/opentelemetry-collector-contrib:latest

验证 Collector 启动:

docker logs otel-collector --tail 10

.net端配置

NuGet 包

dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

注入

builder.AddOT(builder.Configuration);

对应内容如下

public static void AddOT(this WebApplicationBuilder builder, IConfiguration configuration)
{
    builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService(
            serviceName: "MyApi",
            serviceVersion: "1.0.0",
            serviceInstanceId: Environment.MachineName
        )
        .AddAttributes(new Dictionary<string, object>
        {
            ["deployment.environment"] = "development",
            ["host.name"] = Environment.MachineName
        })
    )
    .WithTracing(tracing => tracing
        // 全量采样,方便调试(生产环境务必改回默认或按比率采样)
        .SetSampler(new AlwaysOnSampler())
        // 自动采集 ASP.NET Core 请求
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;  // 记录异常信息
            options.EnrichWithHttpRequest = (activity, request) =>
            {
                activity.SetTag("http.client_ip", request.HttpContext.Connection.RemoteIpAddress?.ToString());
            };
        })
        // 自动采集 HttpClient 调用(如果应用有对外请求)
        .AddHttpClientInstrumentation()
        // 发送到 Collector
        .AddOtlpExporter(options =>
        {
            //改成对应的ip
            //options.Endpoint = new Uri("http://localhost:4317");
            options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
        })
    );
}

验证完整链路

检查 Collector 日志

触发请求后查看

docker logs otel-collector --tail 30

应该看到 Span #0 等输出,且没有 failed to exportno children to pick from 错误。

在 Jaeger UI 查看

浏览器打开 http://<服务器IP>:16686(例如 http://192.168.1.100:16686)。

  • Service 下拉框手动输入 MyApi
  • 点击 Find Traces
  • 应该看到多条 trace,点击可查看瀑布图

常见问题与排查

问题 原因 解决
Collector 日志报 no children to pick from Collector 无法连接到 Jaeger 的 OTLP 端口 检查 endpoint 是否为宿主机 IP + 14317,并确认 Jaeger 已启动且监听 0.0.0.0:4317
Jaeger UI 看不到服务 数据未到达 Jaeger 检查 Collector 日志有无错误,确认 .NET 应用已触发请求
.NET 应用启动报 gRPC 连接失败 Collector 未启动或端口被占用 确认 Collector 容器运行中,且 -p 4317:4317 映射正确
Jaeger 重启后 trace 消失 all-in-one 使用内存存储 开发环境正常,生产环境需配置持久化后端

整体架构图

.NET 8 App (localhost:4317) ──OTLP/gRPC──→ Collector (宿主机:4317)
                                              │
                                              ├─ debug 日志输出
                                              └─ OTLP/gRPC (宿主机IP:14317) ──→ Jaeger (宿主机:14317→容器4317)
                                                                                    │
                                                                                    └─ UI :16686

所有容器均不使用 Docker 网络,通过宿主机 IP + 映射端口通信。

扩展

如果需要记录接口的入参和出参,可以通过自定义Tag字段来记录,通过EnrichWithHttpRequest、EnrichWithHttpResponse回调来记录接口的入参出参。
需要注意,.net下读取body需要设置enablebuffer,可以参考(https://www.cnblogs.com/ives/p/21796524/otlp_export)。

//在var app = builder.Build();后加上
app.Use((context, next) =>
{
    context.Request.EnableBuffering(); // 允许请求体被多次读取
    return next();
});


//在app.UseRouting();后加上
app.UseMiddleware<ResponseCaptureMiddleware>();  // 放在路由之后、端点之前

注入的代码示例如下

 public static void AddOT(this WebApplicationBuilder builder, IConfiguration configuration)
 {
     builder.Services.AddOpenTelemetry()
     .ConfigureResource(resource => resource
         .AddService(
             serviceName: "MyApi",
             serviceVersion: "1.0.0",
             serviceInstanceId: Environment.MachineName
         )
         .AddAttributes(new Dictionary<string, object>
         {
             ["deployment.environment"] = "development",
             ["host.name"] = Environment.MachineName
         })
     )
     .WithTracing(tracing => tracing
         // 全量采样,方便调试(生产环境务必改回默认或按比率采样)
         .SetSampler(new AlwaysOnSampler())
         // 自动采集 ASP.NET Core 请求
         .AddAspNetCoreInstrumentation(options =>
         {
             options.RecordException = true;  // 记录异常信息

             // 1) 过滤:排除健康检查
             options.Filter = ctx =>
                 !ctx.Request.Path.StartsWithSegments("/health");

             // 2) 记录异常详情
             options.RecordException = true;

             // 3) 入参:EnrichWithHttpRequest
             options.EnrichWithHttpRequest = async (activity, request) =>
             {
                 var ctx = request.HttpContext;

                 // —— 基础信息 ——
                 activity.SetTag("request.protocol", request.Protocol);
                 activity.SetTag("request.scheme", request.Scheme);
                 activity.SetTag("request.host", request.Host.ToString());
                 activity.SetTag("request.path", request.Path.ToString());
                 activity.SetTag("request.query_string", request.QueryString.ToString());

                 // —— 客户端 IP ——
                 activity.SetTag("request.client_ip",
                     ctx.Connection.RemoteIpAddress?.ToString());

                 // —— Query 参数(扁平化)——
                 foreach (var q in request.Query)
                 {
                     activity.SetTag($"request.query.{q.Key}", q.Value.ToString());
                 }

                 // —— 指定 Header(注意脱敏)——
                 if (request.Headers.TryGetValue("X-Correlation-Id", out var corr))
                     activity.SetTag("request.correlation_id", corr.ToString());
                 if (request.Headers.TryGetValue("User-Agent", out var ua))
                     activity.SetTag("request.user_agent", ua.ToString());

                 // —— 请求体(仅当存在且体积可控)——
                 if (request.ContentLength > 0 && request.ContentLength <= 1024 * 1024)
                 {
                     try
                     {
                         // 确保已启用缓冲(以防外部未设置)
                         request.EnableBuffering();

                         // 1. 将 Body 内容复制到 MemoryStream
                         var memoryStream = new MemoryStream();
                         await request.Body.CopyToAsync(memoryStream);
                         memoryStream.Position = 0;

                         // 2. 读取 Body 内容
                         var reader = new StreamReader(memoryStream, Encoding.UTF8, leaveOpen: true);
                         var body = reader.ReadToEnd();
                         activity.SetTag("request.body", body);

                         // 3. 将 request.Body 替换为 MemoryStream(可 Seek)
                         memoryStream.Position = 0;
                         request.Body = memoryStream; // 替换后,MVC 模型绑定就能正常读取了
                     }
                     catch (Exception ex)
                     {
                         // 兜底,防止读取 Body 导致主程序崩溃
                         activity.SetTag("request.body.error", ex.Message);
                     }
                 }
             };

             // 4) 出参:EnrichWithHttpResponse
             options.EnrichWithHttpResponse = async (activity, response) =>
             {
                 activity.SetTag("response.status_code", response.StatusCode);
                 activity.SetTag("response.content_type", response.ContentType);

                 if (response.HttpContext.Items.TryGetValue("response_body", out var bodyObj))
                 {
                     var body = bodyObj as string;
                     if (!string.IsNullOrEmpty(body))
                     {
                         // 脱敏
                         //body = MaskSensitiveFields(body);
                         activity.SetTag("response.body", body);
                     }
                 }
             };

             // 5) 异常:EnrichWithException
             options.EnrichWithException = (activity, exception) =>
             {
                 activity.SetTag("exception.type", exception.GetType().FullName);
                 activity.SetTag("exception.message", exception.Message);
                 // 堆栈通过 RecordException=true 自动作为 ActivityEvent 附加
             };
         })
         // 自动采集 HttpClient 调用(如果应用有对外请求)
         .AddHttpClientInstrumentation()
         // 发送到 Collector
         .AddOtlpExporter(options =>
         {
             //options.Endpoint = new Uri("http://localhost:4317");
             options.Endpoint = new Uri("http://127.0.0.1:14317");
             options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
         })
     );
 }

至此,完整的 .NET 8 → OpenTelemetry Collector → Jaeger 链路搭建完成。后续如需增加 Prometheus 或日志收集,只需修改 Collector 配置,应用代码无需改动。

posted @ 2026-07-23 11:46  Hey,Coder!  阅读(2)  评论(0)    收藏  举报