示例:将 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 获取指标和活动。 它还分别注册 MetricsActivitySource 提供程序,用于指标和跟踪。

代码为指标使用 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 中注销日志,有几个选项:

  • stdoutstderr 输出由容器系统(如 Kubernetes)重定向到日志文件。
  • 使用将与 ILogger 集成的日志记录库,这些库包括 SerilogNLog
  • 使用 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 在收到请求时,如果不存在,则会分配 TraceIdHttpClient 在出站请求中默认包含标头。 每个活动都有 SpanId,它是 TraceIdSpanId 的组合,唯一标识每个活动。 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 中查询指标。 使用下图中突出显示的按钮打开指标资源管理器,其中显示了所有可用的指标。

Prometheus 指标资源管理器

选择 greetings_count 指标以查看值图。

greetings_count 图

8. 使用 Grafana 创建指标仪表板

Grafana 是仪表板产品,可以基于 Prometheus 或其他数据源创建仪表板和警报。

按照平台的说明,从 https://grafana.com/oss/grafana/ 下载并安装 Grafana 的 OSS 版本。 安装后,Grafana 通常在端口 3000 上运行,因此请在浏览器中打开 http://localhost:3000。 需要登录;默认的用户名和密码都是 admin

从汉堡菜单中选择连接,然后输入文本 prometheus 以选择终结点类型。 选择“创建 Prometheus 数据源”以添加新的数据源。

Grafana 与 prometheus 的连接

需要设置以下属性:

  • Prometheus 服务器 URL:http://localhost:9090/ 根据需要更改端口

选择“保存并测试”以验证配置。

收到成功消息后,就可以配置仪表板。 单击弹出项中显示的“生成仪表板”链接以获取成功消息。

选择“添加可视化效果”,然后选择刚刚添加的 Prometheus 数据源作为数据源。

应显示仪表板面板设计器。 在屏幕的下半部分,可以定义查询。

使用 greetings_count 的 Grafana 查询

选择 greetings_count 指标,然后选择“运行查询”以查看结果。

使用 Grafana,可以设计复杂的仪表板来跟踪任何数量的指标。

.NET 中的每个指标都可以有额外的维度,这些维度是可用于对数据进行分区的键值对。 ASP.NET 指标都具有适用于计数器的多个维度。 例如,来自 Microsoft.AspNetCore.Hostingcurrent-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 作为标签值。

Grafana current_requests(按主机)

9. 使用 Jaeger 进行分布式跟踪

步骤 6 中,看到分布式跟踪信息正在向控制台公开。 此信息跟踪具有活动的工作单元。 一些活动是由平台自动创建的,例如由 ASP.NET 创建的用于表示请求处理的活动,库和应用代码也可以创建活动。 问候语示例有 Greeter 活动。 使用 TraceIdSpanIdParentId 标记将活动关联起来。

分布式系统中的每个进程都会产生自己的活动信息流,与指标一样,需要系统来收集、存储和关联活动,以便能够可视化为每个事务所做的工作。 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 。

Jaeger 查询跟踪

要查看跟踪列表,请从“服务”下拉列表中选择 OTel-Prometheus-grafana-Jaeger。 选择跟踪应显示作为该跟踪一部分的活动的甘特图。 单击每个操作可以显示有关活动的更多详细信息。

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");
        }
    }
    

这将为请求生成更有趣的棱锥形状图,因为每个级别都在等待来自上一个调用的响应。

Jaeger 嵌套依赖项结果