示例:将 OpenTelemetry 与 Prometheus、Grafana 和 Jaeger 结合使用
本例使用 Prometheus 收集指标,使用 Grafana 创建仪表板,使用 Jaeger 显示分布式跟踪。
1.创建项目
使用 Visual Studio 中的 ASP.NET Core 空模板或以下 .NET CLI 命令创建简单的 Web API 项目:
dotnet new web
2. 添加指标和活动定义
下面的代码为调用 API 的次数定义了新的指标 (greetings.count
) 和新活动源 (OtPrGrYa.Example
)。
// Custom metrics for the application
var greeterMeter = new Meter("OtPrGrYa.Example", "1.0.0");
var countGreetings = greeterMeter.CreateCounter<int>("greetings.count", description: "Counts the number of greetings");
// Custom ActivitySource for the application
var greeterActivitySource = new ActivitySource("OtPrGrJa.Example");
3. 创建 API 端点
app.MapGet("/", SendGreeting);
async Task<String> SendGreeting(ILogger<Program> logger)
{
// Create a new Activity scoped to the method
using var activity = greeterActivitySource.StartActivity("GreeterActivity");
// Log a message
logger.LogInformation("Sending greeting");
// Increment the custom counter
countGreetings.Add(1);
// Add a tag to the Activity
activity?.SetTag("greeting", "Hello World!");
return "Hello World!";
}
注意
API 定义没有使用任何特定于 OpenTelemetry 的事项。 它使用 .NET API 实现可观测性。
4. 引用 OpenTelemetry 包
使用 NuGet 包管理器或命令行添加以下 NuGet 包:
<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
</ItemGroup>
注意
使用最新版本,因为 OTel API 不断发展。
5. 使用正确的提供程序配置 OpenTelemetry
var tracingOtlpEndpoint = builder.Configuration["OTLP_ENDPOINT_URL"];
var otel = builder.Services.AddOpenTelemetry();
// Configure OpenTelemetry Resources with the application name
otel.ConfigureResource(resource => resource
.AddService(serviceName: builder.Environment.ApplicationName));
// Add Metrics for ASP.NET Core and our custom metrics and export to Prometheus
otel.WithMetrics(metrics => metrics
// Metrics provider from OpenTelemetry
.AddAspNetCoreInstrumentation()
.AddMeter(greeterMeter.Name)
// Metrics provides by ASP.NET Core in .NET 8
.AddMeter("Microsoft.AspNetCore.Hosting")
.AddMeter("Microsoft.AspNetCore.Server.Kestrel")
.AddPrometheusExporter());
// Add Tracing for ASP.NET Core and our custom ActivitySource and export to Jaeger
otel.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation();
tracing.AddHttpClientInstrumentation();
tracing.AddSource(greeterActivitySource.Name);
if (tracingOtlpEndpoint != null)
{
tracing.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri(tracingOtlpEndpoint);
});
}
else
{
tracing.AddConsoleExporter();
}
});
此代码使用 ASP.NET Core 检测来从 ASP.NET Core 获取指标和活动。 它还分别注册 Metrics
和 ActivitySource
提供程序,用于指标和跟踪。
代码为指标使用 Prometheus 导出程序,该导出程序使用 ASP.NET Core 来托管终结点,因此还需要添加:
// Configure the Prometheus scraping endpoint
app.MapPrometheusScrapingEndpoint();
6. 运行项目
运行项目,然后使用浏览器或 curl 访问 API。
curl -k http://localhost:7275
每次请求页面时,它都会增加已发出问候语的数量。 可以使用相同的基本 url 访问指标终结点,路径为 /metrics
。
6.1 日志输出
代码中的日志记录语句使用 ILogger
输出。 默认情况下,将启用控制台提供程序,以便将输出定向到控制台。
关于如何从 .NET 中注销日志,有几个选项:
stdout
和stderr
输出由容器系统(如 Kubernetes)重定向到日志文件。- 使用将与 ILogger 集成的日志记录库,这些库包括 Serilog 或 NLog。
- 使用 OTel 的日志记录提供程序,如 OTLP 或 Azure Monitor 导出程序,如下所示。
6.2 访问指标
可以使用 /metrics
终结点访问指标。
curl -k https://localhost:7275/
Hello World!
curl -k https://localhost:7275/metrics
# TYPE greetings_count counter
# HELP greetings_count Counts the number of greetings
greetings_count 1 1686894204856
# TYPE current_connections gauge
# HELP current_connections Number of connections that are currently active on the server.
current_connections{endpoint="127.0.0.1:7275"} 1 1686894204856
current_connections{endpoint="[::1]:7275"} 0 1686894204856
current_connections{endpoint="[::1]:5212"} 1 1686894204856
...
指标输出是请求终结点时指标的快照。 结果以 Prometheus 阐述格式提供,这是人工可读的,但 Prometheus 更容易理解。 下一阶段将讨论该主题。
6.3 访问跟踪
如果查看服务器的控制台,将看到控制台跟踪导出程序的输出,该导出程序以人工可读的格式输出信息。 这应该显示两个活动,一个来自自定义 ActivitySource
,另一个来自 ASP.NET Core:
Activity.TraceId: 2e00dd5e258d33fe691b965607b91d18
Activity.SpanId: 3b7a891f55b97f1a
Activity.TraceFlags: Recorded
Activity.ParentSpanId: 645071fd0011faac
Activity.ActivitySourceName: OtPrGrYa.Example
Activity.DisplayName: GreeterActivity
Activity.Kind: Internal
Activity.StartTime: 2023-06-16T04:50:26.7675469Z
Activity.Duration: 00:00:00.0023974
Activity.Tags:
greeting: Hello World!
Resource associated with Activity:
service.name: OTel-Prometheus-Grafana-Jaeger
service.instance.id: e1afb619-bc32-48d8-b71f-ee196dc2a76a
telemetry.sdk.name: opentelemetry
telemetry.sdk.language: dotnet
telemetry.sdk.version: 1.5.0
Activity.TraceId: 2e00dd5e258d33fe691b965607b91d18
Activity.SpanId: 645071fd0011faac
Activity.TraceFlags: Recorded
Activity.ActivitySourceName: Microsoft.AspNetCore
Activity.DisplayName: /
Activity.Kind: Server
Activity.StartTime: 2023-06-16T04:50:26.7672615Z
Activity.Duration: 00:00:00.0121259
Activity.Tags:
net.host.name: localhost
net.host.port: 7275
http.method: GET
http.scheme: https
http.target: /
http.url: https://localhost:7275/
http.flavor: 1.1
http.user_agent: curl/8.0.1
http.status_code: 200
Resource associated with Activity:
service.name: OTel-Prometheus-Grafana-Jaeger
service.instance.id: e1afb619-bc32-48d8-b71f-ee196dc2a76a
telemetry.sdk.name: opentelemetry
telemetry.sdk.language: dotnet
telemetry.sdk.version: 1.5.0
第一个是创建的内部自定义活动。 第二个由 ASP.NET 为请求创建,并包含 HTTP 请求属性的标记。 将看到两者都有相同的 TraceId
,它标识单个事务,并且在分布式系统中可以用于关联事务中涉及的每个服务的跟踪。 ID 以 HTTP 头的形式传输。 ASP.NET Core 在收到请求时,如果不存在,则会分配 TraceId
。 HttpClient
在出站请求中默认包含标头。 每个活动都有 SpanId
,它是 TraceId
和 SpanId
的组合,唯一标识每个活动。 Greeter
活动通过其 ParentSpanId
作为 HTTP 活动的父级,映射到 HTTP 活动的 SpanId
。
在稍后的阶段,将把这些数据输入到 Jaeger 中,以可视化分布式跟踪。
7. 使用 Prometheus 收集指标
Prometheus 是指标集合、聚合和时序数据库系统。 为它配置每个服务的指标终结点,它会定期抓取值并将其存储在时序数据库中。 然后,可以根据需要对它们进行分析和处理。
以 Prometheus 格式公开的指标数据是进程指标的时间点快照。 每次向指标终结点发出请求时,它都会报告当前值。 虽然当前值很有趣,但与历史值相比,它们变得更有价值,以查看趋势并检测值是否异常。 通常,服务会根据一天中的时间或真实事件(例如假日购物狂欢)出现使用高峰。 通过将这些值与历史趋势进行比较,可以检测它们是否异常,或者某个指标是否随着时间的推移而慢慢恶化。
进程不存储这些指标快照的任何历史记录。 将这种功能添加到进程中可能需要耗费大量资源。 此外,在分布式系统中,每个节点通常有多个实例,因此需要能够从所有节点收集指标,然后聚合并与它们的历史值进行比较。
7.1 安装并配置 Prometheus
从 https://prometheus.io/download/ 为平台下载 Prometheus,并提取下载的内容。
查看正在运行的服务器的输出顶部,以获取 http 终结点的端口号。 例如:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7275
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5212
修改 Prometheus YAML 配置文件以指定 HTTP 抓取终结点的端口,并设置较低的抓取间隔。 例如:
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: "prometheus"
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
scrape_interval: 1s # poll very quickly for a more responsive demo
static_configs:
- targets: ["localhost:5212"]
启动 Prometheus,并查看其运行端口的输出,通常为 9090:
>prometheus.exe
...
ts=2023-06-16T05:29:02.789Z caller=web.go:562 level=info component=web msg="Start listening for connections" address=0.0.0.0:9090
在浏览器中打开此 URL。 现在应该能够在 Prometheus UI 中查询指标。 使用下图中突出显示的按钮打开指标资源管理器,其中显示了所有可用的指标。
选择 greetings_count
指标以查看值图。
8. 使用 Grafana 创建指标仪表板
Grafana 是仪表板产品,可以基于 Prometheus 或其他数据源创建仪表板和警报。
按照平台的说明,从 https://grafana.com/oss/grafana/ 下载并安装 Grafana 的 OSS 版本。 安装后,Grafana 通常在端口 3000 上运行,因此请在浏览器中打开 http://localhost:3000
。 需要登录;默认的用户名和密码都是 admin
。
从汉堡菜单中选择连接,然后输入文本 prometheus
以选择终结点类型。 选择“创建 Prometheus 数据源”以添加新的数据源。
需要设置以下属性:
- Prometheus 服务器 URL:
http://localhost:9090/
根据需要更改端口
选择“保存并测试”以验证配置。
收到成功消息后,就可以配置仪表板。 单击弹出项中显示的“生成仪表板”链接以获取成功消息。
选择“添加可视化效果”,然后选择刚刚添加的 Prometheus 数据源作为数据源。
应显示仪表板面板设计器。 在屏幕的下半部分,可以定义查询。
选择 greetings_count
指标,然后选择“运行查询”以查看结果。
使用 Grafana,可以设计复杂的仪表板来跟踪任何数量的指标。
.NET 中的每个指标都可以有额外的维度,这些维度是可用于对数据进行分区的键值对。 ASP.NET 指标都具有适用于计数器的多个维度。 例如,来自 Microsoft.AspNetCore.Hosting
的 current-requests
计数器具有以下维度:
属性 | 类型 | 说明 | 示例 | 状态 |
---|---|---|---|---|
method |
string |
HTTP 请求方法。 | .- . | 始终 |
scheme |
string |
标识所用协议的 URI 方案。 | %> | 始终 |
host |
string |
接收到请求的本地 HTTP 服务器名称。 | localhost |
始终 |
port |
int |
接收到请求的本地 HTTP 服务器端口。 | 8080 |
如果不是默认值,则添加(80 表示 http 或 443 表示 https) |
Grafana 中的图通常是根据每个唯一维度组合进行分区的。 Grafana 查询中可以使用这些维度来筛选或聚合数据。 例如,如果绘制 current_requests
,将看到基于每个维度组合分区的值。 若要仅基于主机进行筛选,请添加 Sum
操作并使用 host
作为标签值。
9. 使用 Jaeger 进行分布式跟踪
在步骤 6 中,看到分布式跟踪信息正在向控制台公开。 此信息跟踪具有活动的工作单元。 一些活动是由平台自动创建的,例如由 ASP.NET 创建的用于表示请求处理的活动,库和应用代码也可以创建活动。 问候语示例有 Greeter
活动。 使用 TraceId
、SpanId
和 ParentId
标记将活动关联起来。
分布式系统中的每个进程都会产生自己的活动信息流,与指标一样,需要系统来收集、存储和关联活动,以便能够可视化为每个事务所做的工作。 Jaeger 是开放源代码项目,可以实现这种收集和可视化效果。
从 https://www.jaegertracing.io/download/ 下载适用于你的平台的 Jaeger 的最新二进制分发存档。
然后,将下载内容提取到易于访问的本地位置。 运行 jaeger-all-in-one(.exe) 可执行文件:
./jaeger-all-in-one --collector.otlp.enabled
通过控制台输出查找它通过 gRPC 侦听 OTLP 流量的端口。 例如:
{"level":"info","ts":1686963686.3854616,"caller":"otlpreceiver@v0.78.2/otlp.go:83","msg":"Starting GRPC server","endpoint":"0.0.0.0:4317"}
此输出告知它正在侦听 0.0.0.0:4317
,因此可以将该端口配置为 OTLP 导出程序的目标。
打开我们项目的 AppSettings.json
文件,并添加以下行,如果适用,请更改端口。
"OTLP_ENDPOINT_URL" : "http://localhost:4317/"
重新启动 greeter 进程,这样它就可以接收属性更改并开始将跟踪信息定向到 Jaeger。
现在,应该能够从 Web 浏览器中于 http://localhost:16686/
看到 Jaeger UI 。
要查看跟踪列表,请从“服务”下拉列表中选择 OTel-Prometheus-grafana-Jaeger
。 选择跟踪应显示作为该跟踪一部分的活动的甘特图。 单击每个操作可以显示有关活动的更多详细信息。
在分布式系统中,需要将所有进程的跟踪发送到同一个 Jaeger 安装,以便它可以关联整个系统中的事务。
可以让应用对自己进行 HTTP 调用,让它变得更有趣。
将
HttpClient
工厂添加到应用程序builder.Services.AddHttpClient();
添加用于进行嵌套问候语呼叫的新终结点
app.MapGet("/NestedGreeting", SendNestedGreeting);
实现终结点,使其发出也可以跟踪的 HTTP 调用。 在这种情况下,它在人工循环中调用自己(实际上只适用于演示方案)。
async Task SendNestedGreeting(int nestlevel, ILogger<Program> logger, HttpContext context, IHttpClientFactory clientFactory) { // Create a new Activity scoped to the method using var activity = greeterActivitySource.StartActivity("GreeterActivity"); if (nestlevel <= 5) { // Log a message logger.LogInformation("Sending greeting, level {nestlevel}", nestlevel); // Increment the custom counter countGreetings.Add(1); // Add a tag to the Activity activity?.SetTag("nest-level", nestlevel); await context.Response.WriteAsync($"Nested Greeting, level: {nestlevel}\r\n"); if (nestlevel > 0) { var request = context.Request; var url = new Uri($"{request.Scheme}://{request.Host}{request.Path}?nestlevel={nestlevel - 1}"); // Makes an http call passing the activity information as http headers var nestedResult = await clientFactory.CreateClient().GetStringAsync(url); await context.Response.WriteAsync(nestedResult); } } else { // Log a message logger.LogError("Greeting nest level {nestlevel} too high", nestlevel); await context.Response.WriteAsync("Nest level too high, max is 5"); } }
这将为请求生成更有趣的棱锥形状图,因为每个级别都在等待来自上一个调用的响应。