EventCounters dans .NET

Cet article s’applique à : ✔️ SDK .NET Core 3.0 et versions ultérieures

Les EventCounters sont des API .NET utilisées pour la collecte de métriques de performances légères, multiplateformes et en quasi-temps réel. EventCounters a été ajouté comme alternative multiplateforme aux « compteurs de performances » de .NET Framework sur Windows. Dans cet article, vous allez découvrir ce que sont les EventCounters, comment les implémenter et comment les consommer.

Le runtime .NET et quelques bibliothèques .NET publient des informations de diagnostic de base à l’aide d’EventCounters à partir de .NET Core 3.0. Outre les EventCounters fournis par le runtime .NET, vous pouvez choisir d’implémenter vos propres EventCounters. EventCounters peut être utilisé pour suivre différentes mesures. En savoir plus sur ceux-ci dans les EventCounters connus dans .NET

EventCounters en direct dans le cadre d’un EventSource et sont automatiquement envoyés à des outils d’écouteur régulièrement. Comme tous les autres événements sur un EventSource, ils peuvent être consommés à la fois dans et hors processeur via EventListener et EventPipe. Cet article se concentre sur les fonctionnalités multiplateformes d’EventCounters et exclut intentionnellement PerfView et ETW (Suivi d’événements pour Windows), bien que les deux puissent être utilisés avec EventCounters.

Image de diagramme EventCounters dans et hors processeur

Vue d’ensemble de l’API EventCounter

Il existe deux catégories principales d’EventCounters. Certains compteurs concernent des valeurs de « taux », telles que le nombre total d’exceptions, le nombre total de contrôleurs de groupe et le nombre total de requêtes. D’autres compteurs sont des valeurs de « capture instantanée », telles que l’utilisation du tas, l’utilisation du processeur et la taille des ensembles de travail. Dans chacune de ces catégories de compteurs, il existe deux types de compteurs qui varient selon la façon dont ils obtiennent leur valeur. Les compteurs d’interrogation récupèrent leur valeur via un rappel et les compteurs non-interrogation voient leur valeur directement définie sur l’instance de compteur.

Les compteurs sont représentés par les implémentations suivantes :

Un écouteur d’événements spécifie la durée des intervalles de mesure. À la fin de chaque intervalle, une valeur est transmise à l’écouteur pour chaque compteur. Les implémentations d’un compteur déterminent les API et les calculs utilisés pour produire la valeur chaque intervalle.

  • EventCounter enregistre un ensemble de valeurs. La méthode EventCounter.WriteMetric ajoute une nouvelle valeur à l’ensemble. Avec chaque intervalle, un résumé statistique pour l’ensemble est calculé, tel que le min, le max et la moyenne. L’outil dotnet-counters affiche toujours la valeur moyenne. EventCounter est utile pour décrire un ensemble discret d’opérations. L’utilisation courante peut inclure la surveillance de la taille moyenne en octets des opérations d’E/S récentes ou la valeur monétaire moyenne d’un ensemble de transactions financières.

  • IncrementingEventCounter enregistre un total en cours d’exécution pour chaque intervalle de temps. La méthode IncrementingEventCounter.Increment ajoute au total. Par exemple, si Increment() est appelée trois fois pendant un intervalle avec des valeurs 1, 2et 5, le total 8 est signalé comme valeur de compteur pour cet intervalle. L’outil dotnet-counters affiche le taux en tant que total/temps enregistré. IncrementingEventCounter est utile de mesurer la fréquence à laquelle une action se produit, par exemple le nombre de demandes traitées par seconde.

  • PollingCounter utilise un rappel pour déterminer la valeur signalée. Avec chaque intervalle de temps, la fonction de rappel fournie par l’utilisateur est appelée et la valeur de retour est utilisée comme valeur de compteur. Un PollingCounter peut être utilisé pour interroger une métrique à partir d’une source externe, par exemple obtenir les octets libres sur un disque. Il peut également être utilisé pour signaler des statistiques personnalisées qui peuvent être calculées à la demande par une application. Par exemple, il peut s’agir de la création de rapports sur le 95e centile des latences de requête récentes, ou le taux actuel d’accès ou d’absence d’un cache.

  • Le IncrementingPollingCounter utilise un rappel pour déterminer la valeur d’incrément signalée. Avec chaque intervalle de temps, le rappel est appelé, puis la différence entre l’appel actuel et la dernière invocation est la valeur signalée. L’outil dotnet-counters affiche toujours la différence en tant que taux, la valeur/temps signalé. Ce compteur est utile lorsqu’il n’est pas possible d’appeler une API sur chaque occurrence, mais il est possible d’interroger le nombre total d’occurrences. Par exemple, vous pouvez signaler le nombre d’octets écrits dans un fichier par seconde, même sans notification chaque fois qu’un octet est écrit.

Implémenter un EventSource

Le code suivant implémente un exemple EventSource exposé en tant que fournisseur nommé "Sample.EventCounter.Minimal". Cette source contient un EventCounter représentant le temps de traitement des demandes. Un tel compteur a un nom (c’est-à-dire son ID unique dans la source) et un nom complet, tous deux utilisés par les outils d’écouteur tels que dotnet-counter.

using System.Diagnostics.Tracing;

[EventSource(Name = "Sample.EventCounter.Minimal")]
public sealed class MinimalEventCounterSource : EventSource
{
    public static readonly MinimalEventCounterSource Log = new MinimalEventCounterSource();

    private EventCounter _requestCounter;

    private MinimalEventCounterSource() =>
        _requestCounter = new EventCounter("request-time", this)
        {
            DisplayName = "Request Processing Time",
            DisplayUnits = "ms"
        };

    public void Request(string url, long elapsedMilliseconds)
    {
        WriteEvent(1, url, elapsedMilliseconds);
        _requestCounter?.WriteMetric(elapsedMilliseconds);
    }

    protected override void Dispose(bool disposing)
    {
        _requestCounter?.Dispose();
        _requestCounter = null;

        base.Dispose(disposing);
    }
}

Vous utilisez dotnet-counters ps pour afficher la liste des processus .NET qui peuvent être surveillés :

dotnet-counters ps
   1398652 dotnet     C:\Program Files\dotnet\dotnet.exe
   1399072 dotnet     C:\Program Files\dotnet\dotnet.exe
   1399112 dotnet     C:\Program Files\dotnet\dotnet.exe
   1401880 dotnet     C:\Program Files\dotnet\dotnet.exe
   1400180 sample-counters C:\sample-counters\bin\Debug\netcoreapp3.1\sample-counters.exe

Transmettez le nom EventSource à l’option --counters permettant de commencer à surveiller votre compteur :

dotnet-counters monitor --process-id 1400180 --counters Sample.EventCounter.Minimal

L’exemple suivant montre la sortie du moniteur :

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

[Samples-EventCounterDemos-Minimal]
    Request Processing Time (ms)                            0.445

Appuyez sur q pour arrêter la commande de surveillance.

Compteurs conditionnels

Lors de l’implémentation d’un EventSource, les compteurs contenants peuvent être instanciés de manière conditionnelle lorsque la méthode EventSource.OnEventCommand est appelée avec une valeur Command de EventCommand.Enable. Pour instancier en toute sécurité une instance de compteur uniquement si elle est null, utilisez l’opérateur d’affectation de fusion null. En outre, les méthodes personnalisées peuvent évaluer la méthode IsEnabled pour déterminer si la source d’événement actuelle est activée ou non.

using System.Diagnostics.Tracing;

[EventSource(Name = "Sample.EventCounter.Conditional")]
public sealed class ConditionalEventCounterSource : EventSource
{
    public static readonly ConditionalEventCounterSource Log = new ConditionalEventCounterSource();

    private EventCounter _requestCounter;

    private ConditionalEventCounterSource() { }

    protected override void OnEventCommand(EventCommandEventArgs args)
    {
        if (args.Command == EventCommand.Enable)
        {
            _requestCounter ??= new EventCounter("request-time", this)
            {
                DisplayName = "Request Processing Time",
                DisplayUnits = "ms"
            };
        }
    }

    public void Request(string url, float elapsedMilliseconds)
    {
        if (IsEnabled())
        {
            _requestCounter?.WriteMetric(elapsedMilliseconds);
        }
    }

    protected override void Dispose(bool disposing)
    {
        _requestCounter?.Dispose();
        _requestCounter = null;

        base.Dispose(disposing);
    }
}

Conseil

Les compteurs conditionnels sont des compteurs qui sont instanciés de manière conditionnelle, une micro-optimisation. Le runtime adopte ce modèle pour les scénarios où les compteurs ne sont normalement pas utilisés, pour enregistrer une fraction de milliseconde.

Exemples de compteurs du runtime .NET Core

Il existe de nombreux exemples d’implémentations dans le runtime .NET Core. Voici l’implémentation du runtime pour le compteur qui suit la taille du jeu de travail de l’application.

var workingSetCounter = new PollingCounter(
    "working-set",
    this,
    () => (double)(Environment.WorkingSet / 1_000_000))
{
    DisplayName = "Working Set",
    DisplayUnits = "MB"
};

Le rapport PollingCounter indique la quantité actuelle de mémoire physique mappée au processus (jeu de travail) de l’application, car elle capture une métrique à un moment donné. Le rappel d’interrogation d’une valeur est l’expression lambda fournie, qui est simplement un appel à l’API System.Environment.WorkingSet. DisplayName et DisplayUnits sont des propriétés facultatives qui peuvent être définies pour aider le côté consommateur du compteur à afficher plus clairement la valeur. Par exemple, dotnet-counters utilise ces propriétés pour afficher la version plus conviviale des noms de compteurs.

Important

Les propriétés DisplayName ne sont pas localisées.

Pour PollingCounter et IncrementingPollingCounter, rien d’autre ne doit être fait. Ils interrogent les valeurs elles-mêmes selon l’intervalle demandé par le consommateur.

Voici un exemple de compteur d’exécution implémenté à l’aide de IncrementingPollingCounter.

var monitorContentionCounter = new IncrementingPollingCounter(
    "monitor-lock-contention-count",
    this,
    () => Monitor.LockContentionCount
)
{
    DisplayName = "Monitor Lock Contention Count",
    DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};

L’API IncrementingPollingCounter utilise l’API Monitor.LockContentionCount pour signaler l’incrément du nombre total de conflits de verrous. La propriété DisplayRateTimeScale est facultative, mais lorsqu’elle est utilisée, elle peut fournir un indicateur pour l’intervalle de temps dans lequel le compteur est le mieux affiché. Par exemple, le nombre de contentions de verrous est le mieux affiché comme nombre par seconde. Son DisplayRateTimeScale est donc défini sur une seconde. Le taux d’affichage peut être ajusté pour différents types de compteurs de taux.

Notes

Le DisplayRateTimeScalen’est pas utilisé par les dotnet-counters et les écouteurs d’événements ne sont pas requis pour l’utiliser.

Il existe davantage d’implémentations de compteurs à utiliser comme référence dans le référentiel du runtime .NET.

Accès concurrentiel

Conseil

L’API EventCounters ne garantit pas la sécurité des threads. Lorsque les délégués passés aux instances PollingCounter ou IncrementingPollingCounter sont appelés par plusieurs threads, il vous incombe de garantir la sécurité des threads des délégués.

Par exemple, tenez compte des éléments EventSource suivants pour effectuer le suivi des demandes.

using System;
using System.Diagnostics.Tracing;

public class RequestEventSource : EventSource
{
    public static readonly RequestEventSource Log = new RequestEventSource();

    private IncrementingPollingCounter _requestRateCounter;
    private long _requestCount = 0;

    private RequestEventSource() =>
        _requestRateCounter = new IncrementingPollingCounter("request-rate", this, () => _requestCount)
        {
            DisplayName = "Request Rate",
            DisplayRateTimeScale = TimeSpan.FromSeconds(1)
        };

    public void AddRequest() => ++ _requestCount;

    protected override void Dispose(bool disposing)
    {
        _requestRateCounter?.Dispose();
        _requestRateCounter = null;

        base.Dispose(disposing);
    }
}

La méthode AddRequest() peut être appelée à partir d’un gestionnaire de requêtes et RequestRateCounter interroge la valeur selon l’intervalle spécifié par le consommateur du compteur. Toutefois, la méthode AddRequest() peut être appelée par plusieurs threads à la fois, en plaçant une condition de concurrence sur _requestCount. Un autre moyen sûr pour le thread d’incrémenter le _requestCount est d’utiliser Interlocked.Increment.

public void AddRequest() => Interlocked.Increment(ref _requestCount);

Pour empêcher les lectures déchirées (sur les architectures 32 bits) de l’utilisation du champ long_requestCount par Interlocked.Read.

_requestRateCounter = new IncrementingPollingCounter("request-rate", this, () => Interlocked.Read(ref _requestCount))
{
    DisplayName = "Request Rate",
    DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};

Consommer EventCounters

Il existe deux méthodes principales de consommation d’EventCounters : dans et hors processeur. La consommation d’EventCounters peut être distinguée en trois couches de différentes technologies consommatrices.

  • Transport d’événements dans un flux brut via ETW ou EventPipe :

    Les API ETW sont fournies avec le système d’exploitation Windows et EventPipe sont accessibles en tant qu’API .NET ou le protocole IPC de diagnostic.

  • Décodage du flux d’événements binaires en événements :

    La bibliothèque TraceEvent gère les formats de flux ETW et EventPipe.

  • Outils de ligne de commande et d’interface utilisateur :

    Outils tels que PerfView (ETW ou EventPipe), dotnet-counters (EventPipe uniquement) et dotnet-monitor (EventPipe uniquement).

Consommer hors processus

La consommation d’EventCounters hors processeur est une approche courante. Vous pouvez utiliser des compteurs dotnet pour les consommer de manière multiplateforme via un EventPipe. L’outil dotnet-counters est un outil global dotnet CLI multiplateforme qui peut être utilisé pour surveiller les valeurs des compteurs. Pour savoir comment utiliser dotnet-counters pour surveiller vos compteurs, consultez dotnet-counters ou parcourez le tutoriel Mesurer les performances à l’aide d’EventCounters.

dotnet-trace

L’outil dotnet-trace peut être utilisé pour consommer les données de compteur par le biais d’un EventPipe. Voici un exemple utilisant dotnet-trace pour collecter des données de compteur.

dotnet-trace collect --process-id <pid> Sample.EventCounter.Minimal:0:0:EventCounterIntervalSec=1

Pour plus d’informations sur la collecte des valeurs de compteur au fil du temps, consultez la documentation dotnet-trace.

Azure Application Insights

EventCounters peut être consommé par Azure Monitor, en particulier Azure Application Insights. Les compteurs peuvent être ajoutés et supprimés et vous êtes libre de spécifier des compteurs personnalisés ou des compteurs connus. Pour plus d’informations, consultez Personnalisation des compteurs à collecter.

dotnet-monitor

L’outil dotnet-monitor facilite l’accès aux diagnostics à partir d’un processus .NET de manière distante et automatisée. En plus des traces, il peut surveiller les mesures, collecter les images mémoire et collecter les vidages GC. Il est distribué en tant qu’outil CLI et image Docker. Il expose une API REST et la collection d’artefacts de diagnostic se produit par le biais d’appels REST.

Pour plus d’informations, consultez dotnet-monitor.

Consommer dans le processus

Vous pouvez utiliser les valeurs des compteurs via l’API EventListener. EventListener constitue une méthode dans le processeur pour utiliser tout événement écrit par des instances d’une EventSource dans votre application. Pour plus d’informations sur l’utilisation de l’API EventListener, consultez EventListener.

Tout d’abord, la valeur EventSource du compteur doit être activée. Remplacez la méthode EventListener.OnEventSourceCreated pour obtenir une notification lors de la création d’une notification EventSource et, si EventSource est correct dans vos EventCounters, vous pouvez appeler EventListener.EnableEvents. Voici un exemple de remplacement :

protected override void OnEventSourceCreated(EventSource source)
{
    if (!source.Name.Equals("System.Runtime"))
    {
        return;
    }

    EnableEvents(source, EventLevel.Verbose, EventKeywords.All, new Dictionary<string, string>()
    {
        ["EventCounterIntervalSec"] = "1"
    });
}

Exemple de code

Voici un exemple de classe EventListener qui imprime tous les noms et valeurs des compteurs du EventSource du runtime .NET, pour la publication de ses compteurs internes (System.Runtime) toutes les secondes.

using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;

public class SimpleEventListener : EventListener
{
    public SimpleEventListener()
    {
    }

    protected override void OnEventSourceCreated(EventSource source)
    {
        if (!source.Name.Equals("System.Runtime"))
        {
            return;
        }

        EnableEvents(source, EventLevel.Verbose, EventKeywords.All, new Dictionary<string, string>()
        {
            ["EventCounterIntervalSec"] = "1"
        });
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (!eventData.EventName.Equals("EventCounters"))
        {
            return;
        }

        for (int i = 0; i < eventData.Payload.Count; ++ i)
        {
            if (eventData.Payload[i] is IDictionary<string, object> eventPayload)
            {
                var (counterName, counterValue) = GetRelevantMetric(eventPayload);
                Console.WriteLine($"{counterName} : {counterValue}");
            }
        }
    }

    private static (string counterName, string counterValue) GetRelevantMetric(
        IDictionary<string, object> eventPayload)
    {
        var counterName = "";
        var counterValue = "";

        if (eventPayload.TryGetValue("DisplayName", out object displayValue))
        {
            counterName = displayValue.ToString();
        }
        if (eventPayload.TryGetValue("Mean", out object value) ||
            eventPayload.TryGetValue("Increment", out value))
        {
            counterValue = value.ToString();
        }

        return (counterName, counterValue);
    }
}

Comme indiqué ci-dessus, vous devez vous assurer que l’argument "EventCounterIntervalSec" est défini dans l’argument filterPayload lors de l’appel de EnableEvents. Sinon, les compteurs ne pourront pas vider les valeurs, car il ne savent pas à quel intervalle il doivent être vidés.

Voir aussi