仪表化

OpenTelemetry .NET 的插装

Instrumention 是指您自己向应用程序添加可观察性代码的过程。

如果您正在 instrumenting 应用程序,您需要为您使用的语言使用 OpenTelemetry SDK。然后,您将使用 SDK 初始化 OpenTelemetry,并使用 API instrument 您的代码。这将从您的应用程序和任何也带有 instrumention 的已安装库中发出遥测数据。

如果您正在 instrumenting 库,请仅安装您语言的 OpenTelemetry API 包。您的库不会自行发出遥测数据。只有当它作为使用 OpenTelemetry SDK 的应用程序的一部分时,它才会发出遥测数据。有关 instrumenting 库的更多信息,请参阅 Libraries

有关 OpenTelemetry API 和 SDK 的更多信息,请参阅 specification

关于术语的说明

.NET 与其他支持 OpenTelemetry 的语言/运行时不同。 追踪 APISystem.Diagnostics API 实现,底层重用了现有的构造,如 ActivitySourceActivity,使其符合 OpenTelemetry 标准。

然而,.NET 开发人员仍需了解 OpenTelemetry API 和术语的某些部分才能对其应用程序进行插装,这些内容在此以及 System.Diagnostics API 中都有涵盖。

如果您倾向于使用 OpenTelemetry API 而非 System.Diagnostics API,您可以参考 OpenTelemetry API 兼容层追踪文档

示例应用程序准备

本页面使用 入门指南 中的示例应用程序的修改版本,以帮助您了解手动插装。

您不必使用示例应用程序:如果您想对自己的应用程序或库进行插装,请遵循这里的说明,将过程改编到您自己的代码中。

先决条件

创建并启动 HTTP 服务器

首先,在名为 dotnet-otel-example 的新目录中设置一个环境。在该目录中,执行以下命令

dotnet new web

为了突出插装库和独立应用程序之间的区别,将掷骰子功能拆分到一个库文件中,该文件随后将作为依赖项由应用程序文件导入。

创建名为 Dice.cs 的库文件,并添加以下代码

/*Dice.cs*/

public class Dice
{
    private int min;
    private int max;

    public Dice(int min, int max)
    {
        this.min = min;
        this.max = max;
    }

    public List<int> rollTheDice(int rolls)
    {
        List<int> results = new List<int>();

        for (int i = 0; i < rolls; i++)
        {
            results.Add(rollOnce());
        }

        return results;
    }

    private int rollOnce()
    {
        return Random.Shared.Next(min, max + 1);
    }
}

创建应用程序文件 DiceController.cs,并添加以下代码

/*DiceController.cs*/

using Microsoft.AspNetCore.Mvc;
using System.Net;


public class DiceController : ControllerBase
{
    private ILogger<DiceController> logger;

    public DiceController(ILogger<DiceController> logger)
    {
        this.logger = logger;
    }

    [HttpGet("/rolldice")]
    public List<int> RollDice(string player, int? rolls)
    {
        if(!rolls.HasValue)
        {
            logger.LogError("Missing rolls parameter");
            throw new HttpRequestException("Missing rolls parameter", null, HttpStatusCode.BadRequest);
        }

        var result = new Dice(1, 6).rollTheDice(rolls.Value);

        if (string.IsNullOrEmpty(player))
        {
            logger.LogInformation("Anonymous player is rolling the dice: {result}", result);
        }
        else
        {
            logger.LogInformation("{player} is rolling the dice: {result}", player, result);
        }

        return result;
    }
}

将 Program.cs 文件的内容替换为以下代码

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();

app.Run();

Properties 子目录中,将 launchSettings.json 的内容替换为以下内容

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://:8080",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

为了确保其正常工作,请使用以下命令运行应用程序,并在浏览器中打开 https://:8080/rolldice?rolls=12

dotnet run

您应该在浏览器窗口中看到一个 12 个数字的列表,例如

[5,6,5,3,6,1,2,5,4,4,2,4]

手动插装设置

依赖项

安装以下 OpenTelemetry NuGet 包

OpenTelemetry.Exporter.Console

OpenTelemetry.Extensions.Hosting

dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Extensions.Hosting

对于基于 ASP.NET Core 的应用程序,还要安装 AspNetCore 插装包

OpenTelemetry.Instrumentation.AspNetCore

dotnet add package OpenTelemetry.Instrumentation.AspNetCore

初始化 SDK

重要的是尽早配置 OpenTelemetry SDK 的实例。

要为像示例应用程序那样的 ASP.NET Core 应用程序初始化 OpenTelemetry SDK,请将 Program.cs 文件的内容更新为以下代码

using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

// Ideally, you will want this name to come from a config file, constants file, etc.
var serviceName = "dice-server";
var serviceVersion = "1.0.0";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource.AddService(
        serviceName: serviceName,
        serviceVersion: serviceVersion))
    .WithTracing(tracing => tracing
        .AddSource(serviceName)
        .AddAspNetCoreInstrumentation()
        .AddConsoleExporter())
    .WithMetrics(metrics => metrics
        .AddMeter(serviceName)
        .AddConsoleExporter());

builder.Logging.AddOpenTelemetry(options => options
    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(
        serviceName: serviceName,
        serviceVersion: serviceVersion))
    .AddConsoleExporter());

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();

app.Run();

如果为控制台应用程序初始化 OpenTelemetry SDK,请在程序开头,在任何重要的启动操作期间,添加以下代码。

using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

//...

var serviceName = "MyServiceName";
var serviceVersion = "1.0.0";

var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(serviceName)
    .ConfigureResource(resource =>
        resource.AddService(
          serviceName: serviceName,
          serviceVersion: serviceVersion))
    .AddConsoleExporter()
    .Build();

var meterProvider = Sdk.CreateMeterProviderBuilder()
    .AddMeter(serviceName)
    .AddConsoleExporter()
    .Build();

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddOpenTelemetry(logging =>
    {
        logging.AddConsoleExporter();
    });
});

//...

tracerProvider.Dispose();
meterProvider.Dispose();
loggerFactory.Dispose();

为了进行调试和本地开发,示例将遥测导出到控制台。完成手动插装的设置后,您需要配置一个合适的导出器以 导出应用程序的遥测数据 到一个或多个遥测后端。

该示例还设置了强制的 SDK 默认属性 service.name,它保存服务的逻辑名称,以及可选但强烈推荐的属性 service.version,它保存服务 API 或实现的版本。存在设置资源属性的其他方法。有关更多信息,请参阅 资源

要验证您的代码,请构建并运行应用程序

dotnet build
dotnet run

追踪

初始化追踪

要在应用程序中启用 追踪,您需要一个已初始化的 TracerProvider,它将允许您创建一个 Tracer

如果未创建 TracerProvider,OpenTelemetry 的追踪 API 将使用一个无操作实现,并且无法生成数据。

如果您已按照上面的 初始化 SDK 说明进行操作,那么您已经设置了一个 TracerProvider。您可以继续 设置 ActivitySource

设置 ActivitySource

在您的应用程序中任何编写手动追踪代码的地方都应该配置一个 ActivitySource,您将通过它使用 Activity 元素来追踪操作。

通常建议为每个被插装的应用/服务定义一次 ActivitySource,但如果您认为合适,也可以实例化多个 ActivitySource

在示例应用程序的情况下,我们将创建一个新的文件 Instrumentation.cs 作为自定义类型来保存 ActivitySource 的引用。

using System.Diagnostics;

/// <summary>
/// It is recommended to use a custom type to hold references for ActivitySource.
/// This avoids possible type collisions with other components in the DI container.
/// </summary>
public class Instrumentation : IDisposable
{
    internal const string ActivitySourceName = "dice-server";
    internal const string ActivitySourceVersion = "1.0.0";

    public Instrumentation()
    {
        this.ActivitySource = new ActivitySource(ActivitySourceName, ActivitySourceVersion);
    }

    public ActivitySource ActivitySource { get; }

    public void Dispose()
    {
        this.ActivitySource.Dispose();
    }
}

然后,我们将更新 Program.cs 以将 Instrument 对象添加为依赖注入

//...

// Register the Instrumentation class as a singleton in the DI container.
builder.Services.AddSingleton<Instrumentation>();

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();

app.Run();

在应用程序文件 DiceController.cs 中,我们将引用该 activitySource 实例,并且相同的 activitySource 实例也将传递给库文件 Dice.cs

/*DiceController.cs*/

using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Net;

public class DiceController : ControllerBase
{
    private ILogger<DiceController> logger;

    private ActivitySource activitySource;

    public DiceController(ILogger<DiceController> logger, Instrumentation instrumentation)
    {
        this.logger = logger;
        this.activitySource = instrumentation.ActivitySource;
    }

    [HttpGet("/rolldice")]
    public List<int> RollDice(string player, int? rolls)
    {
        List<int> result = new List<int>();

        if (!rolls.HasValue)
        {
            logger.LogError("Missing rolls parameter");
            throw new HttpRequestException("Missing rolls parameter", null, HttpStatusCode.BadRequest);
        }

        result = new Dice(1, 6, activitySource).rollTheDice(rolls.Value);

        if (string.IsNullOrEmpty(player))
        {
            logger.LogInformation("Anonymous player is rolling the dice: {result}", result);
        }
        else
        {
            logger.LogInformation("{player} is rolling the dice: {result}", player, result);
        }

        return result;
    }
}
/*Dice.cs*/

using System.Diagnostics;

public class Dice
{
    public ActivitySource activitySource;
    private int min;
    private int max;

    public Dice(int min, int max, ActivitySource activitySource)
    {
        this.min = min;
        this.max = max;
        this.activitySource = activitySource;
    }

    //...
}

创建 Activity

现在您已经初始化了 activitySources,您可以创建 activities

下面的代码演示了如何创建一个 activity。

public List<int> rollTheDice(int rolls)
{
    List<int> results = new List<int>();

    // It is recommended to create activities, only when doing operations that are worth measuring independently.
    // Too many activities makes it harder to visualize in tools like Jaeger.
    using (var myActivity = activitySource.StartActivity("rollTheDice"))
    {
        for (int i = 0; i < rolls; i++)
        {
            results.Add(rollOnce());
        }

        return results;
    }
}

如果您到目前为止都遵循了 示例应用程序 的说明,您可以将上面的代码复制到您的库文件 Dice.cs 中。现在您应该能够看到从您的应用程序发出的 activities/spans。

按如下方式启动您的应用程序,然后通过访问浏览器中的 https://:8080/rolldice?rolls=12 或使用 curl 来发送请求。

dotnet run

过一会儿,您应该会在控制台中看到 ConsoleExporter 打印出的 spans,类似这样

Activity.TraceId:            841d70616c883db82b4ae4e11c728636
Activity.SpanId:             9edfe4d69b0d6d8b
Activity.TraceFlags:         Recorded
Activity.ParentSpanId:       39fcd105cf958377
Activity.ActivitySourceName: dice-server
Activity.DisplayName:        rollTheDice
Activity.Kind:               Internal
Activity.StartTime:          2024-04-10T15:24:00.3620354Z
Activity.Duration:           00:00:00.0144329
Resource associated with Activity:
    service.name: dice-server
    service.version: 1.0.0
    service.instance.id: 7a7a134f-3178-4ac6-9625-96df77cff8b4
    telemetry.sdk.name: opentelemetry
    telemetry.sdk.language: dotnet
    telemetry.sdk.version: 1.7.0

创建嵌套 Activity

嵌套 spans 让您可以追踪本质上是嵌套的工作。例如,下面的 rollOnce() 函数代表一个嵌套操作。下面的示例创建了一个追踪 rollOnce() 的嵌套 span

private int rollOnce()
{
    using (var childActivity = activitySource.StartActivity("rollOnce"))
    {
      int result;

      result = Random.Shared.Next(min, max + 1);

      return result;
    }
}

当您在追踪可视化工具中查看 spans 时,rollOnce 的 childActivity 将作为嵌套操作被追踪在 rollTheDice activity 下。

获取当前 Activity

有时在程序的特定执行点进行一些操作会很有帮助,例如操作当前/活动的 Activity/Span。

var activity = Activity.Current;

Activity 标签

标签(相当于 Attributes)允许您将键/值对附加到 Activity,以便它携带有关当前正在追踪的操作的更多信息。

private int rollOnce()
{
  using (var childActivity = activitySource.StartActivity("rollOnce"))
    {
      int result;

      result = Random.Shared.Next(min, max + 1);
      childActivity?.SetTag("dicelib.rolled", result);

      return result;
    }
}

向 Activity 添加事件

Spans 可以通过命名事件(称为 Span Events)进行注解,这些事件可以携带零个或多个 Span Attributes,每个 Span Attribute 本身就是一个键值映射,并自动与时间戳配对。

myActivity?.AddEvent(new("Init"));
...
myActivity?.AddEvent(new("End"));
var eventTags = new ActivityTagsCollection
{
    { "operation", "calculate-pi" },
    { "result", 3.14159 }
};

activity?.AddEvent(new("End Computation", DateTimeOffset.Now, eventTags));

一个 Span 可能通过 Span Link 与零个或多个其他因果相关的 Spans 相关联。链接可用于表示批处理操作,其中一个 Span 由多个发起 Span 启动,每个 Span 代表一个正在处理的单个传入项。

var links = new List<ActivityLink>
{
    new ActivityLink(activityContext1),
    new ActivityLink(activityContext2),
    new ActivityLink(activityContext3)
};

var activity = MyActivitySource.StartActivity(
    ActivityKind.Internal,
    name: "activity-with-links",
    links: links);

设置 Activity 状态

可以在 Span 上设置 Status,通常用于指定 Span 未成功完成 - Error。默认情况下,所有 spans 的状态都是 Unset,这意味着 span 完成时没有错误。Ok 状态保留给您需要明确将 span 标记为成功而不是坚持默认的 Unset(即,“无错误”)的情况。

可以在 span 完成之前的任何时间设置状态。

在发生异常时记录异常是一个好主意。建议与 设置 span 状态 结合使用。

private int rollOnce()
{
    using (var childActivity = activitySource.StartActivity("rollOnce"))
    {
        int result;

        try
        {
            result = Random.Shared.Next(min, max + 1);
            childActivity?.SetTag("dicelib.rolled", result);
        }
        catch (Exception ex)
        {
            childActivity?.SetStatus(ActivityStatusCode.Error, "Something bad happened!");
            childActivity?.AddException(ex);
            throw;
        }

        return result;
    }
}

下一步

设置手动插装后,您可能希望使用 插装库。顾名思义,它们将插装您使用的相关库,并为入站和出站 HTTP 请求等生成 spans(activities)。

您还需要配置一个合适的导出器来 将您的遥测数据导出 到一个或多个遥测后端。

您还可以查看 .NET 的 自动插装,目前处于 Beta 阶段。


最后修改日期:2025年8月14日: 添加其余dotnet内容 (#7543) (79e067c8)