在.NET中使用OpenTelemetry实现链路追踪(一)之详细教程与指南

概述

本篇文章介绍如何使在.Net中使用OpenTelemetry把链路数据包括:服务与服务之间,服务与数据库之间,服务与第三方中间件(Redis)之间的数据调用链路都链接起来,并且同时导出Jaeger和Zipkin这2个开源的链路追踪工具中。

创建项目以及插件

  • 如果还没有配置OpenTelemetry Collector,按照之前的文章去配置OpenTelemetry Collector的配置

  • 使用docker安装redis:

    1
    docker run --name redis -p 6379:6379 -d redis
  • 创建2个Asp.Net Core Web Api:

    • ServiceA :在链路中主要是发送Http请求到ServiceB,扮演发生请求的服务角色。

      • 安装如下几个包

        • OpenTelemetry.Exporter.OpenTelemetryProtocol
        • OpenTelemetry.Extensions.Hosting
        • OpenTelemetry.Instrumentation.AspNetCore
        • OpenTelemetry.Instrumentation.Http
      • 在容器中注入如下几个服务:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        services.AddOpenTelemetry()
        .ConfigureResource(resource => resource.AddService("ServiceA", serviceVersion: "1.0.0"))
        .WithTracing(builder => builder
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter());

        services.AddHttpClient("ServiceB", client =>
        {
        client.BaseAddress = new Uri("http://localhost:5062");
        });
    • ServiceB : 在链路中接收ServiceA发送过来的请求,并且在之后依次请求数据库和Redis

      • 安装如下几个包:

        • Microsoft.EntityFrameworkCore.SqlServer
        • Microsoft.EntityFrameworkCore.Tools
        • OpenTelemetry.Exporter.OpenTelemetryProtocol
        • OpenTelemetry.Extensions.Hosting
        • OpenTelemetry.Instrumentation.AspNetCore
        • OpenTelemetry.Instrumentation.EntityFrameworkCore
        • OpenTelemetry.Instrumentation.Http
        • OpenTelemetry.Instrumentation.StackExchangeRedis
        • StackExchange.Redis
      • 在容器中注入如下几个服务:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        services.AddDbContext<ServiceADBContext>(options =>options.UseSqlServer(configuration.GetConnectionString("OpenTelemetryTestServiceBContext")));
        services.AddSingleton<IConnectionMultiplexer>(sp => ConnectionMultiplexer.Connect("localhost:6379"));
        services.AddOpenTelemetry()
        .WithTracing(builder => builder
        .ConfigureResource(resource => resource.AddService("ServiceB", serviceVersion: "1.0.0"))
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddRedisInstrumentation()
        .AddOtlpExporter());
    • 然后分别在两个服务中添加如下代码

      • ServiceA中在ValueController中添加如下代码

        1
        2
        3
        4
        5
        6
        [HttpGet]
        public async Task<List<DBValueEntity>> Get([FromServices]IHttpClientFactory httpClientFactory)
        {
        var list = await httpClientFactory.CreateClient("ServiceB").GetFromJsonAsync<List<DBValueEntity>>("/api/dbvalues");
        return list;
        }
      • ServiceB中DBValueController中添加如下代码:

        1
        2
        3
        4
        5
        6
        7
        [HttpGet]
        public IEnumerable<DBValueEntity> Get([FromServices]ServiceADBContext context, [FromServices] IConnectionMultiplexer connectionMultiplexer)
        {
        var list =context.DBValues.Take(10).ToList(); // Ensure the context is used to trigger DB access
        connectionMultiplexer.GetDatabase(0).StringSet("testKey", "testValue"); // Example Redis operation
        return list;
        }
    • 最后调用ServiceA中/api/value就会产生如下的链路调用

      • Timeline

        Trace显示Timeline

      • Graph

        Trace显示Graph

添加自定义的Span

上面的例子演示了一些官方和社区开发和维护的一些采集器,能够大大提升我们的开发效率,但是有些时候,我们也需要在业务中增加一些我们自己的链路块,接下来我们就演示一下如何增加自己的链路块。我们主要是改动在ServiceA,

在构建Trace的代码中增加一个.AddSource("ServiceA"),不然无法把产生的数据导出。

1
2
3
4
5
6
7
8
9
10
11
12
services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("ServiceA", serviceVersion: "1.0.0"))
.WithTracing(builder => builder
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource("ServiceA")
.AddOtlpExporter());

services.AddHttpClient("ServiceB", client =>
{
client.BaseAddress = new Uri("http://localhost:5062");
});

另外修改一下ServiceA中ValueController

1
2
3
4
5
6
7
8
9
10
public async Task<List<DBValueEntity>> Get([FromServices]IHttpClientFactory httpClientFactory, [FromServices]TracerProvider tracerProvider)
{
using (var tracer = tracerProvider.GetTracer("ServiceA").StartActiveSpan("GetValuesFromServiceB"))
{
tracer.SetAttribute("operation", "GetValuesFromServiceB");
tracer.AddEvent("Calling ServiceB to get DB values");
var list = await httpClientFactory.CreateClient("ServiceB").GetFromJsonAsync<List<DBValueEntity>>("/api/dbvalues");
return list;
}
}

最终产生的链路如下:

自定义Span在链路中的显示

好了基本上掌握以上几点,就可以完成我们应用程序中的链路追踪了,记得在采集某一个中间件或者服务的数据时,可以先去社区寻找是否有现成的采集器帮助我们,加快我们的开发效率。下一节我们会进一步讲解其中的原理,以及如何优雅的实现数据采集。