Compartir a través de


Métricas de red en .NET

Las métricas son medidas numéricas que se comunican a lo largo del tiempo. Normalmente se usan para supervisar el estado de una aplicación y generar alertas.

A partir de .NET 8, los componentes System.Net.Http y System.Net.NameResolutionse instrumentan para publicar métricas mediante la nueva API System.Diagnostics.Metrics de .NET. Estas métricas se diseñaron en cooperación con OpenTelemetry para asegurarse de que son coherentes con el estándar y funcionan bien con herramientas populares como Prometheus y Grafana. También son multidimensionales, lo que significa que las medidas están asociadas a pares clave-valor denominados etiquetas (atributos o etiquetas) que permiten clasificar los datos para su análisis.

Sugerencia

Para obtener una lista completa de todos los instrumentos integrados junto con sus atributos, consulte Métricas de System.Net.

Recopilación de métricas de System.Net

El uso de métricas en una aplicación .NET tiene dos partes:

  • Instrumentación: el código de las bibliotecas .NET toma medidas y las asocia a un nombre de métrica. .NET y ASP.NET Core incluyen muchas métricas integradas.
  • Colección: una aplicaciones .NET configura métricas con nombre para que se transmitan desde la aplicación para el almacenamiento y el análisis externos. Algunas herramientas pueden realizar la configuración fuera de la aplicación mediante archivos de configuración o una herramienta de interfaz de usuario.

En esta sección se muestran varios métodos para recopilar y ver las métricas de System.Net.

Ejemplo de aplicación

Para este tutorial, cree una aplicación sencilla que envíe solicitudes HTTP a varios puntos de conexión en paralelo.

dotnet new console -o HelloBuiltinMetrics
cd ..\HelloBuiltinMetrics

Reemplace el contenido de Program.cs por el código de ejemplo siguiente:

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

Visualización de métricas con dotnet-counters

dotnet-counters es una herramienta de supervisión de rendimiento multiplataforma diseñada para la investigación del rendimiento y la supervisión del estado de primer nivel ad hoc.

dotnet tool install --global dotnet-counters

Cuando se ejecuta en un proceso de .NET 8+, dotnet-counters habilita los instrumentos definidos por el argumento --counters y muestra las medidas. Actualiza continuamente la consola con las cifras más recientes:

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

Visualización de métricas en Grafana con OpenTelemetry y Prometheus

Información general

OpenTelemetry:

  • Es un proyecto de código abierto independiente del proveedor administrado por Cloud Native Computing Foundation.
  • Pretende estandarizar la generación y la recopilación de telemetría para software nativo de nube.
  • Funciona con .NET mediante las API de métricas de .NET.
  • Está aprobado por Azure Monitor y muchos proveedores de APM.

Este tutorial muestra una de las integraciones que hay disponibles para las métricas de OpenTelemetry mediante los proyectos de OSS Prometheus y Grafana. El flujo de datos de métricas consta de los pasos siguientes:

  1. Las API de métricas de .NET registran medidas de la aplicación de ejemplo.

  2. La biblioteca de OpenTelemetry que se ejecuta en la aplicación agrega las medidas.

  3. La biblioteca exportadora de Prometheus hace que los datos agregados estén disponibles a través de un punto de conexión de métricas HTTP. "Exportador" es el término que OpenTelemetry usa para denominar a las bibliotecas que transmiten telemetría a back-ends específicos del proveedor.

  4. Un servidor de Prometheus:

    • Sondea el punto de conexión de métricas.
    • Lee los datos.
    • Almacena los datos en una base de datos para la persistencia a largo plazo. Prometheus hace referencia a la lectura y el almacenamiento de datos como extracción de un punto de conexión.
    • Se puede ejecutar en una máquina diferente.
  5. El servidor de Grafana:

    • Consulta los datos almacenados en Prometheus y los muestra en un panel de supervisión basado en web.
    • Se puede ejecutar en una máquina diferente.

Configuración de la aplicación de ejemplo para usar el exportador de Prometheus de OpenTelemetry

Agregue una referencia al exportador de Prometheus de OpenTelemetry en la aplicación de ejemplo:

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

Nota

En este tutorial se usa una compilación de versión previa de la compatibilidad con Prometheus de OpenTelemetry disponible en el momento de la redacción de este contenido.

Actualice Program.cs con la configuración de OpenTelemetry:

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

En el código anterior:

  • AddMeter("System.Net.Http", "System.Net.NameResolution") configura OpenTelemetry para transmitir todas las métricas recopiladas por los medidores integrados System.Net.Http y System.Net.NameResolution.
  • AddPrometheusHttpListener configura OpenTelemetry para exponer el punto de conexión HTTP de métricas de Prometheus en el puerto 9184.

Nota:

Esta configuración difiere para las aplicaciones de ASP.NET Core, donde las métricas se exportan con OpenTelemetry.Exporter.Prometheus.AspNetCore en lugar de HttpListener. Consulte el ejemplo relacionado de ASP.NET Core.

Ejecute la aplicación y déjela en ejecución para que se puedan recopilar medidas:

dotnet run

Configuración de Prometheus

Siga los primeros pasos de Prometheus para configurar un servidor de Prometheus y confirmar que funciona.

Modifique el archivo de configuración prometheus.yml para que Prometheus extraiga el punto de conexión de métricas que se expone en la aplicación de ejemplo. Agregue el siguiente texto resaltado en la sección 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']

Inicio de Prometheus

  1. Vuelva a cargar la configuración o reinicie el servidor de Prometheus.

  2. Confirme que OpenTelemetryTest se encuentre en el estado UP en la página Estado>Objetivos del portal web de Prometheus. Prometheus status

  3. En la página de Graph del portal web de Prometheus, escriba http en el cuadro de texto de expresión y seleccione http_client_active_requests. http_client_active_requests En la pestaña gráfico, Prometheus muestra el valor del contador http.client.active_requests emitido por la aplicación de ejemplo. Prometheus active requests graph

Visualización de métricas en un panel de Grafana

  1. Siga las instrucciones estándar para instalar Grafana y conectarlo a un origen de datos de Prometheus.

  2. Para crear un panel de Grafana, seleccione el icono + de la barra de herramientas superior y, a continuación, seleccione Panel. En el editor del panel que aparece, escriba Abrir conexiones HTTP/1.1 en el cuadro Título y la siguiente consulta en el campo de expresión PromQL:

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

Grafana HTTP/1.1 Connections

  1. Seleccione Aplicar para guardar y ver el nuevo panel. Muestra el número de conexiones HTTP/1.1 activas frente a inactivas en el grupo.

Enriquecimiento

El enriquecimiento es la adición de etiquetas personalizadas (es decir, atributos o etiquetas) a una métrica. Esto resulta útil si una aplicación quiere agregar una categorización personalizada a paneles o alertas compiladas con métricas. El instrumento http.client.request.duration admite el enriquecimiento mediante el registro de devoluciones de llamada con HttpMetricsEnrichmentContext. Tenga en cuenta que se trata de una API de bajo nivel y se necesita un registro de devolución de llamada independiente para cada HttpRequestMessage.

Una manera sencilla de realizar el registro de devolución de llamada en un solo lugar es implementar DelegatingHandler personalizado. Esto le permitirá interceptar y modificar las solicitudes antes de que se reenvíen al controlador interno y se envíen al servidor:

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

Si está trabajando con IHttpClientFactory, puede usar AddHttpMessageHandler para registrar 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");

Nota:

Por motivos de rendimiento, la devolución de llamada de enriquecimiento solo se invoca cuando el instrumento http.client.request.duration está habilitado, lo que significa que algo debe recopilar las métricas. Puede ser dotnet-monitor, el exportador de Prometheus, MeterListener o MetricCollector<T>.

Integración de IMeterFactory y IHttpClientFactory

Las métricas HTTP se diseñaron teniendo en cuenta el aislamiento y la capacidad de prueba. Estos aspectos son compatibles con el uso de IMeterFactory, lo que permite publicar métricas por una instancia personalizada Meter con el fin de mantener los medidores aislados entre sí. De forma predeterminada, todas las métricas las emite un Meter interno global a la biblioteca System.Net.Http. Este comportamiento se puede invalidar asignando una instancia personalizada IMeterFactory a SocketsHttpHandler.MeterFactory o HttpClientHandler.MeterFactory.

Nota:

Meter.Name es System.Net.Http para todas las métricas emitidas por HttpClientHandler y SocketsHttpHandler.

Al trabajar con Microsoft.Extensions.Http y IHttpClientFactory en .NET 8+, la implementación predeterminada IHttpClientFactory selecciona automáticamente la instancia IMeterFactory registrada en IServiceCollection y la asigna al controlador principal que crea internamente.

Nota:

A partir de .NET 8, el método AddHttpClient llama a AddMetrics automáticamente para inicializar los servicios de métricas y registrar la implementación predeterminada IMeterFactory con IServiceCollection. El IMeterFactory predeterminado almacena en caché las instancias Meter por nombre, lo que significa que habrá Meter con el nombre System.Net.Http por IServiceCollection.

Métricas de prueba

En el ejemplo siguiente se muestra cómo validar las métricas integradas en pruebas unitarias mediante xUnit, IHttpClientFactory y MetricCollector<T> desde el paquete NuGet Microsoft.Extensions.Diagnostics.Testing:

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

Métricas frente a EventCounters

Las métricas cuentan con más características que EventCounters, especialmente debido a su naturaleza multidimensional. Esta multidimensionalidad le permite crear consultas sofisticadas en herramientas como Prometheus y obtener información de un nivel que no es posible con EventCounters.

Sin embargo, a partir de .NET 8, solo los componentes System.Net.Http y System.Net.NameResolutions se instrumentan mediante Métricas, lo que significa que si necesita contadores de los niveles inferiores de la pila, como System.Net.Sockets o System.Net.Security, debe usar EventCounters.

Además, hay algunas diferencias semánticas entre las métricas y sus EventCounters coincidentes. Por ejemplo, cuando se usa HttpCompletionOption.ResponseContentRead, el EventCounter current-requests considera que una solicitud está activa hasta el momento en que se ha leído el último byte del cuerpo de la solicitud. Su homólogo de métricas http.client.active_requests no incluye el tiempo dedicado a leer el cuerpo de la respuesta al contar las solicitudes activas.

¿Necesita más métricas?

Si tiene alguna sugerencia relativa a otra información de utilidad que podría exponerse a través de métricas, cree una incidencia de dotnet/runtime.