EventCounters in .NET

Questo articolo si applica a: ✔️ .NET Core 3.0 SDK e versioni successive

Le EventCounters sono API .NET usate per la raccolta di metriche di prestazioni leggere, multipiattaforma e quasi in tempo reale. Le EventCounters sono state aggiunte come alternativa multipiattaforma ai "contatori delle prestazioni" di .NET Framework in Windows. In questo articolo verranno fornite informazioni su EventCounters, su come implementarle e su come usarle.

Il runtime .NET e alcune librerie .NET pubblicano informazioni di diagnostica di base usando EventCounters a partire da .NET Core 3.0. Oltre a EventCounters fornite dal runtime .NET, è possibile scegliere di implementare le proprie EventCounters. Le EventCounters possono essere usate per tenere traccia di varie metriche. Per ulteriori informazioni, consultare EventCounters note in .NET

Le EventCounters sono attive come parte di un EventSource e sono automaticamente inviate agli strumenti listener a intervalli regolari. Come tutti gli altri eventi in un oggetto EventSource, possono essere utilizzati sia in-proc che out-of-proc tramite EventListener e EventPipe. Questo articolo è incentrato sulle funzionalità multipiattaforma di EventCounters ed esclude intenzionalmente PerfView e ETW (Event Trace for Windows), anche se entrambi possono essere usati con EventCounter.

Immagine del diagramma di EventCounters nel processo e fuori dal processo

Panoramica dell'API EventCounter

Esistono due categorie principali di EventCounter. Alcuni contatori sono relativi ai valori "rate", come il numero totale di eccezioni, il numero totale di GC e il numero totale di richieste. Altri contatori sono valori "snapshot", come l'utilizzo dell'heap, l'utilizzo della CPU e le dimensioni del set di lavoro. All'interno di ognuna di queste categorie di contatori, esistono due tipi di contatori che variano in base al modo in cui ottengono il valore. I contatori di polling ottengono il valore tramite un callback, mentre i valori dei contatori non di polling sono impostati direttamente nell'istanza del contatore.

I contatori sono rappresentati dalle seguenti implementazioni:

Un listener di eventi specifica la durata degli intervalli di misurazione. Alla fine di ogni intervallo, un valore viene trasmesso al listener per ogni contatore. Le implementazioni di un contatore determinano le API e i calcoli usati per produrre il valore a ogni intervallo.

  • EventCounter registra un set di valori. Il metodo EventCounter.WriteMetric aggiunge un nuovo valore al set. Con ogni intervallo, viene calcolato un riepilogo statistico per il set (ad esempio min, max e medio). Lo strumento dotnet-counters mostrerà sempre il valore medio. EventCounter è utile per descrivere un set discreto di operazioni. Può essere comunemente usato per monitorare le dimensioni medie in byte di operazioni recenti di I/O o il valore monetario medio di un set di transazioni finanziarie.

  • IncrementingEventCounter registra un totale in esecuzione per ogni intervallo di tempo. Il metodo IncrementingEventCounter.Increment aggiunge al totale. Ad esempio, se Increment() viene chiamato tre volte durante un intervallo con valori 1, 2e 5, il totale in esecuzione di 8 verrà segnalato come valore del contatore per questo intervallo. Lo strumento dotnet-counters mostrerà la frequenza come totale/tempo registrato. IncrementingEventCounter è utile per misurare la frequenza con cui si verifica un'azione, ad esempio il numero di richieste elaborate al secondo.

  • PollingCounter usa un callback per determinare il valore segnalato. Per ogni intervallo di tempo, la funzione di callback fornita dall'utente viene eseguita e il valore restituito viene usato come valore del contatore. Un PollingCounter può essere usato per eseguire una query su una metrica da un'origine esterna, ad esempio per calcolare i byte liberi correnti di un disco. Può anche essere usato per statistiche personalizzate report che possono essere calcolate su richiesta da un'applicazione. Gli esempi includono la segnalazione del 95° percentile delle recenti latenze delle richieste, o il rapporto di riscontri o mancati riscontri correnti di una cache.

  • IncrementingPollingCounter usa un callback per determinare il valore di incremento segnalato. Per ogni intervallo di tempo, il callback viene eseguito, e la differenza tra l’esecuzione corrente e l’ultima esecuzione è il valore segnalato. Lo strumento dotnet-counters mostrerà sempre la differenza come tasso, il valore/ora segnalato. Questo contatore è utile quando non è possibile chiamare un'API per ogni occorrenza, ma è possibile eseguire una query sul numero totale di occorrenze. Ad esempio, è possibile segnalare il numero di byte scritti in un file al secondo, anche senza una notifica ogni volta che viene scritto un byte.

Implementare EventSource

Il seguente codice implementa un EventSource di esempio esposto come provider denominato "Sample.EventCounter.Minimal". Questa origine contiene un oggetto EventCounter che rappresenta il tempo di elaborazione delle richieste. Un contatore di questo tipo ha un nome (ovvero il relativo ID univoco nell'origine) e un nome visualizzato, entrambi usati dagli strumenti del listener come dotnet-counters.

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

Usare dotnet-counters ps per visualizzare un elenco di processi .NET che possono essere monitorati:

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

Passare il nome EventSource all'opzione --counters per avviare il monitoraggio del contatore:

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

Il seguente esempio mostra l'output di monitoraggio:

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

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

Premere q per arrestare il comando di monitoraggio.

Contatori condizionali

Quando si implementa un oggetto EventSource, è possibile creare un'istanza condizionale dei contatori che lo contengono quando il metodo EventSource.OnEventCommand viene chiamato con un valore Command di EventCommand.Enable. Per creare un'istanza di contatore in sicurezza solo se è null, usare l'operatore di assegnazione null-coalescing. Inoltre, i metodi personalizzati possono valutare il metodo IsEnabled per determinare se l'origine dell’evento corrente è abilitata o meno.

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

Suggerimento

I contatori condizionali sono contatori di cui viene creata un'istanza condizionale, una micro-ottimizzazione. Il runtime adotta questo modello per scenari in cui i contatori non sono normalmente usati, per risparmiare una frazione di millisecondo.

Contatori di esempio di runtime di .NET Core

Esistono molti ottimi esempi di implementazioni nel runtime di .NET Core. Ecco l'implementazione del runtime per il contatore che tiene traccia delle dimensioni del working set dell'applicazione.

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

PollingCounter segnala la quantità corrente di memoria fisica mappata al processo (working set) dell'app, poiché acquisisce una metrica in un momento specifico. Il callback per il polling di un valore è l'espressione lambda fornita, la quale è solo una chiamata all'API System.Environment.WorkingSet. DisplayName e DisplayUnits sono proprietà facoltative che possono essere impostate per aiutare il lato del consumer del contatore a mostrare il valore più chiaramente. Ad esempio, dotnet-counters usa queste proprietà per mostrare la versione più intuitiva dei nomi dei contatori.

Importante

Le proprietà DisplayName non sono localizzate.

Per PollingCounter e IncrementingPollingCounter, non sono necessarie altre operazioni. Entrambi eseguono il polling dei valori all’intervallo richiesto dal consumer.

Di seguito è riportato un esempio di contatore di runtime implementato usando IncrementingPollingCounter.

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

IncrementingPollingCounter usa l'API Monitor.LockContentionCount per segnalare l'incremento del numero totale di conflitti di blocco. La proprietà DisplayRateTimeScale è facoltativa, ma quando usata può fornire un suggerimento sull'intervallo di tempo in cui il contatore funziona al meglio. Ad esempio, il conteggio dei conflitti di blocco viene mostrato al meglio come conteggio al secondo, quindi DisplayRateTimeScale viene impostato su un secondo. La frequenza di visualizzazione può essere modificata per diversi tipi di contatori di frequenza.

Nota

DisplayRateTimeScalenon è usato dai contatori dotnet e non sono necessari listener di eventi per usarlo.

Sono disponibili altre implementazioni di contatori da usare come riferimento nel repository di runtime .NET.

Concorrenza

Suggerimento

Le API EventCounters non garantiscono la thread safety. Quando i delegati passati a istanze PollingCounter o IncrementingPollingCounter vengono chiamati da più thread, è responsabilità dell'utente garantire la thread-safety dei delegati.

Si consideri, ad esempio, il seguente EventSource per tenere traccia delle richieste.

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

Il metodo AddRequest() può essere chiamato da un gestore di richieste, e RequestRateCounter esegue il polling del valore all'intervallo specificato dal consumer del contatore. Tuttavia, il metodo AddRequest() può essere chiamato da più thread contemporaneamente, inserendo una race condition su _requestCount. Un modo alternativo thread-safe per incrementare _requestCount consiste nell’usare Interlocked.Increment.

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

Per evitare letture troncate (nelle architetture a 32 bit) del long campo _requestCount, usare Interlocked.Read.

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

Utilizzare EventCounter

Esistono due modi principali per usare EventCounter: in-proc e out-of-proc. L'utilizzo di EventCounter può essere distinto in tre livelli di diverse tecnologie di consumo.

  • Trasporto di eventi in un flusso non elaborato tramite ETW o EventPipe:

    Le API ETW sono disponibili con il sistema operativo Windows e EventPipe è accessibile come API .NET o come protocollo IPC di diagnostica.

  • Decodifica del flusso di eventi binari a eventi:

    La libreria TraceEvent gestisce entrambi i formati di flusso ETW e EventPipe.

  • Strumenti della riga di comando e dell'interfaccia utente grafica:

    Strumenti come PerfView (ETW o EventPipe), dotnet-counters (solo EventPipe) e dotnet-monitor (solo EventPipe).

Utilizzare out-of-process

Il consumo di EventCounter out-of-process è un approccio comune. È possibile consumare contatori dotnet tra piattaforma tramite EventPipe. Lo strumento dotnet-counters è uno strumento globale dell'interfaccia della riga di comando dotnet multipiattaforma che può essere usato per monitorare i valori dei contatori. Per informazioni su come usare dotnet-counters per monitorare i contatori, consultare dotnet-counters o usare l'esercitazione Misurare le prestazioni con EventCounter.

dotnet-trace

Lo strumento dotnet-trace può essere usato per consumare i dati del contatore tramite un EventPipe. Di seguito è riportato un esempio che usa dotnet-trace per raccogliere i dati dei contatori.

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

Per ulteriori informazioni su come raccogliere i valori dei contatori nel tempo, consultare la documentazione dotnet-trace.

Azure Application Insights

EventCounter possono essere usate da Monitoraggio di Azure, in particolare Da Azure Application Insights. I contatori possono essere aggiunti e rimossi ed è possibile specificare contatori personalizzati o noti. Per ulteriori informazioni, consultare Personalizzazione dei contatori da raccogliere.

dotnet-monitor

Lo strumento dotnet-monitor semplifica l'accesso alla diagnostica da un processo .NET in modalità remota e automatizzata. Oltre alle analisi, può monitorare le metriche, raccogliere dump di memoria e raccogliere dump GC. È distribuito sia come strumento dell'interfaccia della riga di comando che come immagine Docker. Espone un'API REST e la raccolta di artefatti di diagnostica avviene tramite chiamate REST.

Per ulteriori informazioni, consultare dotnet-monitor.

Utilizzare in-process

È possibile consumare i valori dei contatori tramite l'API EventListener. Un EventListener è un metodo in-proc per consumare tutti gli eventi scritti da tutte le istanze di un oggetto EventSource nell'applicazione. Per ulteriori informazioni su come usare l'API EventListener, consultare EventListener.

In primo luogo, l'oggetto EventSource che produce il valore del contatore deve essere abilitato. Eseguire l'override del metodo EventListener.OnEventSourceCreated per ricevere una notifica quando viene creato un oggetto EventSource e, se questo è il EventSource corretto con le EventCounter, è possibile chiamarvi EventListener.EnableEvents. Di seguito è riportato un esempio di override:

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

Codice di esempio

Ecco una classe di esempio EventListener che stampa tutti i nomi dei contatori e i valori del runtime .Net EventSource per la pubblicazione dei contatori interni (System.Runtime) ogni secondo.

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

Come illustrato in precedenza, è necessario assicurarsi che l'argomento "EventCounterIntervalSec" sia impostato sull'argomento filterPayload quando si chiama EnableEvents. In caso contrario, i contatori non saranno in grado di scaricare i valori perché non l’intervallo di scaricamento è sconosciuto.

Vedi anche