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 les 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.
Remarque
.NET a des API de métrique 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 une 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+ 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 Counter nommé « hatco.store.hats_sold ». Au cours de chaque transaction simulée, le code appelle Add pour enregistrer la mesure des chapeaux vendus, soit 4 dans ce cas. L’instrument « hatco.store.hats_sold » définit implicitement certaines métriques qui pourraient être calculées à partir de ces mesures, telles que le nombre total de chapeaux vendus ou de chapeaux vendus/s. 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 comptage, la convention veut que les outils de collecte affichent le nombre total et/ou le taux d'augmentation du nombre.
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 quel byte
, short
, int
, long
, float
, double
ou decimal
en fonction de la taille des données que vous devez stocker et si des valeurs fractionnelles sont nécessaires.
Exécutez l’application et laissez-la en cours d’exécution pour l’instant. Nous allons voir les métriques suivantes.
> dotnet run
Press any key to exit
Meilleures pratiques
Pour le code qui n'est pas conçu pour être utilisé dans un conteneur d'injection de dépendance (DI), créez le mètre une fois et stockez-le dans une variable statique. Les variables statiques sont considérées comme un anti-modèle pour l'utilisation dans les bibliothèques compatibles avec la norme DI et l'exemple 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 Meter plutôt que de réutiliser un Meter existant si vous prévoyez que les développeurs d’applications apprécieront d'activer et de désactiver facilement les groupes d'indicateurs indépendamment.
Le nom transmis au constructeur de Meter doit être unique pour le distinguer des autres compteurs. Nous vous recommandons les consignes de nommage OpenTelemetry , qui utilisent des noms hiérarchiques séparés par des points. Les noms d'assemblages ou d'espaces noms du code instrumenté sont généralement un bon choix. Si un assemblage ajoute l'instrumentation du code dans un second assemblage indépendant, le nom doit être basé sur l'assemblage qui définit le compteur, et non sur l'assemblage dont le code est instrumenté.
.NET n’applique aucun schéma de nommage pour Instruments, mais nous vous recommandons de suivre instructions d’affectation de noms 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 mesure ne conservent pas le nom du compteur dans le nom final de la mesure, il est donc utile de rendre le nom de l'instrument globalement unique.
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 dans <10 ns lorsqu’aucune donnée n’est collectée, ou des dizaines à des centaines de nanosecondes lorsque des mesures sont collectées par une bibliothèque ou un outil de collection hautes performances. Cela permet à ces API d’être utilisées de manière libérale dans la plupart des cas, mais prenez soin du code qui est 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, ce qui est utile pour l’analyse ad hoc. Vous pouvez également consulter le didacticiel de 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 par seconde.
Obtenir un compteur via l'injection de dépendance
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 DI, comme ASP.NET Core ou les applications avec Generic Host, créez l'objet Meter à l'aide de IMeterFactory. À compter de .NET 8, les hôtes inscrivent automatiquement IMeterFactory dans le conteneur de service ou vous pouvez inscrire manuellement le type dans n’importe quel IServiceCollection en appelant AddMetrics.
La fabrique de service intègre les mesures avec DI, en gardant les Meter dans différentes collections de services isolés les uns des autres, même s'ils 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 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);
}
}
Enregistrez le type avec le conteneur DI dans Program.cs
.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<HatCoMetrics>();
Injectez le type de métriques et les valeurs d’enregistrement si nécessaire. Étant donné que le type de métriques est inscrit 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);
});
Meilleures 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 disposant lorsque le conteneur DI est disposé. Il n’est pas nécessaire d’ajouter du code supplémentaire pour appelerDispose()
sur leMeter
, et il n’aura aucun effet.
Types d’instruments
Jusqu’à présent, nous n’avons démontré qu’un instrument Counter<T>, mais il existe plus de types d’instruments disponibles. Les instruments diffèrent de deux façons :
- calculs de métriques par défaut : les outils qui collectent et analysent les mesures d’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 doivent être agrégées à partir de nombreuses mesures. Une option est que 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 d’agrégation et les fournir à la demande dans un rappel.
Types d’instruments actuellement disponibles :
Counter (CreateCounter) - Cet instrument suit une valeur qui augmente avec le temps et l'appelant rapporte les incréments en utilisant Add. La plupart des outils calculent le total et le taux de modification dans le total. Pour les outils qui n’affichent qu’une seule chose, le taux de modification 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 de 1+2+4=7 et le total après six secondes est de 1+2+4+4+3=19. Le taux de modification est le (current_total - previous_total), donc à trois secondes, l’outil signale 7-0=7, et après six secondes, il signale 19-7=12.UpDownCounter (CreateUpDownCounter) : cet instrument suit une valeur qui peut augmenter ou diminuer au fil du temps. L’appelant signale les incréments et les décréments en utilisant 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 de 1+5-2+3-1-3=3.ObservableCounter (CreateObservableCounter) : cet instrument est similaire à Counter, sauf que l’appelant est maintenant responsable du maintien du total agrégé. L'appelant fournit un délégué de rappel lors de la création de l'ObservableCounter, et le rappel est déclenché 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 auront à la fois le total et le taux de variation du total disponible. Si un seul peut être affiché, le taux de change est recommandé. Si le rappel retourne 0 sur 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 en tant que totaux. Pour le taux de modification, 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, elle sera affichée telle quelle dans l’outil de collecte comme total.
jauge (CreateGauge) : cet instrument permet à l’appelant de définir la valeur actuelle de la métrique à l’aide de la méthode Record. La valeur peut être mise à jour à tout moment en appelant à nouveau la méthode et un outil de collecte de métriques affiche la valeur qui a été définie le plus récemment.
ObservableGauge (CreateObservableGauge) : cet instrument permet à l’appelant de fournir un rappel dans lequel 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 la valeur retournée, quelle qu’elle soit, s’affiche dans l’outil.
histogramme (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 les outils sont recommandés pour 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.
Remarque
Pour plus d'informations sur la définition des limites de compartiment recommandées lors de la création d'un instrument histogramme, consultez : Conseils pour personnaliser les instruments histogrammes.
Meilleures pratiques lors de la sélection d’un type d’instrument
Pour compter les é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 : un appel d’API pour chaque opération d’incrémentation ou un rappel qui lit 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 l’utilisation de Add créerait plus d’un million d’appels par seconde par thread, l’utilisation de ObservableCounter peut offrir plus d’opportunité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 centile, 99e centile) plutôt que les moyennes ou les totaux.
D’autres cas courants, tels que les taux d’accès au cache ou les tailles des caches, des files d’attente et des fichiers, sont généralement bien adaptés aux
UpDownCounter
ouObservableUpDownCounter
. Choisissez entre eux selon 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 lit la valeur actuelle d’une variable que le code gère.
Remarque
Si vous utilisez une version antérieure de .NET ou d’un package NuGet DiagnosticSource qui ne prend pas en charge les UpDownCounter
et les 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(5, 15)/1000.0);
}
}
}
Exécutez le nouveau processus et utilisez dotnet-counters comme avant 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
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
Cet exemple utilise des nombres générés de manière aléatoire afin que vos valeurs varient un peu. Dotnet-counters restitue les instruments histogrammes sous forme de trois statistiques de centile (50e, 95e et 99e), mais d’autres outils peuvent résumer la distribution différemment ou offrir plus 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. Toutefois, l’utilisation exacte de la mémoire est déterminée par l’outil de collection utilisé. Si vous définissez un grand nombre (>100) de métriques histogrammes, vous devrez peut-être fournir aux utilisateurs des conseils pour ne pas les activer en même temps, ou pour configurer leurs outils pour économiser de la mémoire en réduisant la précision. Certains outils de collection peuvent avoir des limites strictes sur le nombre d’histogrammes simultanés qu’ils surveillent pour empêcher l’utilisation excessive de la mémoire.
Les rappels pour tous les instruments observables sont appelés en séquence, de sorte que tout rappel qui prend beaucoup de temps peut retarder ou empêcher la collecte de toutes les métriques. Favoriser la lecture rapide d'une valeur mise en cache, le fait de ne renvoyer aucune mesure ou la levée d'une exception plutôt que l'exécution d'une opération potentiellement longue ou bloquante.
Les rappels ObservableCounter, ObservableUpDownCounter et ObservableGauge se produisent sur un thread qui n’est généralement pas synchronisé avec le code qui met à jour les valeurs. Il vous incombe de synchroniser l’accès à la mémoire ou d’accepter les valeurs incohérentes qui peuvent résulter de l’utilisation d’un accès non synchronisé. Les approches courantes pour synchroniser l’accès sont d’utiliser un verrou ou un appel Volatile.Read et Volatile.Write.
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. L’affectation à une variable statique comme nous l’avons fait pour les autres instruments est légale mais sujette à des erreurs, car l’initialisation statique C# est différée 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 que vous avez 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 avant 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
Name Current Value
[HatCo.Store]
hatco.store.hats_sold ({hats}) 40
dotnet-counters n’utilise pas actuellement le texte de description dans l’interface utilisateur, mais il affiche l’unité lorsqu’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.
Meilleures pratiques
Les API .NET permettent d’utiliser n’importe quelle chaîne comme unité, mais nous vous 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 rapportée. Dans cet exemple, chaque mesure est un nombre de chapeaux, donc « {hats} » est l'unité appropriée à passer dans le constructeur. L’outil de collecte peut avoir calculé le taux de modification et dérivé par lui-même que l’unité appropriée pour la métrique de taux calculé 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 aux données d’être classées pour l’analyse. Par exemple, HatCo peut souhaiter enregistrer non seulement le nombre de chapeaux vendus, mais aussi la taille et la couleur qu’ils étaient. Lors de l’analyse des données ultérieurement, les ingénieurs HatCo peuvent décomposer les totaux par taille, couleur ou 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 compteurs dotnet comme précédemment :
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
Name Current Value
[HatCo.Store]
hatco.store.hats_sold (Count)
product.color product.size
blue 19 73
red 12 146
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 avant, le résultat est :
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
Meilleures pratiques
Bien que l’API autorise l’utilisation d’un objet comme valeur de balise, les types numériques et les chaînes sont attendus par les outils de collection. D’autres types peuvent ou non être pris en charge par un outil de collection donné.
Nous recommandons que les noms de balise suivent les directives de nommage d'OpenTelemetry, qui utilisent des noms hiérarchiques en minuscules pointillées avec des caractères '_' pour séparer plusieurs mots dans le même élément. Si les noms d’étiquettes 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 de balises :
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 la gérer, les outils de collecte allouent probablement le stockage pour les données de métriques associées à chaque combinaison d’étiquettes et cela peut devenir très volumineux. Par exemple, il est acceptable que HatCo ait 10 couleurs de chapeaux différentes et 25 tailles de chapeaux, ce qui fait jusqu'à 10*25=250 totaux de ventes à suivre. Toutefois, si HatCo ajoute une troisième balise qui est un ID client pour la vente et qu'ils vendent à 100 millions de clients dans le monde entier, cela crée maintenant des milliards de combinaisons différentes de balises enregistrées. La plupart des outils de collecte de métriques suppriment les données pour rester dans des limites techniques ou il peut y avoir des coûts monétaires importants pour couvrir le stockage et le traitement des données. L’implémentation de chaque outil de collection détermine ses limites, mais il est probable que moins de 1 000 combinaisons pour un instrument soient sécurisées. Tout ce qui précède 1000 combinaisons nécessite que l’outil de collecte applique le filtrage ou qu’il 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 sécurisées peuvent être de 10 à 100 fois plus basses. 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 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 d’étiquettes, préférez utiliser un type de stockage plus petit pour réduire la surcharge mémoire. Par exemple, le stockage de la
short
pour unCounter<short>
occupe uniquement 2 octets par combinaison d’étiquettes, tandis qu’undouble
pourCounter<double>
occupe 8 octets par combinaison d’étiquettes.Les outils de collecte sont encouragés à optimiser le code qui spécifie le même ensemble de noms d’étiquettes dans le même ordre pour chaque appel pour enregistrer les mesures sur le même instrument. Pour le 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 lors des appels Add et Record quand trois balises ou moins sont spécifiées individuellement. Pour éviter les allocations avec un plus grand nombre de balises, utilisez TagList. En général, la surcharge des performances de ces appels augmente à mesure que d’autres balises sont utilisées.
Remarque
OpenTelemetry fait référence aux balises en tant qu’attributs. Il s’agit de deux noms différents pour la même fonctionnalité.
Utilisation de conseils pour personnaliser les instruments d’histogramme
Lors de l’utilisation d’Histogrammes, il incombe à l’outil ou à la bibliothèque de collecter les données pour décider comment représenter le mieux la distribution des valeurs enregistrées. Une stratégie courante (et le mode par défaut lors de l’utilisation d’OpenTelemetry) consiste à diviser la plage de valeurs possibles en sous-plages appelées compartiments et à signaler le nombre de valeurs enregistrées dans chaque compartiment. Par exemple, un outil peut diviser les nombres en trois compartiments, ceux inférieurs à 1, ceux compris entre 1 et 10 et ceux supérieurs à 10. Si votre application a enregistré les valeurs 0,5, 6, 0,1 et 12, il y aurait deux points de données dans le premier compartiment, un dans le second et un dans le troisième.
L'outil ou la bibliothèque qui collecte les données de l'histogramme est responsable de la définition des godets qu'il utilisera. La configuration du compartiment par défaut lors de l’utilisation d’OpenTelemetry est la suivante : [ 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 ]
.
Les valeurs par défaut peuvent ne pas entraîner la meilleure granularité pour chaque histogramme. Par exemple, les durées de requêtes inférieures à une seconde sont toutes comprises dans la catégorie 0
.
L’outil ou la bibliothèque qui collecte les données histogrammes peuvent offrir des mécanismes permettant aux utilisateurs de personnaliser la configuration du compartiment. Par exemple, OpenTelemetry définit une API View. Cela nécessite toutefois une action de l’utilisateur final et rend la responsabilité de l’utilisateur de bien comprendre la distribution des données suffisamment pour choisir les compartiments appropriés.
Pour améliorer l’expérience, la version 9.0.0
du package System.Diagnostics.DiagnosticSource
a introduit l’API (InstrumentAdvice<T>).
L’API InstrumentAdvice
peut être utilisée par les auteurs d’instrumentation pour spécifier l’ensemble de limites de compartiment par défaut recommandées pour un histogramme donné. L’outil ou la bibliothèque qui collecte les données histogrammes peuvent ensuite choisir d’utiliser ces valeurs lors de la configuration de l’agrégation, ce qui entraîne une expérience d’intégration plus transparente pour les utilisateurs. Cela est pris en charge dans le kit de développement logiciel (SDK) .NET OpenTelemetry à partir de la version 1.10.0.
Important
En général, d’autres compartiments entraînent des données plus précises pour un histogramme donné, mais chaque compartiment nécessite de la mémoire pour stocker les détails agrégés et le coût du processeur est de trouver le compartiment approprié lors du traitement d’une mesure. Il est important de comprendre les compromis entre la précision et la consommation de processeur/mémoire lors du choix du nombre de compartiments à recommander via l’API InstrumentAdvice
.
Le code suivant montre un exemple utilisant l’API InstrumentAdvice
pour définir les compartiments par défaut recommandés.
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);
}
}
}
Informations supplémentaires
Pour plus d’informations sur les histogrammes de compartiment explicites dans OpenTelemetry, consultez :
Tester des métriques 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 seul 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 Meter 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);
}
}