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 precedenti, 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 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 in una libreria per creare un gruppo di strumenti denominato. 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 simulata, il codice chiama Add per registrare la quantità di 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 qualsiasi byte, short, int, long, float, doubleo 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 Dependency Injection, creare il Meter una sola volta e archiviarlo in una variabile statica. Per l'utilizzo nelle librerie compatibili con l'inserimento delle dipendenze, le variabili statiche sono considerate un anti-pattern e l'esempio di inserimento delle dipendenze riportato 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 di denominazione di OpenTelemetry , che usano nomi gerarchici a punti. I nomi degli assembly o gli 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 Instruments, ma raccomandiamo di seguire le linee guida per la denominazione di OpenTelemetry, che usano nomi gerarchici in minuscolo separati da punti e un carattere di sottolineatura ('_') come separatore tra più parole nello stesso elemento. Non tutti gli strumenti di misurazione mantengono il nome del metro come parte del nome finale della misurazione, quindi è utile rendere il nome dello strumento globalmente unico di per sé.

    Nomi di strumenti di esempio:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • Le API per creare strumenti e registrare misurazioni 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 degli strumenti per registrare le misurazioni (Add in questo esempio) si eseguono di solito in <10 ns quando non vengono raccolti dati, oppure in decine a centinaia di nanosecondi quando le misurazioni vengono raccolte da una libreria o uno strumento di alta prestazione. In questo modo queste API possono essere usate in modo liberale nella maggior parte dei casi, ma bisogna prestare attenzione al 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. Puoi anche consultare l'esercitazione sulla raccolta di metriche per altre alternative. Se lo strumento dotnet-counters non è già installato, usa 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 Meter tramite iniezione delle dipendenze

Nell'esempio precedente il contatore è stato ottenuto creandolo con new e assegnandolo a un campo statico. L'uso di variabili statiche 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 le app con host generico, creare l'oggetto Meter 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 fabbrica di metri integra le metriche con DI, mantenendo i contatori in collezioni di servizi diverse isolate l'una dall'altra anche se utilizzano nomi identici. 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 in Program.cs.

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

Inserisci il tipo di metriche e registra i valori, se necessario. Poiché il tipo di metriche è registrato nell'inversione di controllo (DI), può essere utilizzato con controller MVC, API minime o qualsiasi altro tipo creato tramite inversione di controllo.

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, disponendoli quando il contenitore DI viene eliminato. Non è necessario aggiungere codice aggiuntivo per richiamare Dispose() nel Metere 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 - Strumenti che raccolgono e analizzano le misurazioni degli strumenti calcolano metriche predefinite diverse a seconda dello strumento.
  • Archiviazione dei dati aggregati: le metriche più utili richiedono l'aggregazione dei dati 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:

  • Counter (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 il tasso di variazione del 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. Il tasso di variazione è (current_total - previous_total), quindi a tre secondi lo strumento segnala 7-0=7 e dopo sei secondi, segnala 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 a Counter, ad eccezione del fatto che il chiamante è ora responsabile della gestione del totale aggregato. Il chiamante fornisce un delegato di callback quando l'ObservableCounter viene creato e il callback viene attivato ogni volta che gli strumenti devono osservare il totale corrente. Ad esempio, se uno strumento di raccolta viene aggiornato ogni tre secondi, la funzione di callback verrà richiamata anche ogni tre secondi. La maggior parte degli strumenti avrà sia il totale che il tasso di variazione del totale disponibili. Se è possibile visualizzare solo uno, è consigliabile modificare 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, 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 l'`ObservableUpDownCounter`, e il callback viene invocato ogni volta che gli strumenti devono monitorare il totale attuale. Ad esempio, se uno strumento di raccolta viene aggiornato ogni tre secondi, la funzione di callback verrà richiamata anche ogni tre secondi. Qualsiasi valore restituito dal callback verrà visualizzato nello strumento di raccolta invariato come totale.

  • misuratore (CreateGauge) - Questo strumento consente al chiamante di impostare il valore corrente della metrica usando il metodo Record. Il valore può essere aggiornato in qualsiasi momento richiamando di nuovo il metodo e uno strumento di raccolta delle metriche visualizzerà qualsiasi valore impostato più di recente.

  • 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 i 50, il 90 e il 95° percentile di queste misurazioni sono rispettivamente 5, 9 e 9.

    Nota

    Per informazioni dettagliate su come impostare i limiti del bucket consigliati durante la creazione di uno strumento Istogramma, consulta: Uso di consigli per personalizzare gli strumenti Istogramma.

Procedure consigliate per la selezione di un tipo di strumento

  • Per contare gli elementi o qualsiasi altro valore che aumenta esclusivamente nel tempo, usare Counter o ObservableCounter. Scegliere tra Counter e ObservableCounter a seconda di quale è più facile aggiungere al codice esistente: una chiamata API per ogni operazione di incremento oppure 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 potrebbe offrire più opportunità di ottimizzazione.

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

  • Altri casi comuni, come i tassi di successo delle cache o le dimensioni delle cache, delle code e dei file, sono generalmente ben adatti per UpDownCounter o ObservableUpDownCounter. Scegliere tra di essi a seconda del quale è più facile 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(5, 15)/1000.0);
        }
    }
}

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

Name                                                  Current Value
[HatCo.Store]
    hatco.store.coats_sold (Count)                        8,181    
    hatco.store.hats_sold (Count)                           548    
    hatco.store.order_processing_time
        Percentile
        50                                                    0.012    
        95                                                    0.013   
        99                                                    0.013
    hatco.store.orders_pending                                9    

Questo esempio usa alcuni numeri generati in modo casuale, in modo che i valori varieranno un po'. Dotnet-counters esegue il rendering degli strumenti di istogramma come tre statistiche di 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 a 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 di istogrammi, potrebbe essere necessario fornire agli utenti indicazioni per non abilitarle tutte contemporaneamente, o configurare gli strumenti per ottimizzare la memoria riducendo la precisione. Alcuni strumenti di raccolta possono avere limiti rigidi sul numero di istogrammi simultanei che 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. Favorire la rapida lettura di un valore memorizzato nella cache, evitare di restituire misurazioni o generare eccezioni, piuttosto che eseguire operazioni potenzialmente lunghe o bloccanti.

  • I callback di ObservableCounter, ObservableUpDownCounter e ObservableGauge si attivano su un thread che di solito non è 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 usare un blocco o chiamare 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 alcuna ulteriore interazione con l'oggetto . Assegnarlo a una variabile statica come abbiamo fatto per gli altri strumenti è legale ma soggetto a errori, perché l'inizializzazione statica di C# è differita e la variabile in genere non viene mai riferita. 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

Name                                                       Current Value
[HatCo.Store]
    hatco.store.hats_sold ({hats})                                40

dotnet-counters non utilizza attualmente il testo della descrizione nell'interfaccia utente, ma mostra l'unità se disponibile. 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 delle 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à sulla metrica finale riportata. In questo esempio ogni misura è un numero di cappelli, quindi "{hats}" è l'unità appropriata da passare al costruttore. Lo strumento di raccolta potrebbe aver calcolato il tasso di variazione e determinato autonomamente che l'unità appropriata per la metrica calcolata è {hats}/secondo.

  • 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 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 del Add e Record che accettano uno o più argomenti KeyValuePair. Per 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 lo strumento "dotnet-counters" 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

Name                                                  Current Value
[HatCo.Store]
    hatco.store.hats_sold (Count)
        product.color product.size
        blue          19                                     73
        red           12                                    146    

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

Name                                                  Current Value
[HatCo.Store]
    hatco.store.orders_pending
        customer.country
        Italy                                                 6
        Mexico                                                1
        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 o non essere supportati da uno strumento di raccolta specifico.

  • È consigliabile che i nomi dei tag seguano le linee guida per la denominazione OpenTelemetry, che usano nomi gerarchici in minuscolo, con caratteri di sottolineatura '_' 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 illimitate di valori di tag registrati effettivamente. Anche se l'implementazione dell'API .NET può gestirla, è probabile che gli strumenti di raccolta alloccheranno spazio di archiviazione per i dati delle metriche associati a ogni combinazione di tag e questo potrebbe diventare molto voluminoso. Ad esempio, va bene se HatCo ha 10 colori di cappelli diversi e 25 taglie per un totale di vendite massimo di 10*25=250 da monitorare. Tuttavia, se HatCo aggiungesse un terzo tag che è un CustomerID per la vendita e vendesse a 100 milioni di clienti in tutto il mondo, ci sarebbero ora probabilmente miliardi di combinazioni di tag diverse da registrare. La maggior parte degli strumenti di raccolta delle metriche consentirà di eliminare i dati per rimanere entro i limiti tecnici oppure possono essere previsti costi monetari elevati per coprire l'archiviazione e l'elaborazione dei dati. L'implementazione di ogni strumento di raccolta determinerà i limiti, ma probabilmente meno di 1000 combinazioni per uno strumento è sicura. Qualsiasi combinazione superiore a 1000 richiederà allo strumento di raccolta di applicare filtri o essere progettato per operare su larga scala. Le implementazioni dell'istogramma tendono a usare molto 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 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 del short per un Counter<short> occupa solo 2 byte per combinazione di tag, mentre un 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 frequentemente, preferire l'uso della stessa sequenza di nomi di tag per ogni chiamata.

  • L'API .NET è ottimizzata per essere senza allocazioni 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à.

Uso di Suggerimenti per personalizzare gli strumenti dell'istogramma

Quando si usano istogrammi, è responsabilità dello strumento o della libreria raccogliere i dati per decidere come rappresentare al meglio la distribuzione dei valori registrati. Una strategia comune (e la modalità predefinita quando si usa OpenTelemetry) consiste nel suddividere l'intervallo di valori possibili in intervalli secondari denominati bucket e segnalare il numero di valori registrati in ogni bucket. Ad esempio, uno strumento può dividere i numeri in tre bucket, quelli minori di 1, quelli compresi tra 1 e 10 e quelli maggiori di 10. Se l'app ha registrato i valori 0,5, 6, 0,1 e 12, ci sarebbero due punti dati nel primo contenitore, uno nel secondo e uno nel terzo.

Lo strumento o la libreria che raccoglie i dati dell'istogramma è responsabile della definizione dei bucket che utilizzerà. La configurazione predefinita del bucket quando si usa OpenTelemetry è: [ 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 ].

I valori predefiniti potrebbero non portare alla granularità migliore per ogni istogramma. Ad esempio, le durate delle richieste inferiori al secondo rientrano tutte nel bucket 0.

Lo strumento o la libreria che raccoglie i dati dell'istogramma possono offrire meccanismi per consentire agli utenti di personalizzare la configurazione del bucket. Ad esempio, OpenTelemetry definisce un'API di visualizzazione . Ciò richiede tuttavia un'azione dell'utente finale e rende l'utente responsabile di comprendere la distribuzione dei dati abbastanza bene per scegliere i bucket corretti.

Per migliorare l'esperienza, la versione 9.0.0 del pacchetto di System.Diagnostics.DiagnosticSource ha introdotto l'API (InstrumentAdvice<T>).

L'API InstrumentAdvice può essere usata dagli autori di strumentazione per specificare il set di limiti di bucket predefiniti consigliati per un determinato Istogramma. Lo strumento o la libreria che raccoglie i dati dell'istogramma può quindi scegliere di usare tali valori durante la configurazione dell'aggregazione, offrendo un'esperienza di onboarding più fluida per gli utenti. Questa funzionalità è supportata in OpenTelemetry .NET SDK a partire dalla versione 1.10.0.

Importante

In generale, più bucket porteranno a dati più precisi per un determinato istogramma, ma ogni bucket richiede memoria per archiviare i dettagli aggregati e c'è un costo della CPU per trovare il bucket corretto durante l'elaborazione di una misura. È importante comprendere i compromessi tra precisione e consumo di CPU/memoria quando si sceglie il numero di bucket da consigliare tramite l'API InstrumentAdvice.

Il codice seguente illustra un esempio che usa l'API InstrumentAdvice per impostare i bucket predefiniti consigliati.

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

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Histogram<double> s_orderProcessingTime = s_meter.CreateHistogram<double>(
        name: "hatco.store.order_processing_time",
        unit: "s",
        description: "Order processing duration",
        advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = [0.01, 0.05, 0.1, 0.5, 1, 5] });

    static Random s_rand = new Random();

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while (!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms
            Thread.Sleep(100);

            // 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(5, 15) / 1000.0);
        }
    }
}

Informazioni aggiuntive

Per altri dettagli sugli istogrammi di bucket espliciti in OpenTelemetry, vedere:

Testare le metriche personalizzate

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

Test con iniezione di dipendenze

Il codice seguente illustra un esempio di test case per i componenti di codice che usano l'iniezione 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 instrumento. Se è necessario verificare le misurazioni da più strumenti, creare un metriccollector per ognuno di essi.

Test senza iniezione di dipendenza

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