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

Create a new console application that references the System.Diagnostics.DiagnosticSource NuGet package version 8 or greater. 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 package add System.Diagnostics.DiagnosticSource

Se si usa una versione SDK di .NET 9 o versioni precedenti, usare invece il dotnet add package modulo.

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

The System.Diagnostics.Metrics.Meter type is the entry point for a library to create a named group of instruments. Gli strumenti registrano le misurazioni numeriche necessarie per calcolare le metriche. Here we used CreateCounter to create a Counter instrument named "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. For Counter instruments, the convention is that collection tools show the total count and/or the rate at which the count is increasing.

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.

Run the app and leave it running for now. Le metriche verranno visualizzate successivamente.

> dotnet run
Press any key to exit

Procedure consigliate

  • For code that is not designed for use in a Dependency Injection (DI) container, create the Meter once and store it in a static variable. For usage in DI-aware libraries static variables are considered an anti-pattern and the DI example below shows a more idiomatic approach. Each library or library subcomponent can (and often should) create its own 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.

  • The name passed to the Meter constructor should be unique to distinguish it from other Meters. We recommend OpenTelemetry naming guidelines, which use dotted hierarchical names. Assembly names or namespace names for code being instrumented are usually a good choice. If an assembly adds instrumentation for code in a second, independent assembly, the name should be based on the assembly that defines the Meter, not the assembly whose code is being instrumented.

  • .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. Not all metric tools preserve the Meter name as part of the final metric name, so it's beneficial to make the instrument name globally unique on its own.

    Nomi di strumenti di esempio:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • The APIs to create instruments and record measurements are 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. This tutorial uses the dotnet-counters tool, which is useful for ad-hoc analysis. You can also see the metrics collection tutorial for other alternatives. 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.

Get a Meter via dependency injection

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). In code that uses DI, such as ASP.NET Core or apps with Generic Host, create the Meter object using 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. The meter factory integrates metrics with DI, keeping Meters in different service collections isolated from each other even if they use an identical name. Ciò è particolarmente utile per i test in modo che più test in esecuzione in parallelo osservino solo le misurazioni prodotte dallo stesso test case.

To obtain a Meter in a type designed for DI, add an IMeterFactory parameter to the constructor, then call Create. Questo esempio mostra l'uso di IMeterFactory in un'app ASP.NET Core.

Define a type to hold the instruments:

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

Register the type with DI container in Program.cs.

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

Inserisci il tipo di metriche e registra i valori, se necessario. Because the metrics type is registered in DI, it can be used with MVC controllers, minimal APIs, or any other type that is created by DI:

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. The rate of change is the (current_total - previous_total), so at three seconds the tool reports 7-0=7, and after six seconds, it reports 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. The caller provides a callback delegate when the ObservableCounter is created and the callback is invoked whenever tools need to observe the current total. Ad esempio, se uno strumento di raccolta viene aggiornato ogni tre secondi, la funzione di callback verrà richiamata anche ogni tre secondi. Most tools will have both the total and rate of change in the total available. If only one can be shown, rate of change is recommended. 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. The caller provides a callback delegate when the ObservableUpDownCounter is created and the callback is invoked whenever tools need to observe the current total. Ad esempio, se uno strumento di raccolta viene aggiornato ogni tre secondi, la funzione di callback verrà richiamata anche ogni tre secondi. Whatever value is returned by the callback will be shown in the collection tool unchanged as the total.

  • 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

    For details about how to set the recommended bucket boundaries when creating a Histogram instrument see: Using Advice to customize Histogram instruments.

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. Choose between Counter and ObservableCounter depending on which is easier to add to the existing code: either an API call for each increment operation, or a callback that will read the current total from a variable the code maintains. 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. Often it's useful to understand the tail of these distributions (90th, 95th, 99th percentile) rather than averages or totals.

  • Other common cases, such as cache hit rates or sizes of caches, queues, and files are usually well suited for UpDownCounter or 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 renders Histogram instruments as three percentile statistics (50th, 95th, and 99th) but other tools might summarize the distribution differently or offer more configuration options.

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. Favor quickly reading a cached value, returning no measurements, or throwing an exception over performing any potentially long-running or blocking operation.

  • 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 . Assigning it to a static variable as we did for the other instruments is legal but error prone, because C# static initialization is lazy and the variable is usually never referenced. 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 doesn't currently use the description text in the UI, but it does show the unit when it is provided. In this case, you see "{hats}" has replaced the generic term "Count" that is visible in previous descriptions.

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à. The curly braces around "{hats}" is part of the UCUM standard, indicating that it is a descriptive annotation rather than a unit name with a standardized meaning like seconds or bytes.

  • The unit specified in the constructor should describe the units appropriate for an individual measurement. This will sometimes differ from the units on the final reported metric. In this example, each measurement is a number of hats, so "{hats}" is the appropriate unit to pass in the constructor. Lo strumento di raccolta potrebbe aver calcolato il tasso di variazione e determinato autonomamente che l'unità appropriata per la metrica calcolata è {hats}/secondo.

  • When recording measurements of time, prefer units of seconds recorded as a floating point or double value.

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.

Counter and Histogram tags can be specified in overloads of the Add and Record that take one or more KeyValuePair arguments. Per esempio:

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

Replace the code of Program.cs and rerun the app and dotnet-counters as before:

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

For ObservableCounter and ObservableGauge, tagged measurements can be provided in the callback passed to the constructor:

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
  • Beware of having very large or unbounded combinations of tag values being recorded in practice. 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à.

Using Advice to customize Histogram instruments

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.

The tool or library collecting the Histogram data is responsible for defining the buckets it will use. 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. For example, OpenTelemetry defines a View API. 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.

Important

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

For more details about explicit bucket Histograms in OpenTelemetry see:

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 with dependency injection

The following code shows an example test case for code components that use dependency injection and 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);
    }
}