使用 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。
为方便同学们学习,我准备好了一个 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 的 InstrumentationOpenTelemetry.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。
欢迎关注个人技术公众号