.NET 中的网络指标

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

从 .NET 8 开始,将对 System.Net.HttpSystem.Net.NameResolution 组件启用监测,以便使用 .NET 的新 System.Diagnostics.Metrics API发布指标。 这些指标是与 OpenTelemetry 合作设计的,以确保它们与标准一致,并能与常用工具(如 PrometheusGrafana)良好配合。 它们也是多维的,这意味着度量值与称为标记(也称为属性或标签)的键值对相关联。 标记允许对度量进行分类以帮助分析。

提示

有关所有内置工具及其属性的综合列表,请参阅 System.Net 指标

收集 System.Net 指标

若要利用内置指标检测,需要将 .NET 应用配置为收集这些指标。 这通常意味着将它们转换为外部存储和分析,例如,转换为监视系统。

可通过多种方式在 .NET 中收集网络指标。

使用 dotnet-counters 收集指标

dotnet-counters 是一种跨平台命令行工具,用于临时检查 .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)];
        try
        {
            byte[] bytes = await client.GetByteArrayAsync(uri, ct);
            await Console.Out.WriteLineAsync($"{uri} - received {bytes.Length} bytes.");
        }
        catch { await Console.Out.WriteLineAsync($"{uri} - failed."); }
    });
}

确保已安装 dotnet-counters

dotnet tool install --global dotnet-counters

启动 HelloBuiltinMetrics 应用。

dotnet run -c Release

在单独的 CLI 窗口中启动 dotnet-counters 并指定要监视的进程名称和计量,然后按 HelloBuiltinMetrics 应用中的键,以便开始发送请求。 测量数据一传入,dotnet-counters 就使用最新数据连续刷新控制台:

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

dotnet-counters 输出

使用 .NET Aspire 收集指标

在 ASP.NET 应用程序中收集跟踪和指标的一种简单方法是使用 .NET Aspire。 .NET Aspire 是 .NET 的一组扩展,便于创建和使用分布式应用程序。 使用 .NET Aspire 的好处之一是,遥测是内置的,使用适用于 .NET 的 OpenTelemetry 库。

.NET Aspire 的默认项目模板包含 ServiceDefaults 项目。 .NET Aspire 解决方案中的每个服务都有对服务默认值项目的引用。 服务使用它来设置和配置 OTel。

服务默认值项目模板包括 OTel SDK、ASP.NET、HttpClient 和运行时检测包。 这些检测组件在 Extensions.cs 文件中配置。 为了支持 Aspire 仪表板中的遥测可视化,服务默认值项目还默认包含 OTLP 导出程序。

Aspire 仪表板旨在将遥测观察引入本地调试周期,使开发人员能够确保应用程序生成遥测数据。 遥测可视化还有助于在本地诊断这些应用程序。 能够查看服务之间的调用在调试时与在生产中一样有用。 F5 Visual Studio 中的 AppHost 项目或从命令行 dotnet run AppHost 项目时,将自动启动 .NET Aspire 仪表板。

快速指南

  1. 使用 dotnet new创建 .NET Aspire 9 入门应用

    dotnet new aspire-starter-9 --output AspireDemo
    

    或在 Visual Studio 中,创建新项目并选择 .NET Aspire 9 初学者应用 模板:

    在 Visual Studio 中创建 .NET Aspire 9 初学者应用

  2. ServiceDefaults 项目中打开 Extensions.cs,然后滚动到 ConfigureOpenTelemetry 方法。 请注意订阅网络计量的 AddHttpClientInstrumentation() 调用。

    .WithMetrics(metrics =>
    {
        metrics.AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddRuntimeInstrumentation();
    })
    

    请注意,在 .NET 8+ 上,AddHttpClientInstrumentation() 可以替换为手动计量订阅:

    .WithMetrics(metrics =>
    {
        metrics.AddAspNetCoreInstrumentation()
            .AddMeter("System.Net.Http")
            .AddMeter("System.Net.NameResolution")
            .AddRuntimeInstrumentation();
    })
    
  3. 运行 AppHost 项目。 这应启动 Aspire 仪表板。

  4. 导航到 webfrontend 应用的“天气”页,以生成一个指向 apiserviceHttpClient 请求。 多次刷新页面以发送多个请求。

  5. 返回到仪表板,导航到 指标 页,然后选择 webfrontend 资源。 向下滚动后,应该能够浏览内置 System.Net 指标。

    Aspire 仪表板中的网络指标

有关 .NET Aspire 的详细信息,请参阅:

在不使用 .NET Aspire 编排的情况下复用服务默认值项目

Aspire Service Defaults 项目提供了一种简单的方式来为 ASP.NET 项目配置 OTel,即使不使用 .NET Aspire 的其余部分(例如用于业务流程的 AppHost)。 服务默认值项目通过 Visual Studio 或 dotnet new作为项目模板提供。 它配置 OTel 并设置 OTLP 导出程序。 然后,可以使用 OTel 环境变量 将 OTLP 终结点配置为向其发送遥测数据,并为应用程序提供资源属性。

使用 ServiceDefaults 在 .NET Aspire 外部的步骤如下:

  1. 使用 Visual Studio 中的“添加新项目”将 ServiceDefaults 项目添加到解决方案,或使用 dotnet new

    dotnet new aspire-servicedefaults --output ServiceDefaults
    
  2. 从 ASP.NET 应用程序中引用 ServiceDefaults 项目。 在 Visual Studio 中,选择 添加>项目引用 并选择 ServiceDefaults 项目”

  3. 调用 OpenTelemetry 安装程序函数 ConfigureOpenTelemetry() 作为应用程序生成器初始化的一部分。

    var builder = WebApplication.CreateBuilder(args)
    builder.ConfigureOpenTelemetry(); // Extension method from ServiceDefaults.
    var app = builder.Build();
    app.MapGet("/", () => "Hello World!");
    app.Run();
    

有关详细指南,请参阅 示例:将 OpenTelemetry 与 OTLP 和独立的 Aspire 仪表板配合使用。

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

若要了解如何将示例应用与 Prometheus 和 Grafana 连接,请按照将 OpenTelemetry 与 Prometheus、Grafana 和 Jaeger 结合使用中的演练进行。

若要通过向各种终结点发送并行请求来强调 HttpClient,请使用以下终结点扩展示例应用:

app.MapGet("/ClientStress", async Task<string> (ILogger<Program> logger, HttpClient client) =>
{
    string[] uris = ["http://example.com", "http://httpbin.org/get", "https://example.com", "https://httpbin.org/get"];
    await Parallel.ForAsync(0, 50, async (_, ct) =>
    {
        string uri = uris[Random.Shared.Next(uris.Length)];

        try
        {
            await client.GetAsync(uri, ct);
            logger.LogInformation($"{uri} - done.");
        }
        catch { logger.LogInformation($"{uri} - failed."); }
    });
    return "Sent 50 requests to example.com and httpbin.org.";
});

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

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

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

在 Grafana 中的 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 Exporter、MeterListenerMetricCollector<T>

IMeterFactoryIHttpClientFactory 集成

HTTP 指标设计时考虑到了隔离性和可测试性。 使用 IMeterFactory 就能够支持这些方面,这样就可以通过自定义 Meter 实例发布指标,以使计量彼此隔离。 默认情况下,全局标识符 Meter 用于发出所有指标。 此 MeterSystem.Net.Http 库的内部部分。 通过将自定义 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

测试指标

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

[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 组件,这意味着,如果需要堆栈较低级别的计数器(如 System.Net.SocketsSystem.Net.Security),则必须使用 EventCounters。

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

需要更多的指标?

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