Partage via


Métriques générées par la source avec des balises fortement typées

Les applications .NET modernes peuvent capturer des métriques à l’aide de l’API System.Diagnostics.Metrics . Ces métriques incluent souvent un contexte supplémentaire sous la forme de paires clé-valeur appelées balises (parfois appelées dimensions dans les systèmes de télémétrie). Cet article montre comment utiliser un générateur de source au moment de la compilation pour définir des balises de métriques fortement typées (TagNames) et des types et méthodes d’enregistrement de métriques. En utilisant des balises fortement typées, vous éliminez le code standard répétitif et assurez que les métriques liées partagent le même ensemble de noms d’étiquettes avec une sécurité vérifiée à la compilation. L’avantage principal de cette approche est d’améliorer la productivité des développeurs et la sécurité des types.

Remarque

Dans le contexte des métriques, une balise est parfois appelée « dimension ». Cet article utilise la « balise » pour la clarté et la cohérence avec la terminologie des métriques .NET.

Commencez

Pour commencer, installez le 📦 package NuGet Microsoft.Extensions.Telemetry.Abstractions :

dotnet add package Microsoft.Extensions.Telemetry.Abstractions

Pour plus d’informations, consultez dotnet add package or Manage package dependencies in .NET applications.

Paramètres par défaut et personnalisation du nom de balise

Par défaut, le générateur de source dérive les noms des balises de métriques à partir des noms des champs et des propriétés de votre classe de balises. En d’autres termes, chaque champ ou propriété public dans l’objet de balise fortement typé devient un nom de balise par défaut. Vous pouvez le remplacer à l’aide du champ ou de la TagNameAttribute propriété pour spécifier un nom d’étiquette personnalisé. Dans les exemples ci-dessous, vous verrez les deux approches en action.

Exemple 1 : Métrique de base avec une seule balise

L’exemple suivant illustre une métrique de compteur simple avec une seule balise. Dans ce scénario, nous voulons compter le nombre de requêtes traitées et les classer par une Region balise :

public struct RequestTags
{
    public string Region { get; set; }
}

public static partial class MyMetrics
{
    [Counter<int>(typeof(RequestTags))]
    public static partial RequestCount CreateRequestCount(Meter meter);
}

Dans le code précédent, RequestTags est un struct de balise fortement typé avec une propriété Region unique. La méthode CreateRequestCount est marquée avec CounterAttribute<T>T est un int, indiquant qu'elle génère un instrument Counter qui suit les valeurs int. L’attribut référence typeof(RequestTags), ce qui signifie que le compteur utilise les balises définies dans RequestTags lors de l’enregistrement des métriques. Le générateur source produit une classe d'instrument fortement typée (nommée RequestCount) avec une méthode Add qui accepte une valeur entière et un objet RequestTags.

Pour utiliser la métrique générée, créez une Meter mesure et enregistrez des mesures, comme indiqué ci-dessous :

Meter meter = new("MyCompany.MyApp", "1.0");
RequestCount requestCountMetric = MyMetrics.CreateRequestCount(meter);

// Create a tag object with the relevant tag value
var tags = new RequestTags { Region = "NorthAmerica" };

// Record a metric value with the associated tag
requestCountMetric.Add(1, tags);

Dans cet exemple d’utilisation, l’appel MyMetrics.CreateRequestCount(meter) crée un instrument de compteur (via le Meter) et retourne un RequestCount objet de métrique. Lorsque vous appelez requestCountMetric.Add(1, tags), le système de métriques enregistre un nombre de 1 associé à la balise Region="NorthAmerica". Vous pouvez réutiliser l’objet RequestTags ou en créer de nouveaux pour enregistrer le nombre de régions différentes, et le nom Region de la balise sera appliqué de manière cohérente à chaque mesure.

Exemple 2 : Métrique avec des objets de balise imbriqués

Pour les scénarios plus complexes, vous pouvez définir des classes d’étiquettes qui incluent plusieurs balises, objets imbriqués ou même des propriétés héritées. Cela permet à un groupe de métriques associées de partager efficacement un ensemble commun de balises. Dans l’exemple suivant, vous définissez un ensemble de classes d’étiquettes et les utilisez pour trois métriques différentes :

using Microsoft.Extensions.Diagnostics.Metrics;

namespace MetricsGen;

public class MetricTags : MetricParentTags
{
    [TagName("Dim1DimensionName")]
    public string? Dim1;                      // custom tag name via attribute
    public Operations Operation { get; set; } // tag name defaults to "Operation"
    public MetricChildTags? ChildTagsObject { get; set; }
}

public enum Operations
{
    Unknown = 0,
    Operation1 = 1,
}

public class MetricParentTags
{
    [TagName("DimensionNameOfParentOperation")]
    public string? ParentOperationName { get; set; }  // custom tag name via attribute
    public MetricTagsStruct ChildTagsStruct { get; set; }
}

public class MetricChildTags
{
    public string? Dim2 { get; set; }  // tag name defaults to "Dim2"
}

public struct MetricTagsStruct
{
    public string Dim3 { get; set; }   // tag name defaults to "Dim3"
}

Le code précédent définit l’héritage des métriques et les formes d’objet. Le code suivant montre comment utiliser ces formes avec le générateur, comme indiqué dans la Metric classe :

using System.Diagnostics.Metrics;
using Microsoft.Extensions.Diagnostics.Metrics;

public static partial class Metric
{
    [Histogram<long>(typeof(MetricTags))]
    public static partial Latency CreateLatency(Meter meter);

    [Counter<long>(typeof(MetricTags))]
    public static partial TotalCount CreateTotalCount(Meter meter);

    [Counter<int>(typeof(MetricTags))]
    public static partial TotalFailures CreateTotalFailures(Meter meter);
}

Dans cet exemple, MetricTags est une classe de balise qui hérite de MetricParentTags et contient également un objet de balise imbriqué (MetricChildTags) ainsi qu'une structure imbriquée (MetricTagsStruct). Les propriétés de balise illustrent les noms de balises par défaut et personnalisés :

  • Le Dim1 champ dans MetricTags a un attribut [TagName("Dim1DimensionName")], de sorte que son nom de balise sera "Dim1DimensionName".
  • La propriété Operation n’a pas d’attribut, donc son nom de balise par défaut est "Operation".
  • Dans MetricParentTags, la ParentOperationName propriété est remplacée par un nom "DimensionNameOfParentOperation"d’étiquette personnalisé.
  • La classe imbriquée MetricChildTags définit une Dim2 propriété (aucun attribut, nom "Dim2"de balise).
  • Le MetricTagsStruct struct définit un Dim3 champ (nom "Dim3"de balise).

Les trois définitions CreateLatencyde métriques, CreateTotalCountet CreateTotalFailures les utilisent MetricTags comme type d’objet de balise. Cela signifie que les types de métriques générés (Latency, TotalCountet TotalFailures) attendent toutes une MetricTags instance lors de l’enregistrement des données. Chacune de ces métriques aura le même ensemble de noms de balise :Dim1DimensionName, , Operation, Dim2, Dim3, et DimensionNameOfParentOperation.

Le code suivant montre comment créer et utiliser ces métriques dans une classe :

internal class MyClass
{
    private readonly Latency _latencyMetric;
    private readonly TotalCount _totalCountMetric;
    private readonly TotalFailures _totalFailuresMetric;

    public MyClass(Meter meter)
    {
        // Create metric instances using the source-generated factory methods
        _latencyMetric = Metric.CreateLatency(meter);
        _totalCountMetric = Metric.CreateTotalCount(meter);
        _totalFailuresMetric = Metric.CreateTotalFailures(meter);
    }

    public void DoWork()
    {
        var startingTimestamp = Stopwatch.GetTimestamp();
        bool requestSuccessful = true;
        // Perform some operation to measure
        var elapsedTime = Stopwatch.GetElapsedTime(startingTimestamp);

        // Create a tag object with values for all tags
        var tags = new MetricTags
        {
            Dim1 = "Dim1Value",
            Operation = Operations.Operation1,
            ParentOperationName = "ParentOpValue",
            ChildTagsObject = new MetricChildTags
            {
                Dim2 = "Dim2Value",
            },
            ChildTagsStruct = new MetricTagsStruct
            {
                Dim3 = "Dim3Value"
            }
        };

        // Record the metric values with the associated tags
        _latencyMetric.Record(elapsedTime.ElapsedMilliseconds, tags);
        _totalCountMetric.Add(1, tags);
        if (!requestSuccessful)
        {
            _totalFailuresMetric.Add(1, tags);
        }
    }
}

Dans la méthode précédente MyClass.DoWork , un MetricTags objet est rempli avec des valeurs pour chaque balise. Cet objet unique tags est ensuite passé aux trois instruments lors de l’enregistrement des données. La Latency métrique (histogramme) enregistre le temps écoulé, et les compteurs (TotalCount et TotalFailures) enregistrent les nombres d’occurrences. Étant donné que toutes les métriques partagent le même type d’objet d’étiquette, les balises (Dim1DimensionName, Operation, Dim2, Dim3, DimensionNameOfParentOperation) sont présentes sur chaque mesure.

Spécification d’unités

À compter de .NET 10.2, vous pouvez éventuellement spécifier une unité de mesure pour vos métriques à l’aide du Unit paramètre. Cela permet de fournir un contexte sur les mesures de métrique (par exemple, « secondes », « octets » et « demandes »). L’unité est transmise au sous-jacent Meter lors de la création de l’instrument.

Le code suivant montre comment utiliser le générateur avec des types primitifs avec des unités spécifiées :

public static partial class Metric
{
    [Histogram<long>(typeof(MetricTags), Unit = "ms")]
    public static partial Latency CreateLatency(Meter meter);

    [Counter<long>(typeof(MetricTags), Unit = "requests")]
    public static partial TotalCount CreateTotalCount(Meter meter);

    [Counter<int>(typeof(MetricTags), Unit = "failures")]
    public static partial TotalFailures CreateTotalFailures(Meter meter);
}

Considérations relatives aux performances

L’utilisation de balises fortement typées via la génération de source n’ajoute aucune surcharge par rapport à l’utilisation directe de métriques. Si vous devez réduire davantage les allocations pour les métriques à très haute fréquence, envisagez de définir votre objet de balise en tant que struct (type valeur) au lieu d’un class. L’utilisation d’un struct pour l’objet de balise peut éviter les allocations de segment lors de l’enregistrement des métriques, car les balises sont transmises par valeur.

Exigences de méthode de métrique générées

Lorsque vous définissez des méthodes de fabrique de métriques (les méthodes partielles décorées avec [Counter], [Histogram]etc.), le générateur source impose quelques exigences :

  • Chaque méthode doit être public static partial (pour que le générateur source fournisse l’implémentation).
  • Le type de retour de chaque méthode partielle doit être unique (afin que le générateur puisse créer un type nommé de manière unique pour la métrique).
  • Le nom de la méthode ne doit pas commencer par un trait de soulignement (_), et les noms de paramètres ne doivent pas commencer par un trait de soulignement.
  • Le premier paramètre doit être un Meter (il s’agit de l’instance de compteur utilisée pour créer l’instrument sous-jacent).
  • Les méthodes ne peuvent pas être génériques et ne peuvent pas avoir de paramètres génériques.
  • Les propriétés de balise de la classe de balise ne peuvent être de type string ou enum. Pour d’autres types (par exemple, bool ou types numériques), convertissez la valeur en chaîne avant de l’affecter à l’objet de balise.

L’adhésion à ces exigences garantit que le générateur source peut produire correctement les types de métriques et les méthodes.

Voir aussi