.NET 中的网络指标

指标是一段时间内报告的数字度量值。 它们通常用于监视应用的运行状况并生成警报。

从 .NET 8 开始,System.Net.HttpSystem.Net.NameResolution 组件在经过检测后可以使用 .NET 的新 System.Diagnostics.Metrics API 来发布指标。 这些指标是与 OpenTelemetry 合作设计的,目的是确保它们符合标准,并且能够与 PrometheusGrafana 等常用工具良好配合。 它们也是多维的,这意味着度量与称为标记(也称为属性或标签)的键值对相关联,因此可以对数据分类以进行分析。

提示

有关所有内置检测及其属性的完整列表,请参阅 System.Net 指标

收集 System.Net 指标

在 .NET 应用中使用指标涉及两个部分:

  • 检测: .NET 库中的代码采用度量值,并将这些度量值与指标名称关联起来。 .NET 和 ASP.NET Core 包括许多内置指标。
  • 收集: 由一个 .NET 应用来配置要从应用传输的命名指标以用于外部存储和分析。 某些工具可能会使用配置文件或 UI 工具在应用外部执行配置。

本部分演示了收集和查看 System.Net 指标的各种方法。

示例应用

在本教程中,请创建一个简单的应用,以便将 HTTP 请求并行发送到各个终结点。

dotnet new console -o HelloBuiltinMetrics
cd ..\HelloBuiltinMetrics

Program.cs 的内容替换为以下示例代码:

using System.Net;

string[] uris = ["http://example.com", "http://httpbin.org/get", "https://example.com", "https://httpbin.org/get"];
using HttpClient client = new()
{
    DefaultRequestVersion = HttpVersion.Version20
};

Console.WriteLine("Press any key to start.");
Console.ReadKey();

while (!Console.KeyAvailable)
{
    await Parallel.ForAsync(0, Random.Shared.Next(20), async (_, ct) =>
    {
        string uri = uris[Random.Shared.Next(uris.Length)];
        byte[] bytes = await client.GetByteArrayAsync(uri, ct);
        await Console.Out.WriteLineAsync($"{uri} - received {bytes.Length} bytes.");
    });
}

使用 dotnet-counters 查看指标

dotnet-counters 是跨平台性能监视工具,用于临时运行状况监视和初级性能调查。

dotnet tool install --global dotnet-counters

针对 .NET 8+ 进程运行时,dotnet-counters 启用由 --counters 参数定义的检测并显示度量。 它会持续使用最新数字刷新控制台:

dotnet-counters monitor --counters System.Net.Http,System.Net.NameResolution -n HelloBuiltinMetrics

使用 OpenTelemetry 和 Prometheus 查看 Grafana 中的指标

概述

OpenTelemetry

  • 是一个由云原生计算基金会支持的供应商中立开源项目。
  • 标准化云原生软件的遥测数据生成和收集。
  • 使用 .NET 指标 API 与 .NET 配合使用。
  • 得到 Azure Monitor 和许多 APM 供应商的认可。

本教程使用 OSS PrometheusGrafana 项目展示了可用于 OpenTelemetry 指标的集成之一。 指标数据流包含以下步骤:

  1. .NET 指标 API 记录示例应用中的度量值。

  2. 在应用中运行的 OpenTelemetry 库将聚合这些度量值。

  3. Prometheus 导出程序库通过 HTTP 指标终结点提供聚合数据。 “导出程序”指的是 OpenTelemetry 调用库来将遥测数据传输到供应商特定的后端。

  4. Prometheus 服务器:

    • 轮询指标终结点。
    • 读取数据。
    • 将数据存储在数据库中以实现长期持久存储。 Prometheus 将读取和存储数据称为抓取终结点。
    • 可以在其他计算机上运行。
  5. Grafana 服务器:

    • 查询 Prometheus 中存储的数据并将其显示在基于 Web 的监控仪表板上。
    • 可以在其他计算机上运行。

将示例应用配置为使用 OpenTelemetry 的 Prometheus 导出程序

向示例应用添加对 OpenTelemetry Prometheus 导出程序的引用:

dotnet add package OpenTelemetry.Exporter.Prometheus.HttpListener --prerelease

注意

本教程使用编写本文时 OpenTelemetry 的 Prometheus 支持的预发布版本。

使用 OpenTelemetry 配置更新 Program.cs

using OpenTelemetry.Metrics;
using OpenTelemetry;
using System.Net;

using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder()
    .AddMeter("System.Net.Http", "System.Net.NameResolution")
    .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { "http://localhost:9184/" })
    .Build();

string[] uris = ["http://example.com", "http://httpbin.org/get", "https://example.com", "https://httpbin.org/get"];
using HttpClient client = new()
{
    DefaultRequestVersion = HttpVersion.Version20
};

while (!Console.KeyAvailable)
{
    await Parallel.ForAsync(0, Random.Shared.Next(20), async (_, ct) =>
    {
        string uri = uris[Random.Shared.Next(uris.Length)];
        byte[] bytes = await client.GetByteArrayAsync(uri, ct);
        await Console.Out.WriteLineAsync($"{uri} - received {bytes.Length} bytes.");
    });
}

在上述代码中:

  • AddMeter("System.Net.Http", "System.Net.NameResolution") 将 OpenTelemetry 配置为传输内置 System.Net.HttpSystem.Net.NameResolution 计量收集的所有指标。
  • AddPrometheusHttpListener 配置 OpenTelemetry 以在端口 9184 上公开 Prometheus 的指标 HTTP 终结点。

注意

ASP.NET Core 应用的此配置有所不同,其中的指标是使用 OpenTelemetry.Exporter.Prometheus.AspNetCore 而不是 HttpListener 导出的。 请参阅相关的 ASP.NET Core 示例

运行应用并使其保持运行状态,以便可以收集度量值:

dotnet run

设置和配置 Prometheus

按照 Prometheus 起始步骤设置 Prometheus 服务器并确认其正常工作。

修改 prometheus.yml 配置文件,以便 Prometheus 抓取示例应用公开的指标终结点。 在 scrape_configs 部分中添加以下突出显示的文本:

# my global config
global:
  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
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'.

    static_configs:
      - targets: ["localhost:9090"]

  - job_name: 'OpenTelemetryTest'
    scrape_interval: 1s # poll very quickly for a more responsive demo
    static_configs:
      - targets: ['localhost:9184']

启动 prometheus

  1. 重新加载配置或重启 Prometheus 服务器。

  2. 确认 OpenTelemetryTest 在 Prometheus Web 门户的“状态”>“目标”页中处于 UP 状态。 Prometheus status

  3. 在 Prometheus Web 门户“Graph”页面上的表达式文本框中输入 http,然后选择 http_client_active_requestshttp_client_active_requests 在图选项卡中,Prometheus 显示示例应用发出的 http.client.active_requests 计数器的值。 Prometheus active requests graph

在 Grafana 仪表板上显示指标

  1. 按照标准说明安装 Grafana,并将其连接到 Prometheus 数据源。

  2. 创建 Grafana 仪表板,其方式是:先选择顶部工具栏上的 + 图标,然后选择“仪表板”。 在出现的仪表板编辑器中,在“标题”框中输入“打开 HTTP/1.1 连接”,并在 PromQL 表达式字段中输入以下查询:

sum by(http_connection_state) (http_client_open_connections{network_protocol_version="1.1"})

Grafana HTTP/1.1 Connections

  1. 选择“应用”保存并查看新仪表板。 它显示池中的活动和空闲 HTTP/1.1 连接的数量。

扩充

扩充是向指标添加自定义标记(也称为属性或标签)。 如果应用要向使用指标生成的仪表板或警报添加自定义分类,这非常有用。 http.client.request.duration 检测通过向 HttpMetricsEnrichmentContext 注册回调来支持扩充。 请注意,这是一个低级 API,每个 HttpRequestMessage 都需要一个单独的回调注册。

若要在单个位置进行回调注册,一种简单的方法是实现自定义 DelegatingHandler。 这样就可以在请求转发到内部处理程序并发送到服务器之前截获和修改请求:

using System.Net.Http.Metrics;

using HttpClient client = new(new EnrichmentHandler() { InnerHandler = new HttpClientHandler() });

await client.GetStringAsync("https://httpbin.org/response-headers?Enrichment-Value=A");
await client.GetStringAsync("https://httpbin.org/response-headers?Enrichment-Value=B");

sealed class EnrichmentHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpMetricsEnrichmentContext.AddCallback(request, static context =>
        {
            if (context.Response is not null) // Response is null when an exception occurs.
            {
                // Use any information available on the request or the response to emit custom tags.
                string? value = context.Response.Headers.GetValues("Enrichment-Value").FirstOrDefault();
                if (value != null)
                {
                    context.AddCustomTag("enrichment_value", value);
                }
            }
        });
        return base.SendAsync(request, cancellationToken);
    }
}

如果你使用 IHttpClientFactory,则可以使用 AddHttpMessageHandler 来注册 EnrichmentHandler

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Net.Http.Metrics;

ServiceCollection services = new();
services.AddHttpClient(Options.DefaultName).AddHttpMessageHandler(() => new EnrichmentHandler());

ServiceProvider serviceProvider = services.BuildServiceProvider();
HttpClient client = serviceProvider.GetRequiredService<HttpClient>();

await client.GetStringAsync("https://httpbin.org/response-headers?Enrichment-Value=A");
await client.GetStringAsync("https://httpbin.org/response-headers?Enrichment-Value=B");

注意

出于性能原因,仅在启用 http.client.request.duration 检测时才调用扩充回调,这意味着应该有某些东西正在收集指标。 它可能是 dotnet-monitor、Prometheus 导出程序、MeterListenerMetricCollector<T>

IMeterFactoryIHttpClientFactory 集成

HTTP 指标在设计时考虑到了隔离和可测试性。 使用 IMeterFactory 就能够支持这些方面,这样就可以通过自定义 Meter 实例发布指标,以使计量彼此隔离。 默认情况下,所有指标均由 System.Net.Http 库内部的全局 Meter 发出。 可以通过将自定义 IMeterFactory 实例分配给 SocketsHttpHandler.MeterFactoryHttpClientHandler.MeterFactory 来替代此行为。

注意

对于 HttpClientHandlerSocketsHttpHandler 发出的所有指标,Meter.NameSystem.Net.Http

在 .NET 8+ 上使用 Microsoft.Extensions.HttpIHttpClientFactory 时,默认的 IHttpClientFactory 实现会自动选取在 IServiceCollection 中注册的 IMeterFactory 实例,并将其分配给它在内部创建的主处理程序。

注意

从 .NET 8 开始,AddHttpClient 方法自动调用 AddMetrics 来初始化指标服务并向 IServiceCollection 注册默认的 IMeterFactory 实现。 默认的 IMeterFactory 按名称缓存 Meter 实例,这意味着每个 IServiceCollection 都会有一个名称为 System.Net.HttpMeter

测试指标

以下示例演示如何使用 Microsoft.Extensions.Diagnostics.Testing NuGet 包中的 xUnit、IHttpClientFactoryMetricCollector<T> 验证单元测试中的内置指标:

[Fact]
public async Task RequestDurationTest()
{
    // Arrange
    ServiceCollection services = new();
    services.AddHttpClient();
    ServiceProvider serviceProvider = services.BuildServiceProvider();
    var meterFactory = serviceProvider.GetService<IMeterFactory>();
    var collector = new MetricCollector<double>(meterFactory,
        "System.Net.Http", "http.client.request.duration");
    var client = serviceProvider.GetRequiredService<HttpClient>();

    // Act
    await client.GetStringAsync("http://example.com");

    // Assert
    await collector.WaitForMeasurementsAsync(minCount: 1).WaitAsync(TimeSpan.FromSeconds(5));
    Assert.Collection(collector.GetMeasurementSnapshot(),
        measurement =>
        {
            Assert.Equal("http", measurement.Tags["url.scheme"]);
            Assert.Equal("GET", measurement.Tags["http.request.method"]);
        });
}

Metrics 与EventCounters

Metrics 比 EventCounters 功能更丰富,最明显的是因为它们具有多维性质。 这种多维性使你可以在 Prometheus 之类的工具中创建复杂的查询,并获得 EventCounters 无法达到的级别的见解。

不过,从 .NET 8 开始,只有 System.Net.HttpSystem.Net.NameResolutions 组件使用 Metrics 进行检测,这意味着,如果你需要来自较低级别堆栈(例如 System.Net.SocketsSystem.Net.Security)的计数器,则必须使用 EventCounters。

此外,Metrics 与其匹配的 EventCounters 之间存在一些语义差异。 例如,使用 HttpCompletionOption.ResponseContentRead 时,current-requests EventCounter 会认为请求处于活动状态,直到读取请求正文的最后一个字节为止。 其指标对应项 http.client.active_requests 不包括在计算活动请求数时读取响应正文所花的时间。

需要更多的指标?

如果对可以通过指标公开的其他有用信息有建议,请创建 dotnet/运行时问题