本文將介紹如何在 ASP.NET Core 應用中整合 OTel SDK,並使用 elastic 構建可觀測性平臺展示 OTel 的資料。
本文只是使用 elastic 做基本的資料展示,詳細的使用方式同學可以參考 elastic 的官方檔案,後面也會介紹其他的對 OTel 支援較好的可觀測性後端。
範例程式碼已經上傳到了 github,地址為:
https://github.com/eventhorizon-cli/otel-demo
elastic 提供了一套完整的可觀測性平臺,並支援 OpenTelemetry protocol (OTLP) 協定。
elastic apm 部署相對比較複雜,如果有同學想在生產環境中使用,可以參考 elastic 的官方檔案進行部署或直接購買 elastic cloud。
為方便同學們學習,我準備好了一個 elastic 的 docker-compose 檔案,包含了以下元件:
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 專案,然後安裝以下依賴:
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"));
});
});
ASP.NET Core 以及 Entity Framework Core 等框架中有很多預置的埋點(通過 DiagnosticSource 實現),通過這些預置的埋點,我們可以收集到大量的資料,並藉此建立出 Trace、Metric。
比如,通過 ASP.NET Core 中 HTTP 請求 的埋點,可以建立出代表此次 HTTP 請求的 Span,並記錄下各個 API 的耗時、請求頻率等 Metrics。
下面我們在應用中新增兩個 Instrumentation
OpenTelemetry.Instrumentation.AspNetCore
:ASP.NET Core 的 InstrumentationOpenTelemetry.Instrumentation.Http
:HTTP 請求的 Instrumentation,如果想要跨程序傳輸 Baggage,也需要新增此 InstrumentationtracerBuilder
// 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
前一篇文章中,我們介紹了利用 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";
}
}
啟動 FooService 和 BarService,然後存取 FooService 的 /api/foo。
接下來我們就可以在 kibana 中檢視資料了。
如果檢視資料時,時區顯示有問題,可以在 kibana 的 Management -> Advanced Settings 中修改時區。
在 kibana 中,選擇 APM,然後選擇 Services 或者 Traces 索引標籤,就可以看到 FooService 和 BarService 的 Trace 了。
隨意點開一個 Trace,就可以看到這個 Trace 的詳細資訊了。
Timeline 中的每一段都是一個 Span,還可以看到我們之前建立的自定義 Span FooActivity。
點選 Span,可以看到 Span 的詳細資訊。
可以在 kibana 中選擇 Metrics Explorer 檢視 Metrics 資料。
詳細的使用方式可以參考 elastic 的官方檔案:
https://www.elastic.co/guide/en/observability/current/explore-metrics.html
在 trace 介面,我們點選邊上的 Logs 索引標籤,就可以看到這個 Trace 所關聯的 Logs 了。
我們也可以在 Discover 中檢視所有的 Logs,並根據 log 中的 trace.id 去查詢相關的 trace。
歡迎關注個人技術公眾號