Condividi tramite


Metriche di rete in .NET

Le metriche sono misurazioni numeriche segnalate nel tempo. Vengono in genere usate per monitorare l'integrità di un'app e generare avvisi.

A partire da .NET 8, i componenti System.Net.Http e System.Net.NameResolution vengono instrumentati per pubblicare le metriche usando la nuova API System.Diagnostics.Metrics di NET. Queste metriche sono state progettate in collaborazione con OpenTelemetry per assicurarsi che siano coerenti con lo standard e funzionino bene con gli strumenti più diffusi come Prometheus e Grafana. Sono anche multidimensionali, ovvero le misurazioni sono associate a coppie chiave-valore denominate tag (attributi o etichette) che consentono di classificare i dati per l'analisi.

Suggerimento

Per un elenco completo di tutti gli strumenti predefiniti insieme ai relativi attributi, vedere le Metriche System.Net.

Raccogliere metriche di System.Net

Esistono due parti per l'uso delle metriche in un'app .NET:

  • Strumentazione: il codice nelle librerie .NET accetta misure e associa queste misurazioni a un nome di metrica. .NET e ASP.NET Core includono molte metriche predefinite.
  • Raccolta: un'app .NET configura le metriche denominate da trasmettere dall'app per l'archiviazione e l'analisi esterne. Alcuni strumenti possono eseguire la configurazione all'esterno dell'app usando file di configurazione o uno strumento dell'interfaccia utente.

Questa sezione illustra vari metodi per raccogliere e visualizzare metriche System.Net.

App di esempio

Ai fini di questa esercitazione, creare una semplice app che invia richieste HTTP a vari endpoint in parallelo.

dotnet new console -o HelloBuiltinMetrics
cd ..\HelloBuiltinMetrics

Sostituire il contenuto di Program.cs con il codice di esempio seguente:

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

Visualizzare le metriche con dotnet-counters

dotnet-counters è uno strumento di monitoraggio delle prestazioni multipiattaforma per il monitoraggio ad hoc dello stato e l'analisi delle prestazioni di primo livello.

dotnet tool install --global dotnet-counters

Quando si esegue su un processo .NET 8+, dotnet-counters abilita gli strumenti definiti dall'argomento --counters e visualizza le misurazioni. Aggiorna continuamente la console con i numeri più recenti:

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

Visualizzare le metriche in Grafana con OpenTelemetry e Prometheus

Panoramica

OpenTelemetry:

  • Progetto open source indipendente dal fornitore supportato da Cloud Native Computing Foundation.
  • Standardizza la generazione e la raccolta dei dati di telemetria per il software nativo del cloud.
  • Funziona con .NET usando le API delle metriche .NET.
  • È approvato da Monitoraggio di Azure e da molti fornitori di APM.

Questa esercitazione illustra una delle integrazioni disponibili per le metriche OpenTelemetry usando i progetti OSS Prometheus e Grafana. Il flusso di lavoro delle metriche è costituito dai passaggi seguenti:

  1. Le API delle metriche .NET registrano le misurazioni dall'app di esempio.

  2. La libreria OpenTelemetry in esecuzione nell'app aggrega le misurazioni.

  3. La libreria di esportazione Prometheus rende disponibili i dati aggregati tramite un endpoint delle metriche HTTP. 'Exporter' è ciò che OpenTelemetry chiama le librerie che trasmettono dati di telemetria a back-end specifici del fornitore.

  4. Un server Prometheus:

    • Esegue il polling dell'endpoint delle metriche.
    • Legge i dati.
    • Archivia i dati in un database per la persistenza a lungo termine. Prometheus si riferisce alla lettura e all'archiviazione dei dati come scraping di un endpoint.
    • Può essere eseguito in un computer diverso.
  5. Server Grafana:

    • Esegue una query sui dati archiviati in Prometheus e li visualizza in una dashboard di monitoraggio basata sul Web.
    • Può essere eseguito in un computer diverso.

Configurare l'app di esempio per l'uso dell'utilità di esportazione Prometheus di OpenTelemetry

Aggiungere un riferimento all'utilità di esportazione Prometheus OpenTelemetry all'app di esempio:

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

Nota

Questa esercitazione usa una build non definitiva del supporto Prometheus di OpenTelemetry disponibile al momento della scrittura.

Aggiornare Program.cs con la configurazione di 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.");
    });
}

Nel codice precedente:

  • AddMeter("System.Net.Http", "System.Net.NameResolution") configura OpenTelemetry per trasmettere tutte le metriche raccolte dai contatori System.Net.Http e System.Net.NameResolution predefiniti.
  • AddPrometheusHttpListener configura OpenTelemetry per esporre l'endpoint HTTP delle metriche di Prometheus sulla porta 9184.

Nota

Questa configurazione è diversa per le app ASP.NET Core, in cui le metriche vengono esportate con OpenTelemetry.Exporter.Prometheus.AspNetCore invece di HttpListener. Vedere l'esempio correlato ASP.NET Core.

Eseguire l'app e lasciarla in esecuzione in modo che le misurazioni possano essere raccolte:

dotnet run

Impostare e configurare Prometheus

Seguire i primi passaggi di Prometheus per configurare un server Prometheus e verificare che funzioni.

Modificare il file di configurazione prometheus.yml in modo che Prometheus esemplifichi l'endpoint delle metriche esposto dall'app di esempio. Aggiungere il testo evidenziato seguente nella sezione 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']

Avviare Prometheus

  1. Ricaricare la configurazione o riavviare il server Prometheus.

  2. Verificare che OpenTelemetryTest sia nello stato UP nella pagina Stato> Destinazioni del portale Web Prometheus. Prometheus status

  3. Nella pagina Grafo del portale Web di Prometheus immettere http nella casella di testo dell'espressione e selezionare http_client_active_requests. http_client_active_requests Nella scheda del grafo Prometheus mostra il valore del contatore http.client.active_requests generato dall'app di esempio. Prometheus active requests graph

Visualizzare le metriche in una dashboard di Grafana

  1. Seguire le istruzioni standard per installare Grafana e connetterlo a un'origine dati Prometheus.

  2. Creare un dashboard di Grafana selezionando l'icona + sulla barra degli strumenti superiore e quindi selezionando Dashboard. Nell'editor del dashboard visualizzato immettere Apri connessioni HTTP/1.1 nella casella Titolo e la query seguente nel campo espressione PromQL:

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

Grafana HTTP/1.1 Connections

  1. Selezionare Applica per salvare e visualizzare la nuova dashboard. Visualizza il numero di connessioni HTTP/1.1 attive e inattive nel pool.

Arricchimento

L'arricchimento è l'aggiunta di tag personalizzati (ad esempio attributi o etichette) a una metrica. Ciò è utile se un'app vuole aggiungere una categorizzazione personalizzata ai dashboard o agli avvisi compilati con le metriche. Lo strumento http.client.request.duration supporta l'arricchimento registrando i callback con HttpMetricsEnrichmentContext. Si noti che si tratta di un'API di basso livello e una registrazione di callback separata è necessaria per ogni HttpRequestMessage.

Un modo semplice per eseguire la registrazione di callback in un'unica posizione consiste nell'implementare un oggetto personalizzato DelegatingHandler. In questo modo sarà possibile intercettare e modificare le richieste prima che vengano inoltrate al gestore interno e inviate al server:

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

Se si usa IHttpClientFactory, è possibile usare AddHttpMessageHandler per registrare 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

Per motivi di prestazioni, il callback di arricchimento viene richiamato solo quando lo strumentohttp.client.request.duration è abilitato, vale a dire che qualcosa deve raccogliere le metriche. Può essere dotnet-monitor, l'utilità di esportazione Prometheus, un MeterListener o un MetricCollector<T>.

Integrazione IMeterFactory e IHttpClientFactory

Le metriche HTTP sono state progettate tenendo conto dell'isolamento e della possibilità di test. Questi aspetti sono supportati dall'uso di IMeterFactory, che consente la pubblicazione di metriche da un'istanza Meter personalizzata per mantenere i contatori isolati l'uno dall'altro. Per impostazione predefinita, tutte le metriche vengono generate da un Meter interno globale alla libreria System.Net.Http. Questo comportamento può essere sottoposto a override assegnando un'istanza personalizzata IMeterFactory a SocketsHttpHandler.MeterFactory o HttpClientHandler.MeterFactory.

Nota

Meter.Name è System.Net.Http per tutte le metriche generate da HttpClientHandler e SocketsHttpHandler.

Quando si lavora con Microsoft.Extensions.Http e IHttpClientFactory in .NET 8+, l'implementazione predefinita IHttpClientFactory seleziona automaticamente l'istanza IMeterFactory registrata in IServiceCollection e la assegna al gestore primario che crea internamente.

Nota

A partire da .NET 8, il metodo AddHttpClient chiama automaticamente AddMetrics per inizializzare i servizi delle metriche e registrare l'implementazione IMeterFactory predefinita con IServiceCollection. IMeterFactory predefinito memorizza nella cache le istanze Meter in base al nome, vale a dire che ne sarà presente una Meter con il nome System.Net.Http per IServiceCollection.

Metriche di test

L'esempio seguente illustra come convalidare le metriche predefinite negli unit test usando xUnit, IHttpClientFactory e MetricCollector<T> dal pacchetto 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"]);
        });
}

Metrica ed EventCounters

Le metriche sono più ricche di funzionalità rispetto a EventCounters, in particolare grazie alla loro natura multidimensionale. Questa multidimensionalità consente di creare query sofisticate in strumenti come Prometheus e ottenere informazioni dettagliate su un livello non possibile con EventCounters.

Tuttavia, a partire da .NET 8, solo i componenti System.Net.Http e System.Net.NameResolutions vengono instrumentati usando metriche, il che significa che se sono necessari contatori dai livelli inferiori dello stack, ad esempio System.Net.Sockets o System.Net.Security, è necessario usare EventCounters.

Esistono inoltre alcune differenze semantiche tra le metriche e i relativi EventCounters corrispondenti. Ad esempio, quando si usa HttpCompletionOption.ResponseContentRead, current-requests EventCounter considera attiva una richiesta fino al momento in cui è stato letto l'ultimo byte del corpo della richiesta. La controparte delle metriche http.client.active_requests non include il tempo impiegato per leggere il corpo della risposta durante il conteggio delle richieste attive.

Sono necessarie altre metriche?

Se si hanno suggerimenti per altre informazioni utili che potrebbero essere esposte tramite metriche, creare un problema dotnet/runtime.