Condividi tramite


Creazione di metriche

Questo articolo si applica a: ✔️ .NET Core 6 e versioni successive ✔️ .NET Framework 4.6.1 e versioni successive

Le applicazioni .NET possono essere instrumentate usando le API System.Diagnostics.Metrics per tenere traccia delle metriche importanti. Alcune metriche sono incluse nelle librerie .NET standard, ma è consigliabile aggiungere nuove metriche personalizzate rilevanti per le applicazioni e le librerie. In questa esercitazione si aggiungeranno nuove metriche e si comprenderanno i tipi di metriche disponibili.

Nota

.NET include alcune API delle metriche meno recenti, vale a dire EventCounters e System.Diagnostics.PerformanceCounter, che non sono descritte qui. Per altre informazioni su queste alternative, vedere Confrontare le API delle metriche.

Creare una metrica personalizzata

Prerequisiti: .NET Core 6 SDK o una versione successiva

Creare una nuova applicazione console che faccia riferimento al pacchetto NuGet System.Diagnostics.DiagnosticSource versione 8 o successiva. Le applicazioni destinate a .NET 8+ includono questo riferimento per impostazione predefinita. Aggiornare quindi il codice in Program.cs in modo che corrisponda:

> dotnet new console
> dotnet add package System.Diagnostics.DiagnosticSource
using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each second that sells 4 hats
            Thread.Sleep(1000);
            s_hatsSold.Add(4);
        }
    }
}

Il tipo System.Diagnostics.Metrics.Meter è il punto di ingresso di una libreria per creare un gruppo denominato di strumenti. Gli strumenti registrano le misurazioni numeriche necessarie per calcolare le metriche. Qui abbiamo usato CreateCounter per creare uno strumento Counter denominato "hatco.store.hats_sold". Durante ogni transazione finta, il codice chiama Add per registrare la misurazione dei cappelli venduti, 4 in questo caso. Lo strumento "hatco.store.hats_sold" definisce in modo implicito alcune metriche che potrebbero essere calcolate da queste misurazioni, ad esempio il numero totale di cappelli venduti o cappelli venduti/sec. In definitiva, spetta agli strumenti di raccolta delle metriche determinare quali metriche calcolare e come eseguire tali calcoli, ma ogni strumento ha alcune convenzioni predefinite che trasmettono l'intento dello sviluppatore. Per gli strumenti contatori, la convenzione è che gli strumenti di raccolta mostrano il conteggio totale e/o la frequenza con cui il conteggio aumenta.

Il parametro generico int in Counter<int> e CreateCounter<int>(...) definisce che questo contatore deve essere in grado di archiviare i valori fino a Int32.MaxValue. È possibile usare uno qualsiasi di byte, short, int, long float, double o decimal a seconda delle dimensioni dei dati da archiviare e se sono necessari valori frazionari.

Eseguire l'app e lasciarla in esecuzione per il momento. Le metriche verranno visualizzate successivamente.

> dotnet run
Press any key to exit

Procedure consigliate

  • Per il codice non progettato per l'uso in un contenitore di inserimento delle dipendenze, creare il contatore una sola volta e archiviarlo in una variabile statica. Per l'utilizzo nelle librerie con riconoscimento delle dipendenze, le variabili statiche sono considerate anti-pattern e l'esempio di inserimento delle dipendenze di seguito mostra un approccio più idiomatico. Ogni libreria o sottocomponente di libreria può (e spesso deve) creare il proprio Meter. È consigliabile creare un nuovo contatore anziché riutilizzarne uno esistente se si prevede che gli sviluppatori di app apprezzino la possibilità di abilitare e disabilitare facilmente i gruppi di metriche separatamente.

  • Il nome passato al costruttore Meter deve essere univoco per distinguerlo da altri contatori. Si consigliano le Linee guida per la denominazione di OpenTelemetry, che usano nomi gerarchici punteggiati. I nomi degli assembly o degli spazi dei nomi per il codice instrumentato sono in genere una scelta ottimale. Se un assembly aggiunge strumentazione per il codice in un secondo assembly indipendente, il nome deve essere basato sull'assembly che definisce il contatore, non sull'assembly il cui codice viene instrumentato.

  • .NET non applica alcuno schema di denominazione per gli Strumenti, ma è consigliabile seguire le linee guida per la denominazione di OpenTelemetry, che usano nomi gerarchici con punti minuscoli e un carattere di sottolineatura ('_') come separatore tra più parole nello stesso elemento. Non tutti gli strumenti di metrica mantengono il nome del contatore come parte del nome finale della metrica, quindi è utile rendere il nome dello strumento univoco a livello globale in modo autonomo.

    Nomi di strumenti di esempio:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • Le API per creare strumenti e misurazioni di record sono thread-safe. Nelle librerie .NET la maggior parte dei metodi di istanza richiede la sincronizzazione quando viene richiamata sullo stesso oggetto da più thread, ma in questo caso non è necessaria.

  • Le API Strumento per registrare le misurazioni (Add in questo esempio) vengono in genere eseguite in <10 ns quando non vengono raccolti dati, o da decine a centinaia di nanosecondi quando le misurazioni vengono raccolte da una libreria o uno strumento di raccolta ad alte prestazioni. In questo modo queste API possono essere usate in modo liberale nella maggior parte dei casi, ma si occupano di codice estremamente sensibile alle prestazioni.

Visualizzare la nuova metrica

Sono disponibili molte opzioni per archiviare e visualizzare le metriche. Questa esercitazione usa lo strumento dotnet-counters, utile per l'analisi ad hoc. È anche possibile visualizzare l'esercitazione sulla raccolta di metriche per altre alternative. Se lo strumento dotnet-counters non è già installato, usare l'SDK per installarlo:

> dotnet tool update -g dotnet-counters
You can invoke the tool using the following command: dotnet-counters
Tool 'dotnet-counters' (version '7.0.430602') was successfully installed.

Mentre l'app di esempio è ancora in esecuzione, usare dotnet-counters per monitorare il nuovo contatore:

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)                          4

Come previsto, si può vedere che il negozio HatCo vende costantemente 4 cappelli ogni secondo.

Ottenere un contatore tramite inserimento delle dipendenze

Nell'esempio precedente, il contatore è stato ottenuto creandolo con new e assegnandolo a un campo statico. L'uso di statici in questo modo non è un buon approccio quando si usa l'inserimento delle dipendenze (DI). Nel codice che usa l'inserimento delle dipendenze, ad esempio ASP.NET Core o app con host generico, creare l'oggetto Contatore usando IMeterFactory. A partire da .NET 8, gli host registreranno automaticamente IMeterFactory nel contenitore del servizio oppure è possibile registrare manualmente il tipo in qualsiasi IServiceCollection chiamando AddMetrics. La factory del contatore integra le metriche con l'inserimento delle dipendenze, mantenendo i contatori in raccolte di servizi diverse, isolate l'una dall'altra anche se usano un nome identico. Ciò è particolarmente utile per i test in modo che più test in esecuzione in parallelo osservino solo le misurazioni prodotte dallo stesso test case.

Per ottenere un contatore in un tipo progettato per l'inserimento delle dipendenze, aggiungere un parametro IMeterFactory al costruttore, quindi chiamare Create. Questo esempio mostra l'uso di IMeterFactory in un'app ASP.NET Core.

Definire un tipo per contenere gli strumenti:

public class HatCoMetrics
{
    private readonly Counter<int> _hatsSold;

    public HatCoMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("HatCo.Store");
        _hatsSold = meter.CreateCounter<int>("hatco.store.hats_sold");
    }

    public void HatsSold(int quantity)
    {
        _hatsSold.Add(quantity);
    }
}

Registrare il tipo con il contenitore di inserimento delle dipendenze in Program.cs.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<HatCoMetrics>();

Inserire i valori di tipo e record delle metriche, se necessario. Poiché il tipo di metriche è registrato nell'inserimento delle dipendenze, può essere usato con controller MVC, API minime o qualsiasi altro tipo creato dall'inserimento delle dipendenze:

app.MapPost("/complete-sale", ([FromBody] SaleModel model, HatCoMetrics metrics) =>
{
    // ... business logic such as saving the sale to a database ...

    metrics.HatsSold(model.QuantitySold);
});

Procedure consigliate

  • System.Diagnostics.Metrics.Meter implementa IDisposable, ma IMeterFactory gestisce automaticamente la durata di tutti gli oggetti Meter creati, eliminandoli quando il contenitore di inserimento delle dipendenze viene eliminato. Non è necessario aggiungere codice aggiuntivo per richiamare Dispose() su Meter, e non avrà alcun effetto.

Tipi di strumenti

Finora abbiamo dimostrato solo uno strumento Counter<T>, ma ci sono più tipi di strumenti disponibili. Gli strumenti differiscono in due modi:

  • Calcoli delle metriche predefiniti: gli strumenti che raccolgono e analizzano le misurazioni dello strumento calcolano metriche predefinite diverse a seconda dello strumento.
  • Archiviazione dei dati aggregati: le metriche più utili devono essere aggregate da molte misurazioni. Un'opzione è che il chiamante fornisce singole misurazioni in momenti arbitrari e lo strumento di raccolta gestisce l'aggregazione. In alternativa, il chiamante può gestire le misurazioni di aggregazione e fornirle su richiesta in un callback.

Tipi di strumenti attualmente disponibili:

  • Contatore (CreateCounter): questo strumento tiene traccia di un valore che aumenta nel tempo e il chiamante segnala gli incrementi usando Add. La maggior parte degli strumenti calcolerà il totale e la frequenza di modifica nel totale. Per gli strumenti che mostrano una sola cosa, è consigliabile impostare la frequenza di modifica. Si supponga, ad esempio, che il chiamante richiami Add() una volta al secondo con valori successivi 1, 2, 4, 5, 4, 3. Se lo strumento di raccolta viene aggiornato ogni tre secondi, il totale dopo tre secondi è 1+2+4=7 e il totale dopo sei secondi è 1+2+4+5+4+3=19. La frequenza di modifica è (current_total - previous_total), quindi a tre secondi lo strumento riporta 7-0=7 e dopo sei secondi, riporta 19-7=12.

  • UpDownCounter (CreateUpDownCounter): questo strumento tiene traccia di un valore che può aumentare o diminuire nel tempo. Il chiamante segnala gli incrementi e i decrementi usando Add. Si supponga, ad esempio, che il chiamante richiami Add() una volta al secondo con valori successivi 1, 5, -2, 3, -1, -3. Se lo strumento di raccolta viene aggiornato ogni tre secondi, il totale dopo tre secondi è 1+5-2=4 e il totale dopo sei secondi è 1+5-2+3-1-3=3.

  • ObservableCounter (CreateObservableCounter): questo strumento è simile al Contatore, ad eccezione del fatto che il chiamante è ora responsabile della gestione del totale aggregato. Il chiamante fornisce un delegato di callback quando viene creato ObservableCounter e il callback viene richiamato ogni volta che gli strumenti devono osservare il totale corrente. Ad esempio, se uno strumento di raccolta viene aggiornato ogni tre secondi, anche la funzione di callback verrà richiamata ogni tre secondi. La maggior parte degli strumenti avrà sia il totale che la frequenza di modifica nel totale disponibile. Se è possibile visualizzarne solo uno, è consigliabile che sia la frequenza di modifica. Se il callback restituisce 0 nella chiamata iniziale, 7 quando viene chiamato di nuovo dopo tre secondi e 19 quando viene chiamato dopo sei secondi, allora lo strumento indicherà tali valori invariati come totali. Per la frequenza di modifica, lo strumento mostrerà 7-0=7 dopo tre secondi e 19-7=12 dopo sei secondi.

  • ObservableUpDownCounter (CreateObservableUpDownCounter): questo strumento è simile a UpDownCounter, ad eccezione del fatto che il chiamante è ora responsabile della gestione del totale aggregato. Il chiamante fornisce un delegato di callback quando viene creato ObservableUpDownCounter e il callback viene richiamato ogni volta che gli strumenti devono osservare il totale corrente. Ad esempio, se uno strumento di raccolta viene aggiornato ogni tre secondi, anche la funzione di callback verrà richiamata ogni tre secondi. Qualsiasi valore restituito dal callback verrà visualizzato nello strumento di raccolta invariato come totale.

  • ObservableGauge (CreateObservableGauge): questo strumento consente al chiamante di fornire un callback in cui il valore misurato viene passato direttamente come metrica. Ogni volta che lo strumento di raccolta viene aggiornato, viene richiamato il callback e qualsiasi valore restituito dal callback viene visualizzato nello strumento.

  • Istogramma (CreateHistogram): questo strumento tiene traccia della distribuzione delle misurazioni. Non esiste un unico modo canonico per descrivere un set di misurazioni, ma è consigliabile usare istogrammi o percentili calcolati. Si supponga, ad esempio, che il chiamante abbia richiamato Record per registrare queste misurazioni durante l'intervallo di aggiornamento dello strumento di raccolta: 1,5,2,3,10,9,7,4,6,8. Uno strumento di raccolta potrebbe segnalare che il 50°, il 90° e il 95° percentile di queste misurazioni siano rispettivamente 5, 9 e 9.

Procedure consigliate per la selezione di un tipo di strumento

  • Per contare gli elementi o qualsiasi altro valore che aumenta solo nel tempo, usare Counter o ObservableCounter. Scegliere tra Counter e ObservableCounter a seconda di quale è più facile da aggiungere al codice esistente: o una chiamata API per ogni operazione di incremento, o un callback che leggerà il totale corrente da una variabile gestita dal codice. Nei percorsi di codice estremamente frequente in cui le prestazioni sono importanti e l'uso di Add creerebbe più di un milione di chiamate al secondo per ogni thread, l'uso di ObservableCounter può offrire più opportunità per l'ottimizzazione.

  • Per i tempi, in genere è preferibile l'istogramma. Spesso è utile comprendere la coda di queste distribuzioni (90°, 95°, 99° percentile) anziché medie o totali.

  • Altri casi comuni, ad esempio la frequenza di riscontri nella cache o le dimensioni delle cache, delle code e dei file, sono in genere particolarmente adatti per UpDownCounter o ObservableUpDownCounter. Scegliere tra questi a seconda di quale è più facile da aggiungere al codice esistente: una chiamata API per ogni operazione di incremento e decremento, o un callback che leggerà il valore corrente da una variabile gestita dal codice.

Nota

Se si usa una versione precedente di .NET o un pacchetto NuGet DiagnosticSource che non supporta UpDownCounter e ObservableUpDownCounter (prima della versione 7), ObservableGauge è spesso un buon sostituto.

Esempio di tipi di strumenti diversi

Arrestare il processo di esempio avviato in precedenza, e sostituire il codice di esempio in Program.cs con:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");
    static Histogram<double> s_orderProcessingTime = s_meter.CreateHistogram<double>("hatco.store.order_processing_time");
    static int s_coatsSold;
    static int s_ordersPending;

    static Random s_rand = new Random();

    static void Main(string[] args)
    {
        s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_coatsSold);
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", () => s_ordersPending);

        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms that each sell 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);

            // Pretend we also sold 3 coats. For an ObservableCounter we track the value in our variable and report it
            // on demand in the callback
            s_coatsSold += 3;

            // Pretend we have some queue of orders that varies over time. The callback for the orders_pending gauge will report
            // this value on-demand.
            s_ordersPending = s_rand.Next(0, 20);

            // Last we pretend that we measured how long it took to do the transaction (for example we could time it with Stopwatch)
            s_orderProcessingTime.Record(s_rand.Next(0.005, 0.015));
        }
    }
}

Eseguire il nuovo processo e usare dotnet-counters come prima in una seconda shell per visualizzare le metriche:

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.coats_sold (Count / 1 sec)                                27
    hatco.store.hats_sold (Count / 1 sec)                                 36
    hatco.store.order_processing_time
        Percentile=50                                                      0.012
        Percentile=95                                                      0.014
        Percentile=99                                                      0.014
    hatco.store.orders_pending                                             5

Questo esempio usa alcuni numeri generati in modo casuale, in modo che i valori varieranno un po'. È possibile notare che hatco.store.hats_sold (il Counter) e hatco.store.coats_sold (ObservableCounter) vengono entrambi visualizzati come frequenza. ObservableGauge, hatco.store.orders_pending, viene visualizzato come valore assoluto. Dotnet-counters esegue il rendering degli strumenti istogrammi come tre statistiche percentili (50°, 95° e 99°), ma altri strumenti possono riepilogare la distribuzione in modo diverso o offrire più opzioni di configurazione.

Procedure consigliate

  • Gli istogrammi tendono ad archiviare molti più dati in memoria rispetto ad altri tipi di metrica. Tuttavia, l'utilizzo esatto della memoria è determinato dallo strumento di raccolta in uso. Se si definisce un numero elevato (>100) di metriche istogramma, potrebbe essere necessario fornire agli utenti indicazioni per non abilitarli tutti contemporaneamente o configurare gli strumenti per risparmiare memoria riducendo la precisione. Alcuni strumenti di raccolta possono avere dei limiti rigidi sul numero di istogrammi simultanei che essi monitoreranno per evitare un uso eccessivo della memoria.

  • I callback per tutti gli strumenti osservabili vengono richiamati in sequenza, quindi qualsiasi callback che richiede molto tempo può ritardare o impedire la raccolta di tutte le metriche. Favoriscono la lettura rapida di un valore memorizzato nella cache, non restituiscono alcuna misurazione né generano un'eccezione per l'esecuzione di qualsiasi operazione potenzialmente a esecuzione prolungata o di blocco.

  • I callback ObservableCounter, ObservableUpDownCounter e ObservableGauge si verificano in un thread che in genere non viene sincronizzato con il codice che aggiorna i valori. È responsabilità dell'utente sincronizzare l'accesso alla memoria o accettare i valori incoerenti che possono derivare dall'uso dell'accesso non sincronizzato. Gli approcci comuni per sincronizzare l'accesso sono l'uso di un blocco o di una chiamata a Volatile.Read e Volatile.Write.

  • Le funzioni CreateObservableGauge e CreateObservableCounter restituiscono un oggetto strumento, ma nella maggior parte dei casi non è necessario salvarlo in una variabile perché non è necessaria un'ulteriore interazione con l'oggetto. Assegnarlo a una variabile statica come è stato fatto per gli altri strumenti è legale ma soggetto a errori, perché l'inizializzazione statica C# è differita e la variabile in genere non viene mai referenziata. Ecco un esempio del problema:

    using System;
    using System.Diagnostics.Metrics;
    
    class Program
    {
        // BEWARE! Static initializers only run when code in a running method refers to a static variable.
        // These statics will never be initialized because none of them were referenced in Main().
        //
        static Meter s_meter = new Meter("HatCo.Store");
        static ObservableCounter<int> s_coatsSold = s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_rand.Next(1,10));
        static Random s_rand = new Random();
    
        static void Main(string[] args)
        {
            Console.ReadLine();
        }
    }
    

Descrizioni e unità

Gli strumenti possono specificare descrizioni e unità facoltative. Questi valori sono opachi per tutti i calcoli delle metriche, ma possono essere visualizzati nell'interfaccia utente dello strumento di raccolta per aiutare i tecnici a comprendere come interpretare i dati. Arrestare il processo di esempio avviato in precedenza e sostituire il codice di esempio in Program.cs con:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>(name: "hatco.store.hats_sold",
                                                                unit: "{hats}",
                                                                description: "The number of hats sold in our store");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each 100ms that sells 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);
        }
    }
}

Eseguire il nuovo processo e usare dotnet-counters come prima in una seconda shell per visualizzare le metriche:

Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold ({hats} / 1 sec)                                40

dotnet-counters attualmente non usa il testo della descrizione nell'interfaccia utente, ma mostra l'unità quando viene fornita. In questo caso, si noterà che "{hats}" ha sostituito il termine generico "Count" visibile nelle descrizioni precedenti.

Procedure consigliate

  • Le API .NET consentono l'uso di qualsiasi stringa come unità, ma è consigliabile usare UCUM, uno standard internazionale per i nomi di unità. Le parentesi graffe intorno a "{hats}" fanno parte dello standard UCUM, a indicare che si tratta di un'annotazione descrittiva anziché di un nome di unità con un significato standardizzato come secondi o byte.

  • L'unità specificata nel costruttore deve descrivere le unità appropriate per una singola misura. Questo a volte differisce dalle unità nella metrica finale. In questo esempio ogni misura è un numero di cappelli (hats), quindi "{hats}" è l'unità appropriata da passare al costruttore. Lo strumento di raccolta ha calcolato una frequenza e ha dedotto autonomamente che l'unità appropriata per la metrica calcolata è {hats}/sec.

  • Quando si registrano le misurazioni del tempo, preferire le unità di secondi registrate come valore a virgola mobile o doppio.

Metriche multidimensionali

Le misurazioni possono anche essere associate a coppie chiave-valore denominate tag che consentono di classificare i dati per l'analisi. Ad esempio, HatCo potrebbe voler registrare non solo il numero di cappelli venduti, ma anche di quali dimensioni e colore erano. Quando si analizzano i dati in un secondo momento, i tecnici HatCo possono suddividere i totali in base alle dimensioni, al colore o a qualsiasi combinazione di entrambi.

I tag contatori e istogrammi possono essere specificati negli overload di Add e Record che accettano uno o più argomenti KeyValuePair. Ad esempio:

s_hatsSold.Add(2,
               new KeyValuePair<string, object>("product.color", "red"),
               new KeyValuePair<string, object>("product.size", 12));

Sostituire il codice di Program.cs ed eseguire di nuovo l'app e i contatori dotnet come prima:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction, every 100ms, that sells two size 12 red hats, and one size 19 blue hat.
            Thread.Sleep(100);
            s_hatsSold.Add(2,
                           new KeyValuePair<string,object>("product.color", "red"),
                           new KeyValuePair<string,object>("product.size", 12));
            s_hatsSold.Add(1,
                           new KeyValuePair<string,object>("product.color", "blue"),
                           new KeyValuePair<string,object>("product.size", 19));
        }
    }
}

Dotnet-counters mostra ora una categorizzazione di base:

Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)
        product.color=blue,product.size=19                                 9
        product.color=red,product.size=12                                 18

Per ObservableCounter e ObservableGauge, le misurazioni con tag possono essere fornite nel callback passato al costruttore:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");

    static void Main(string[] args)
    {
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", GetOrdersPending);
        Console.WriteLine("Press any key to exit");
        Console.ReadLine();
    }

    static IEnumerable<Measurement<int>> GetOrdersPending()
    {
        return new Measurement<int>[]
        {
            // pretend these measurements were read from a real queue somewhere
            new Measurement<int>(6, new KeyValuePair<string,object>("customer.country", "Italy")),
            new Measurement<int>(3, new KeyValuePair<string,object>("customer.country", "Spain")),
            new Measurement<int>(1, new KeyValuePair<string,object>("customer.country", "Mexico")),
        };
    }
}

Quando viene eseguito con dotnet-counters come in precedenza, il risultato è:

Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.orders_pending
        customer.country=Italy                                             6
        customer.country=Mexico                                            1
        customer.country=Spain                                             3

Procedure consigliate

  • Sebbene l'API consenta l'uso di qualsiasi oggetto come valore del tag, i tipi numerici e le stringhe vengono previsti dagli strumenti di raccolta. Altri tipi possono essere o non essere supportati da uno strumento di raccolta specifico.

  • È consigliabile che i nomi dei tag seguano le linee guida per la denominazione di OpenTelemetry, che usano nomi gerarchica con punti minuscoli con caratteri '_' per separare più parole nello stesso elemento. Se i nomi dei tag vengono riutilizzati in metriche diverse o in altri record di telemetria, devono avere lo stesso significato e set di valori legali ovunque vengano usati.

    Nomi di tag di esempio:

    • customer.country
    • store.payment_method
    • store.purchase_result
  • Attenzione ad avere combinazioni molto grandi o non associate di valori di tag registrati in pratica. Anche se l'implementazione dell'API .NET può gestirlo, probabilmente gli strumenti di raccolta allocano l'archiviazione per i dati delle metriche associati a ogni combinazione di tag e questa potrebbe diventare molto grande. Ad esempio, è corretto se HatCo ha 10 colori di cappello diversi e 25 taglie per un massimo di 10*25=250 totali di vendita di cui tenere traccia. Tuttavia, se HatCo ha aggiunto un terzo tag che è un CustomerID per la vendita e vendono a 100 milioni di clienti in tutto il mondo, ora ci sono probabilmente miliardi di combinazioni di tag diverse registrate. La maggior parte degli strumenti di raccolta delle metriche consentirà di eliminare i dati per rimanere entro i limiti tecnici, oppure possono essere previsti dei costi monetari elevati per coprire l'archiviazione e l'elaborazione dei dati. L'implementazione di ogni strumento di raccolta ne determinerà i limiti, ma probabilmente meno di 1000 combinazioni per uno strumento sono sicure. Se si superano le 1000 combinazioni, sarà necessario che lo strumento di raccolta applichi dei filtri o sia progettato per operare su larga scala. Le implementazioni dell'istogramma tendono a usare molta più memoria rispetto ad altre metriche, quindi i limiti sicuri potrebbero essere inferiori di 10-100 volte. Se si prevede un numero elevato di combinazioni di tag univoci, i log, i database transazionali o i sistemi di elaborazione dei Big Data possono essere delle soluzioni più appropriate per operare su larga scala.

  • Per gli strumenti che avranno un numero molto elevato di combinazioni di tag, preferire l'uso di un tipo di archiviazione più piccolo per ridurre il sovraccarico di memoria. Ad esempio, l'archiviazione di short per Counter<short> occupa solo 2 byte per combinazione di tag, mentre double per Counter<double> occupa 8 byte per combinazione di tag.

  • Gli strumenti di raccolta sono incoraggiati a ottimizzare il codice che specifica lo stesso set di nomi di tag nello stesso ordine per ogni chiamata per registrare le misurazioni sullo stesso strumento. Per il codice ad alte prestazioni che deve chiamare Add e Record di frequente, preferire l'uso della stessa sequenza di nomi di tag per ogni chiamata.

  • L'API .NET è ottimizzata per essere senza allocazione per le chiamate Add e Record con tre o meno tag specificati singolarmente. Per evitare allocazioni con un numero maggiore di tag, usare TagList. In generale, il sovraccarico delle prestazioni di queste chiamate aumenta man mano che vengono usati più tag.

Nota

OpenTelemetry fa riferimento ai tag come "attributi". Si tratta di due nomi diversi per la stessa funzionalità.

Testare le metriche personalizzate

È possibile testare le metriche personalizzate aggiunte usando MetricCollector<T>. Questo tipo rende più semplice registrare le misurazioni da strumenti specifici e affermare che i valori erano corretti.

Eseguire test con inserimento delle dipendenze

Il codice seguente illustra un esempio di test case per i componenti di codice che usano l'inserimento delle dipendenze e IMeterFactory.

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var services = CreateServiceProvider();
        var metrics = services.GetRequiredService<HatCoMetrics>();
        var meterFactory = services.GetRequiredService<IMeterFactory>();
        var collector = new MetricCollector<int>(meterFactory, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }

    // Setup a new service provider. This example creates the collection explicitly but you might leverage
    // a host or some other application setup code to do this as well.
    private static IServiceProvider CreateServiceProvider()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddMetrics();
        serviceCollection.AddSingleton<HatCoMetrics>();
        return serviceCollection.BuildServiceProvider();
    }
}

Ogni oggetto MetricCollector registra tutte le misurazioni per un solo strumento. Se è necessario verificare le misurazioni da più strumenti, creare un MetricCollector per ognuno di essi.

Testare senza inserimento delle dipendenze

È anche possibile testare il codice che usa un oggetto Meter globale condiviso in un campo statico, ma assicurarsi che tali test siano configurati per non essere eseguiti in parallelo. Poiché l'oggetto Meter viene condiviso, MetricCollector in un test osserverà le misurazioni create da qualsiasi altro test in esecuzione in parallelo.

class HatCoMetricsWithGlobalMeter
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    public void HatsSold(int quantity)
    {
        s_hatsSold.Add(quantity);
    }
}

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var metrics = new HatCoMetricsWithGlobalMeter();
        // Be careful specifying scope=null. This binds the collector to a global Meter and tests
        // that use global state should not be configured to run in parallel.
        var collector = new MetricCollector<int>(null, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }
}