使用 OpenTelemetry 構建 .NET 應用可觀測性(4):ASP.NET Core 應用中集成 OTel

前言

本文將介紹如何在 ASP.NET Core 應用中集成 OTel SDK,並使用 elastic 構建可觀測性平臺展示 OTel 的數據。

本文只是使用 elastic 做基本的數據展示,詳細的使用方式同學可以參考 elastic 的官方文檔,後面也會介紹其他的對 OTel 支持較好的可觀測性後端。

示例代碼已經上傳到了 github,地址爲:
https://github.com/eventhorizon-cli/otel-demo

使用 elastic 構建可觀測性平臺

elastic 提供了一套完整的可觀測性平臺,並支持 OpenTelemetry protocol (OTLP) 協議。

elastic apm 部署相對比較複雜,如果有同學想在生產環境中使用,可以參考 elastic 的官方文檔進行部署或直接購買 elastic cloud。

https://www.elastic.co/cn/blog/adding-free-and-open-elastic-apm-as-part-of-your-elastic-observability-deployment

爲方便同學們學習,我準備好了一個 elastic 的 docker-compose 文件,包含了以下組件:

  • elasticsearch:用於存儲數據
  • kibana:用於展示數據
  • apm-server:處理 OTel 的數據
  • fleet-server:用於管理 apm-agent,apm-agent 可以接收 OTLP 的數據,並將數據發送給 apm-server

docker-compose 文件已經上傳到了 github,地址爲:

https://github.com/eventhorizon-cli/otel-demo/blob/main/ElasticAPM/docker-compose.yml

docker-compose 啓動的過程中可能會遇到部分容器啓動失敗的情況,可以手動重啓這部分容器。

啓動完成後,我們還需要一點配置,才能啓用 apm-server。

打開 http://localhost:5601 ,進入 kibana 的管理界面,用戶名 admin,密碼是 changeme。

進入後會提示你添加集成。

點擊 Add integrations,選擇 APM。

然後一路確定,就可以了。



在 ASP.NET Core 應用中集成 OTel SDK

安裝依賴

創建一個 ASP.NET Core 項目,然後安裝以下依賴:

  • OpenTelemetry:OpenTelemetry 的核心庫,包含了 OTel 的數據模型和 API。
  • OpenTelemetry.Extensions.Hosting:ASP.NET Core 的擴展,用於在 ASP.NET Core 應用中集成 OTel。
  • OpenTelemetry.Exporter.OpenTelemetryProtocol:OTel 的 OTLP exporter,用於將 OTel 的數據發送給可觀測性後端。
  • OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs:OTel Logs 的 OTLP exporter,用於將 OTel 的 Logs 數據發送給可觀測性後端。

基礎配置

在 Program.cs 中,我們需要添加以下代碼:

builder.Services.AddOpenTelemetry()
    // 這邊配置的 Resource 是全局的,Log、Metric、Trace 都會使用這個 Resource
    .ConfigureResource(resourceBuilder =>
    {
        resourceBuilder
            .AddService("FooService", "TestNamespace", "1.0.0")
            .AddTelemetrySdk();
    })
    .WithTracing(tracerBuilder =>
    {
        tracerBuilder
            .AddOtlpExporter(options => options.Endpoint = new Uri("http://localhost:8200"));
    }).WithMetrics(meterBuilder =>
    {
        meterBuilder
            .AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri("http://localhost:8200"));
    });

builder.Services.AddLogging(loggingBuilder =>
{
    loggingBuilder.AddOpenTelemetry(options =>
    {
        options.IncludeFormattedMessage = true;
        options.AddOtlpExporter(options => options.Endpoint = new Uri("http://localhost:8200"));
    });
});

Instrumentation 配置

ASP.NET Core 以及 Entity Framework Core 等框架中有很多預置的埋點(通過 DiagnosticSource 實現),通過這些預置的埋點,我們可以收集到大量的數據,並藉此創建出 Trace、Metric。

比如,通過 ASP.NET Core 中 HTTP 請求 的埋點,可以創建出代表此次 HTTP 請求的 Span,並記錄下各個 API 的耗時、請求頻率等 Metrics。

下面我們在應用中添加兩個 Instrumentation

  • OpenTelemetry.Instrumentation.AspNetCore:ASP.NET Core 的 Instrumentation
  • OpenTelemetry.Instrumentation.Http:HTTP 請求的 Instrumentation,如果想要跨進程傳輸 Baggage,也需要添加此 Instrumentation
tracerBuilder
    // ASP.NET Core 的 Instrumentation
    .AddAspNetCoreInstrumentation(options =>
    {
        // 配置 Filter,忽略 swagger 的請求
        options.Filter =
            httpContent => httpContent.Request.Path.StartsWithSegments("/swagger") == false;
    })
    // HTTP 請求的 Instrumentation,如果想要跨進程傳輸 Baggage,也需要添加此 Instrumentation
    .AddHttpClientInstrumentation()
    .AddOtlpExporter(options => options.Endpoint = new Uri("http://localhost:8200"));
meterBuilder
    .AddAspNetCoreInstrumentation()
    .AddHttpClientInstrumentation()
    .AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri("http://localhost:8200"));

除了上面介紹的兩個兩個 Instrumentation,OTel SDK 還提供了很多 Instrumentation,可以在下面的鏈接中查看:

https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src

https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src

創建自定義 Span 和 Metric

前一篇文章中,我們介紹了利用 ActivitySource 創建 自定義Span 和利用 Meter 創建 自定義Metric 的方法。

在 ASP.NET Core 中集成了 OTel SDK 後,我們可以將這些自定義的 Span 和 Metric 通過 OTel SDK 的 Exporter 發送給可觀測性後端。

tracerBuilder
    // 這邊註冊了 ActivitySource,OTel SDK 會去監聽這個 ActivitySource 創建的 Activity
    .AddSource("FooActivitySource")
    .AddOtlpExporter(options => options.Endpoint = new Uri("http://localhost:8200"));
meterBuilder
    // 這邊註冊了 Meter,OTel SDK 會去監聽這個 Meter 創建的 Metric
    .AddMeter("FooMeter")
    .AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri("http://localhost:8200"));

完整的代碼演示

下面我們創建兩個 API 項目,一個叫做 FooService,一個叫做 BarService。兩個服務都配置了 OTel SDK,其中 FooService 會調用 BarService。

FooService 的關鍵代碼如下:

builder.Services.AddHttpClient();
builder.Services.AddOpenTelemetry()
    // 這邊配置的 Resource 是全局的,Log、Metric、Trace 都會使用這個 Resource
    .ConfigureResource(resourceBuilder =>
    {
        resourceBuilder
            .AddService("FooService", "TestNamespace", "1.0.0")
            .AddTelemetrySdk();
    })
    .WithTracing(tracerBuilder =>
    {
        tracerBuilder
            .AddAspNetCoreInstrumentation(options =>
            {
                // 配置 Filter,忽略 swagger 的請求
                options.Filter =
                    httpContent => httpContent.Request.Path.StartsWithSegments("/swagger") == false;
            })
            .AddHttpClientInstrumentation()
            .AddSource("FooActivitySource")
            .AddOtlpExporter(options => options.Endpoint = new Uri("http://localhost:8200"));
    }).WithMetrics(meterBuilder =>
    {
        meterBuilder
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddMeter("FooMeter")
            .AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri("http://localhost:8200"));
    });

builder.Services.AddLogging(loggingBuilder =>
{
    loggingBuilder.AddOpenTelemetry(options =>
    {
        options.IncludeFormattedMessage = true;
        options.AddOtlpExporter(options => options.Endpoint = new Uri("http://localhost:8200"));
    });
});
[Route("/api/[controller]")]
public class FooController : ControllerBase
{
    private static readonly ActivitySource FooActivitySource
        = new ActivitySource("FooActivitySource");
    private static readonly Counter<int> FooCounter
        = new Meter("FooMeter").CreateCounter<int>("FooCounter");

    private readonly IHttpClientFactory _clientFactory;
    private readonly ILogger<FooController> _logger;

    public FooController(
        IHttpClientFactory clientFactory,
        ILogger<FooController> logger)
    {
        _clientFactory = clientFactory;
        _logger = logger;
    }
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        _logger.LogInformation("/api/foo called");

        Baggage.SetBaggage("FooBaggage1", "FooValue1");
        Baggage.SetBaggage("FooBaggage2", "FooValue2");

        var client = _clientFactory.CreateClient();
        var result = await client.GetStringAsync("http://localhost:5002/api/bar");

        using var activity = FooActivitySource.StartActivity("FooActivity");
        activity?.AddTag("FooTag", "FooValue");
        activity?.AddEvent(new ActivityEvent("FooEvent"));
        await Task.Delay(100);

        FooCounter.Add(1);

        return Ok(result);
    }
}

BarService 的關鍵代碼如下:

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resourceBuilder =>
    {
        resourceBuilder
            .AddService("BarService", "TestNamespace", "1.0.0")
            .AddTelemetrySdk();
    })
    .WithTracing(options =>
    {
        options
            .AddAspNetCoreInstrumentation(options =>
            {
                // 配置 Filter,忽略 swagger 的請求
                options.Filter =
                    httpContent => httpContent.Request.Path.StartsWithSegments("/swagger") == false;
            })
            .AddHttpClientInstrumentation()
            .AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri("http://localhost:8200"));
    }).WithMetrics(options =>
    {
        options
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri("http://localhost:8200"));
    });

builder.Services.AddLogging(loggingBuilder =>   
{
    loggingBuilder.AddOpenTelemetry(options =>
    {
        options.IncludeFormattedMessage = true;
        options.AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri("http://localhost:8200"));
    });
});
[Route("/api/[controller]")]
public class BarController : ControllerBase
{
    private readonly ILogger<BarController> _logger;

    public BarController(ILogger<BarController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public async Task<string> Get()
    {
        _logger.LogInformation("/api/bar called");

        var baggage1 = Baggage.GetBaggage("FooBaggage1");
        var baggage2 = Baggage.GetBaggage("FooBaggage2");
        
        _logger.LogInformation($"FooBaggage1: {baggage1}, FooBaggage2: {baggage2}");

        return "Hello from Bar";
    }
}

kibana 中查看數據

啓動 FooService 和 BarService,然後訪問 FooService 的 /api/foo。

接下來我們就可以在 kibana 中查看數據了。

如果查看數據時,時區顯示有問題,可以在 kibana 的 Management -> Advanced Settings 中修改時區。

Tracing

在 kibana 中,選擇 APM,然後選擇 Services 或者 Traces 選項卡,就可以看到 FooService 和 BarService 的 Trace 了。

隨意點開一個 Trace,就可以看到這個 Trace 的詳細信息了。
Timeline 中的每一段都是一個 Span,還可以看到我們之前創建的自定義 Span FooActivity。

點擊 Span,可以看到 Span 的詳細信息。

Metrics

可以在 kibana 中選擇 Metrics Explorer 查看 Metrics 數據。

詳細的使用方式可以參考 elastic 的官方文檔:

https://www.elastic.co/guide/en/observability/current/explore-metrics.html

Tracing 和 Logs 的關聯

在 trace 界面,我們點擊邊上的 Logs 選項卡,就可以看到這個 Trace 所關聯的 Logs 了。

我們也可以在 Discover 中查看所有的 Logs,並根據 log 中的 trace.id 去查詢相關的 trace。

歡迎關注個人技術公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章