Création de métriques

Cet article s’applique à : ✔️ .NET Core 6 et versions ultérieures✔️ .NET Framework 4.6.1 et versions ultérieures

Les applications .NET peuvent être instrumentées à l’aide des API System.Diagnostics.Metrics pour suivre des métriques importantes. Certaines métriques sont incluses dans les bibliothèques .NET standard, mais vous pouvez ajouter de nouvelles métriques personnalisées pertinentes pour vos applications et bibliothèques. Dans ce tutoriel, vous allez ajouter de nouvelles métriques et comprendre les types de métriques disponibles.

Notes

.NET a des API de métriques plus anciennes, à savoir EventCounters et System.Diagnostics.PerformanceCounter, qui ne sont pas couvertes ici. Pour en savoir plus sur ces alternatives, consultez Comparer les API de métrique.

Créer une métrique personnalisée

Prérequis : SDK .NET Core 6 ou version ultérieure

Créez une nouvelle application console qui référence le package NuGet System.Diagnostics.DiagnosticSource version 8 ou ultérieure. Les applications qui ciblent .NET 8 et versions ultérieures incluent cette référence par défaut. Ensuite, mettez à jour le code dans Program.cs pour qu’il corresponde à :

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

Le type System.Diagnostics.Metrics.Meter est le point d’entrée d’une bibliothèque pour créer un groupe nommé d’instruments. Les instruments enregistrent les mesures numériques nécessaires pour calculer les métriques. Ici, nous avons utilisé CreateCounter pour créer un instrument Compteur nommé « hatco.store.hats_sold ». Lors de chaque transaction factice, le code appelle Add pour enregistrer la mesure des chapeaux qui ont été vendus, 4 dans ce cas. L’instrument « hatco.store.hats_sold » définit implicitement certaines métriques qui peuvent être calculées à partir de ces mesures, telles que le nombre total de chapeaux vendus ou de chapeaux vendus par seconde. En fin de compte, il incombe aux outils de collecte de métriques de déterminer quelles métriques calculer et comment effectuer ces calculs, mais chaque instrument a des conventions par défaut qui transmettent l’intention du développeur. Pour les instruments de compteur, la convention est que les outils de collecte affichent le nombre total et/ou la vitesse à laquelle le nombre augmente.

Le paramètre générique int sur Counter<int> et CreateCounter<int>(...) définit que ce compteur doit être en mesure de stocker des valeurs jusqu’à Int32.MaxValue. Vous pouvez utiliser n’importe lequel de byte, short, intlong, float, double ou decimal selon la taille des données que vous devez stocker et si des valeurs fractionnaires sont nécessaires.

Exécutez l’application et laissez-la en cours d’exécution pour l’instant. Nous afficherons ensuite les métriques.

> dotnet run
Press any key to exit

Bonnes pratiques

  • Pour le code qui n’est pas conçu pour être utilisé dans un conteneur d’injection de dépendances (DI), créez le compteur une fois et stockez-le dans une variable statique. Pour l’utilisation dans les bibliothèques prenant en charge la DI, les variable statiques sont considérées comme un anti-modèle et l’exemple de DI ci-dessous montre une approche plus idiomatique. Chaque bibliothèque ou sous-composant de bibliothèque peut (et doit souvent) créer son propre Meter. Envisagez de créer un nouveau compteur plutôt que de réutiliser un compteur existant si vous prévoyez que les développeurs d’applications apprécieraient de pouvoir facilement activer et désactiver les groupes de métriques séparément.

  • Le nom passé au constructeur Meter doit être unique pour le distinguer des autres compteurs. Nous recommandons les instructions d’affectation de noms d’OpenTelemetry, qui utilisent des noms hiérarchiques en pointillés. Les noms d’assembly ou d’espace de noms pour le code instrumenté constituent généralement un bon choix. Si un assembly ajoute une instrumentation pour le code dans un deuxième assembly indépendant, le nom doit être basé sur l’assembly qui définit le compteur, et non sur l’assembly dont le code est instrumenté.

  • .NET n’applique aucun schéma d’affectation de noms pour les instruments, mais nous recommandons de suivre les instructions d’affectation de noms d’OpenTelemetry, qui utilisent des noms hiérarchiques en pointillés minuscules et un trait de soulignement (« _ ») comme séparateur entre plusieurs mots dans le même élément. Tous les outils de métriques ne conservent pas le nom du compteur dans le cadre du nom de la métrique finale. Il est donc utile de rendre le nom de l’instrument globalement unique par lui-même.

    Exemples de noms d’instruments :

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • Les API permettant de créer des instruments et d’enregistrer des mesures sont thread-safe. Dans les bibliothèques .NET, la plupart des méthodes d’instance nécessitent une synchronisation lorsqu’elles sont appelées sur le même objet à partir de plusieurs threads, mais cela n’est pas nécessaire dans ce cas.

  • Les API Instrument pour enregistrer les mesures (Add dans cet exemple) s’exécutent généralement en <10 ns quand aucune donnée n’est collectée, ou en des dizaines à des centaines de nanosecondes lorsque des mesures sont collectées par une bibliothèque ou un outil de collecte très performant. Cela permet d’utiliser ces API de manière libérale dans la plupart des cas, mais veillez à ce que le code soit extrêmement sensible aux performances.

Afficher la nouvelle métrique

Il existe de nombreuses options pour stocker et afficher les métriques. Ce tutoriel utilise l’outil dotnet-counters, qui est utile pour l’analyse ad hoc. Vous pouvez également consulter le tutoriel sur la collecte de métriques pour d’autres alternatives. Si l’outil dotnet-counters n’est pas déjà installé, utilisez le Kit de développement logiciel (SDK) pour l’installer :

> 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.

Pendant que l’exemple d’application est toujours en cours d’exécution, utilisez dotnet-counters pour surveiller le nouveau compteur :

> 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

Comme prévu, vous pouvez voir que le magasin HatCo vend régulièrement 4 chapeaux chaque seconde.

Obtenir un compteur via l’injection de dépendances

Dans l’exemple précédent, le compteur a été obtenu en le construisant avec new et en l’affectant à un champ statique. L’utilisation de statiques de cette façon n’est pas une bonne approche lors de l’utilisation de l’injection de dépendances (DI). Dans le code qui utilise la DI, comme ASP.NET Core ou les applications avec Hôte générique, créez l’objet de compteur à l’aide de IMeterFactory. À compter de .NET 8, les hôtes inscrivent IMeterFactory automatiquement dans le conteneur de service ou vous pouvez inscrire manuellement le type dans n’importe quel IServiceCollection en appelant AddMetrics. La fabrique de compteurs intègre des métriques avec la DI, en maintenant les compteurs dans différentes collections de services isolées les unes des autres, même si elles utilisent un nom identique. Cela est particulièrement utile pour les tests afin que plusieurs tests s’exécutant en parallèle observent uniquement les mesures produites à partir du même cas de test.

Pour obtenir un compteur dans un type conçu pour la DI, ajoutez un paramètre IMeterFactory au constructeur, puis appelez Create. Cet exemple montre l’utilisation d’IMeterFactory dans une application ASP.NET Core.

Définissez un type pour contenir les 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);
    }
}

Inscrivez le type auprès du conteneur DI dans Program.cs.

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

Injectez le type d’indicateur de performance et enregistrez les valeurs si nécessaire. Étant donné que le type de métrique est enregistré dans DI, il peut être utilisé avec des contrôleurs MVC, des API minimales ou tout autre type créé par DI :

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

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

Bonnes pratiques

  • System.Diagnostics.Metrics.Meter implémente IDisposable, mais IMeterFactory gère automatiquement la durée de vie de tous les objets Meter qu’il crée, en les supprimant lorsque le conteneur DI est supprimé. Il n’est pas nécessaire d’ajouter du code supplémentaire pour appeler Dispose() sur le Meter, et il n’aura aucun effet.

Types d’instruments

Jusqu’à présent, nous n’avons présenté qu’un instrument Counter<T>, mais d’autres types d’instruments sont disponibles. Les instruments diffèrent de deux façons :

  • Calculs de métriques par défaut : les outils qui collectent et analysent les mesures de l’instrument calculent différentes métriques par défaut en fonction de l’instrument.
  • Stockage des données agrégées : les métriques les plus utiles ont besoin d’être agrégées à partir de nombreuses mesures. L’appelant fournit des mesures individuelles à des moments arbitraires et l’outil de collecte gère l’agrégation. L’appelant peut également gérer les mesures agrégées et les fournir à la demande dans un rappel.

Types d’instruments actuellement disponibles :

  • Counter (CreateCounter) : cet instrument suit une valeur qui augmente au fil du temps et l’appelant signale les incréments à l’aide de Add. La plupart des outils calculent le total et le taux de change du total. Pour les outils qui n’affichent qu’une seule chose, le taux de change est recommandé. Par exemple, supposons que l’appelant appelle Add() une fois par seconde avec des valeurs successives 1, 2, 4, 5, 4, 3. Si l’outil de collecte est mis à jour toutes les trois secondes, le total après trois secondes est 1+2+4=7 et le total après six secondes est 1+2+4+5+4+3=19. Le taux de change étant le (current_total - previous_total, c’est à dire le total actuel - le total précédent), à trois secondes, l’outil signale 7-0=7, et après six secondes, il signale 19-7=12.

  • UpDownCounter (CreateUpDownCounter) : cet instrument effectue le suivi d’une valeur qui peut augmenter ou diminuer au fil du temps. L’appelant signale les incréments et les décréments à l’aide de Add. Par exemple, supposons que l’appelant appelle Add() une fois par seconde avec des valeurs successives 1, 5, -2, 3, -1, -3. Si l’outil de collecte est mis à jour toutes les trois secondes, le total après trois secondes est 1+5-2=4 et le total après six secondes est 1+5-2+3-1-3=3.

  • ObservableCounter (CreateObservableCounter) : cet instrument est similaire à Counter, sauf que l’appelant est maintenant responsable de la maintenance du total agrégé. L’appelant fournit un délégué de rappel lorsque l’ObservableCounter est créé et que le rappel est appelé chaque fois que les outils doivent observer le total actuel. Par exemple, si un outil de collecte est mis à jour toutes les trois secondes, la fonction de rappel est également appelée toutes les trois secondes. La plupart des outils indiqueront à la fois le total et le taux de change dans le total disponible. Si un seul peut être affiché, le taux de change est recommandé. Si le rappel retourne 0 lors de l’appel initial, 7 lorsqu’il est appelé à nouveau après trois secondes et 19 lorsqu’il est appelé après six secondes, l’outil signale ces valeurs inchangées comme totaux. Pour le taux de change, l’outil affiche 7-0=7 après trois secondes et 19-7=12 après six secondes.

  • ObservableUpDownCounter (CreateObservableUpDownCounter) : cet instrument est similaire à UpDownCounter, sauf que l’appelant est maintenant responsable de la maintenance du total agrégé. L’appelant fournit un délégué de rappel lorsque l’ObservableCounter est créé et que le rappel est appelé chaque fois que les outils doivent observer le total actuel. Par exemple, si un outil de collecte est mis à jour toutes les trois secondes, la fonction de rappel est également appelée toutes les trois secondes. Quelle que soit la valeur retournée par le rappel, l’outil de collecte n’est pas modifié en tant que total.

  • ObservableGauge (CreateObservableGauge) : cet instrument permet à l’appelant de fournir un rappel où la valeur mesurée est transmise directement en tant que métrique. Chaque fois que l’outil de collecte est mis à jour, le rappel est appelé et toute valeur retournée par le rappel s’affiche dans l’outil.

  • Histogram (CreateHistogram) : cet instrument suit la distribution des mesures. Il n’existe pas de méthode canonique unique pour décrire un ensemble de mesures, mais il est recommandé d’utiliser des histogrammes ou des centiles calculés. Par exemple, supposons que l’appelant a appelé Record pour enregistrer ces mesures pendant l’intervalle de mise à jour de l’outil de collecte : 1,5,2,3,10,9,7,4,6,8. Un outil de collecte peut signaler que les 50e, 90e et 95e centiles de ces mesures sont respectivement 5, 9 et 9.

Meilleures pratiques lors de la sélection d’un type d’instrument

  • Pour compter des éléments ou toute autre valeur qui augmente uniquement au fil du temps, utilisez Counter ou ObservableCounter. Choisissez entre Counter et ObservableCounter en fonction de ce qui est plus facile à ajouter au code existant : soit un appel d’API pour chaque opération d’incrémentation, soit un rappel qui lira le total actuel à partir d’une variable que le code gère. Dans les chemins de code extrêmement chauds où les performances sont importantes et où l’utilisation de Add créerait plus d’un million d’appels par seconde par thread, l’utilisation d’ObservableCounter peut offrir plus de possibilités d’optimisation.

  • Pour les éléments de minutage, Histogram est généralement préféré. Il est souvent utile de comprendre la fin de ces distributions (90e, 95e, 99e centile) plutôt que les moyennes ou les totaux.

  • D’autres cas courants, tels que les taux de correspondance dans le cache ou la taille des caches, des files d’attente et des fichiers, sont généralement bien adaptés à UpDownCounter ou ObservableUpDownCounter. Faites votre choix en fonction de ce qui est plus facile à ajouter au code existant : soit un appel d’API pour chaque opération d’incrémentation et de décrémentation, soit un rappel qui lira le total actuel à partir d’une variable que le code gère.

Notes

Si vous utilisez une version antérieure de .NET ou un package NuGet DiagnosticSource qui ne prend pas en charge UpDownCounter et ObservableUpDownCounter (avant la version 7), ObservableGauge est souvent un bon substitut.

Exemple de différents types d’instruments

Arrêtez l’exemple de processus démarré précédemment et remplacez l’exemple de code dans Program.cs par :

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

Exécutez le nouveau processus et utilisez dotnet-counters comme précédemment dans un deuxième interpréteur de commandes pour afficher les métriques :

> 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

Cet exemple utilise des nombres générés de manière aléatoire afin que vos valeurs varient un peu. Vous pouvez voir que hatco.store.hats_sold (Counter) et hatco.store.coats_sold (ObservableCounter) s’affichent tous les deux sous la forme d’un taux. ObservableGauge, hatco.store.orders_pending, apparaît en tant que valeur absolue. Dotnet-counters affiche les instruments Histogram sous forme de statistiques à trois centiles (50e, 95e et 99e), mais d’autres outils peuvent résumer la distribution différemment ou offrir davantage d’options de configuration.

Meilleures pratiques

  • Les histogrammes ont tendance à stocker beaucoup plus de données en mémoire que d’autres types de métriques, mais l’utilisation exacte de la mémoire est déterminée par l’outil de collecte utilisé. Si vous définissez un grand nombre (>100) de métriques Histogram, vous devrez peut-être conseiller aux utilisateurs de ne pas les activer tous en même temps, ou de configurer leurs outils pour économiser de la mémoire en réduisant la précision. Certains outils de collecte peuvent avoir des limites strictes quant au nombre d’histogrammes simultanés qu’ils surveilleront pour éviter une utilisation excessive de la mémoire.

  • Les rappels de tous les instruments observables étant appelés dans l’ordre, tout rappel qui prend beaucoup de temps peut retarder ou empêcher la collecte de toutes les métriques. Favorisez la lecture rapide d’une valeur mise en cache, le retour d’aucune mesure ou la levée d’une exception plutôt que d’effectuer une opération potentiellement longue ou bloquante.

  • Les fonctions CreateObservableGauge et CreateObservableCounter retournent un objet instrument, mais dans la plupart des cas, vous n’avez pas besoin de l’enregistrer dans une variable, car aucune autre interaction avec l’objet n’est nécessaire. Son affectation à une variable statique comme nous l’avons fait pour les autres instruments est légale, mais sujette aux erreurs, car l’initialisation statique C# est paresseuse et la variable n’est généralement jamais référencée. Voici un exemple de problème :

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

Descriptions et unités

Les instruments peuvent spécifier des descriptions et des unités facultatives. Ces valeurs sont opaques pour tous les calculs de métriques, mais peuvent être affichées dans l’interface utilisateur de l’outil de collecte pour aider les ingénieurs à comprendre comment interpréter les données. Arrêtez l’exemple de processus démarré précédemment et remplacez l’exemple de code dans Program.cs par :

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

Exécutez le nouveau processus et utilisez dotnet-counters comme précédemment dans un deuxième interpréteur de commandes pour afficher les métriques :

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

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

dotnet-counters n’utilise pas actuellement le texte de description dans l’interface utilisateur, mais il affiche l’unité quand elle est fournie. Dans ce cas, vous voyez que « {hats} » a remplacé le terme générique « Count » visible dans les descriptions précédentes.

Bonnes pratiques

  • Les API .NET permettent d’utiliser n’importe quelle chaîne comme unité, mais nous recommandons d’utiliser UCUM, une norme internationale pour les noms d’unités. Les accolades autour de « {hats} » font partie de la norme UCUM, indiquant qu’il s’agit d’une annotation descriptive plutôt que d’un nom d’unité avec une signification standardisée comme des secondes ou des octets.

  • L’unité spécifiée dans le constructeur doit décrire les unités appropriées pour une mesure individuelle. Cela diffère parfois des unités de la métrique finale. Dans cet exemple, chaque mesure étant un nombre de chapeaux. « {hats} » est donc l’unité appropriée à communiquer dans le constructeur. L’outil de collecte a calculé un taux et a compris de lui-même que l’unité appropriée pour la métrique calculée est {hats}/s.

  • Lors de l’enregistrement des mesures de temps, préférez les unités de secondes enregistrées sous forme de virgule flottante ou de valeur double.

Métriques multidimensionnelles

Les mesures peuvent également être associées à des paires clé-valeur appelées balises qui permettent de classer les données à des fins d’analyse. Par exemple, HatCo peut souhaiter enregistrer non seulement le nombre de chapeaux vendus, mais également leur taille et leur couleur. Lors de l’analyse des données ultérieurement, les ingénieurs HatCo peuvent diviser les totaux par taille, couleur ou toute combinaison des deux.

Les balises Counter et Histogram peuvent être spécifiées dans des surcharges de Add et Record qui prennent un ou plusieurs arguments KeyValuePair. Par exemple :

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

Remplacez le code de Program.cs et réexécutez l’application et les dotnet-counters comme auparavant :

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 affiche désormais une catégorisation de 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

Pour ObservableCounter et ObservableGauge, les mesures balisées peuvent être fournies dans le rappel communiqué au constructeur :

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

Lors de l’exécution avec dotnet-counters comme auparavant, le résultat est :

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

Meilleures pratiques

  • Bien que l’API autorise n’importe quel objet à être utilisé comme valeur de balise, les outils de collecte anticipent l’utilisation de types numériques et de chaînes. D’autres types peuvent ou non être pris en charge par un outil de collecte donné.

  • Nous recommandons de suivre les instructions d’affectation de noms d’OpenTelemetry pour les noms d’étiquette, qui utilisent des noms hiérarchiques en pointillés minuscules avec des caractères « _ » pour séparer plusieurs mots dans le même élément. Si les noms d’étiquette sont réutilisés dans différentes métriques ou d’autres enregistrements de télémétrie, ils doivent avoir la même signification et un ensemble de valeurs légales partout où ils sont utilisés.

    Exemples de noms d’étiquette :

    • customer.country
    • store.payment_method
    • store.purchase_result
  • Méfiez-vous d’avoir des combinaisons très volumineuses ou non limitées de valeurs de balise enregistrées dans la pratique. Bien que l’implémentation de l’API .NET puisse le gérer, les outils de collecte vont probablement allouer le stockage pour les données de métrique associées à chaque combinaison de balises et cela peut devenir très volumineux. Par exemple, c’est parfait si HatCo a 10 couleurs de chapeau différentes et 25 tailles de chapeau pour un total de ventes allant jusqu’à 10*25=250 à suivre. Toutefois, si HatCo ajoute une troisième balise qui est un CustomerID pour la vente et qu’il vend à 100 millions de clients dans le monde entier, il est maintenant probable que des milliards de combinaisons de balises différentes soient enregistrées. La plupart des outils de collecte de métriques suppriment les données pour rester dans les limites techniques ou peuvent entraîner des coûts importants pour couvrir le stockage et le traitement des données. L’implémentation de chaque outil de collecte déterminera ses limites, mais il est probablement sûr d’avoir moins de 1 000 combinaisons pour un instrument. Tout ce qui dépasse 1000 combinaisons nécessite que l’outil de collecte applique un filtrage ou soit conçu pour fonctionner à grande échelle. Les implémentations d’histogrammes ont tendance à utiliser beaucoup plus de mémoire que d’autres métriques, de sorte que les limites de sécurité peuvent être 10 à 100 fois inférieures. Si vous prévoyez un grand nombre de combinaisons d’étiquettes uniques, les journaux, les bases de données transactionnelles ou les systèmes de traitement du Big Data peuvent être des solutions plus appropriées pour fonctionner à l’échelle nécessaire.

  • Pour les instruments qui auront un très grand nombre de combinaisons de balises, préférez utiliser un type de stockage plus petit pour réduire la surcharge de mémoire. Par exemple, le stockage de short pour un Counter<short> n’occupe que 2 octets par combinaison de balises, tandis qu’un double pour Counter<double> occupe 8 octets par combinaison de balises.

  • Les outils de collecte sont encouragés à optimiser le code qui spécifie le même ensemble de noms de balises dans le même ordre pour chaque appel afin d’enregistrer des mesures sur le même instrument. Pour du code hautes performances qui doit appeler Add et Record fréquemment, préférez utiliser la même séquence de noms de balises pour chaque appel.

  • L’API .NET est optimisée pour être sans allocation pour les appels Add et Record avec trois balises ou moins spécifiées individuellement. Pour éviter les allocations avec un plus grand nombre de balises, utilisez TagList. En général, la surcharge de performances de ces appels augmente à mesure que d’autres balises sont utilisées.

Notes

OpenTelemetry fait référence aux balises en tant qu’« attributs ». Il s’agit de deux noms différents pour la même fonctionnalité.

Tester les mesures personnalisées

Il est possible de tester les métriques personnalisées que vous ajoutez à l’aide de MetricCollector<T>. Ce type permet d’enregistrer facilement les mesures à partir d’instruments spécifiques et d’affirmer que les valeurs étaient correctes.

Tester avec l’injection de dépendances

Le code suivant montre un exemple de cas de test pour les composants de code qui utilisent l’injection de dépendances et 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();
    }
}

Chaque objet MetricCollector enregistre toutes les mesures d’un instrument. Si vous devez vérifier les mesures de plusieurs instruments, créez un MetricCollector pour chacun d’eux.

Tester sans injection de dépendances

Il est également possible de tester le code qui utilise un objet de compteur global partagé dans un champ statique, mais assurez-vous que ces tests ne sont pas configurés pour s’exécuter en parallèle. Étant donné que l’objet de compteur est partagé, MetricCollector dans un test observe les mesures créées à partir de tous les autres tests exécutés en parallèle.

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