Compartir a través de


Contadores de Eventos de .NET

Este artículo se aplica a: ✔️ SDK de .NET Core 3.0 y versiones posteriores

Nota:

Para desarrollar nuevos proyectos de .NET, Microsoft recomienda usar las API System.Diagnostics.Metrics más recientes. Las API System.Diagnostics.Metrics ofrecen mayor funcionalidad, estandarización e integración con un ecosistema más amplio de herramientas. Consulte la comparativa entre API de métricas para obtener más información.

Los EventCounters son API de .NET que se usan para la recopilación ligera, multiplataforma y casi en tiempo real de métricas de rendimiento. Los EventCounters se agregaron como una alternativa multiplataforma a los "contadores de rendimiento" de .NET Framework en Windows. En este artículo se aprende qué son los EventCounters, cómo se implementan y cómo se usan.

El entorno de ejecución de .NET y algunas bibliotecas de .NET publican información de diagnóstico básica mediante EventCounters a partir de .NET Core 3.0. Además de los EventCounters proporcionados por el entorno de ejecución de .NET, puede implementar sus propios EventCounters. Los EventCounters se pueden usar para realizar el seguimiento de diversas métricas. Obtenga más información en EventCounters conocidos en .NET.

Los EventCounters existen como parte de EventSource y se insertan automáticamente en las herramientas de escucha de forma periódica. Al igual que todos los demás eventos de EventSource, se pueden consumir tanto en proceso como fuera de proceso mediante EventListener y EventPipe. Este artículo se centra en las capacidades multiplataforma de los EventCounters y excluye de forma deliberada a PerfView y ETW (seguimiento de eventos para Windows), aunque ambos se pueden usar con los EventCounters.

Imagen de diagrama de EventCounters en proceso y fuera de proceso

Información general sobre la API EventCounter

Hay dos categorías principales de EventCounters. Algunos contadores son para valores de tipo "tasa", como el número total de excepciones, el número total de GC y el número total de solicitudes. Otros contadores son valores de tipo "instantánea", como el uso del montón, el uso de la CPU y el tamaño del espacio de trabajo. Dentro de cada una de estas categorías de contadores, hay dos tipos de contadores que varían según el modo en que obtienen su valor. Los contadores de sondeo recuperan su valor mediante un callback, mientras que los contadores no son de sondeo tienen sus valores establecidos directamente en la instancia del contador.

Los contadores se representan mediante las siguientes implementaciones:

Un listener de eventos especifica la duración de los intervalos de medición. Al final de cada intervalo se transmite un valor al receptor para cada contador. Las implementaciones de un contador determinan las API y los cálculos que se usan para generar el valor de cada intervalo.

  • EventCounter registra un conjunto de valores. El método EventCounter.WriteMetric agrega un nuevo valor al conjunto. Con cada intervalo se calcula un resumen estadístico del conjunto, como valor mínimo, máximo y medio. La herramienta dotnet-counters siempre muestra el valor medio. EventCounter es útil para describir un conjunto discreto de operaciones. Su uso habitual puede incluir la supervisión del tamaño medio en bytes de operaciones de E/S recientes o el valor monetario medio de un conjunto de transacciones financieras.

  • IncrementingEventCounter registra un total acumulado de cada intervalo de tiempo. El método IncrementingEventCounter.Increment agrega al total. Por ejemplo, si se llama a Increment() tres veces durante un intervalo con valores 1, 2 y 5, el total acumulado de 8 se comunica como valor del contador de este intervalo. La herramienta dotnet-counters muestra la tasa como el total / tiempo registrado. IncrementingEventCounter resulta útil para medir la frecuencia con que se produce una acción, como el número de solicitudes procesadas por segundo.

  • PollingCounter emplea un callback para determinar el valor que se informa. Con cada intervalo de tiempo, se invoca la función de callback proporcionada por el usuario, y el valor de retorno se utiliza como valor del contador. PollingCounter se puede usar para consultar una métrica de un origen externo, por ejemplo, para obtener los bytes libres actuales en un disco. También se puede usar para notificar estadísticas personalizadas que una aplicación puede calcular a petición. Los ejemplos incluyen el reporte del percentil 95 de las latencias de solicitud recientes o el ratio de aciertos y fallos actual de una caché.

  • IncrementingPollingCounter utiliza un callback para determinar el valor del incremento informado. Con cada intervalo de tiempo se invoca el callback, y la diferencia entre el momento de la invocación actual y el de la última invocación es el valor informado. La herramienta dotnet-counters siempre muestra la diferencia como una tasa, el valor / tiempo comunicado. Este contador es útil cuando no resulta factible llamar a una API en cada repetición, pero es posible consultar el número total de repeticiones. Por ejemplo, puede notificar el número de bytes escritos en un archivo por segundo, incluso sin una notificación cada vez que se escribe un byte.

Implementación de un EventSource

En el código siguiente se implementa un ejemplo EventSource expuesto como proveedor "Sample.EventCounter.Minimal" con nombre. Este origen contiene un EventCounter que representa el tiempo de procesamiento de la solicitud. Un contador de este tipo tiene un nombre (es decir, su identificador único en el origen) y un nombre de visualización, ambos usados por herramientas de escucha como 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);
    }
}

Use dotnet-counters ps para mostrar una lista de los procesos de .NET que se pueden supervisar:

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

Pase el nombre EventSource a la opción --counters para empezar a supervisar el contador:

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

En el siguiente ejemplo se muestra la salida del monitor:

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

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

Presione q para detener el comando de supervisión.

Contadores condicionales

Al implementar un EventSource, los contadores contenedores pueden instanciarse de forma condicional al llamar al método EventSource.OnEventCommand con un valor Command de EventCommand.Enable. Para crear una instancia de un contador de forma segura solo si es null, use el operador de asignación de coalescencia nula. Además, los métodos personalizados pueden evaluar el método IsEnabled para determinar si el origen del evento actual está habilitado o no.

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

Sugerencia

Los contadores condicionales son contadores que se instancian condicionalmente, una optimización menor. El entorno de ejecución adopta este patrón para los escenarios en los que normalmente no se usan contadores, para ahorrar una fracción de un milisegundo.

Contadores de ejemplo del entorno de ejecución de .NET Core

Hay muchas implementaciones de ejemplo excelentes del entorno de ejecución de .NET Core. Esta es la implementación en tiempo de ejecución del contador que realiza el seguimiento del tamaño del conjunto de trabajo de la aplicación.

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

PollingCounter comunica la cantidad actual de memoria física asignada al proceso (espacio de trabajo) de la aplicación, ya que captura una métrica en un momento dado. La expresión lambda proporcionada sirve como la devolución de llamada para el sondeo de un valor, que es simplemente una llamada a la API System.Environment.WorkingSet. DisplayName y DisplayUnits son propiedades opcionales que se pueden establecer para ayudar al lado del consumidor del contador a mostrar el valor con más claridad. Por ejemplo, dotnet-counters usa estas propiedades para mostrar la versión más descriptiva de los nombres de contador.

Importante

Las propiedades DisplayName no están localizadas.

En el caso de PollingCounter e IncrementingPollingCounter, no es necesario hacer nada más. Ambos sondean los valores por sí mismos en un intervalo solicitado por el consumidor.

Este es un ejemplo de un contador de entorno de ejecución implementado mediante IncrementingPollingCounter.

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

IncrementingPollingCounter utiliza la API Monitor.LockContentionCount para comunicar el incremento del recuento total de contenciones de bloqueo. La propiedad DisplayRateTimeScale es opcional pero, cuando se usa, puede proporcionar una sugerencia sobre el intervalo de tiempo en el que se muestra mejor el contador. Por ejemplo, el recuento de contenciones de bloqueo se muestra mejor como recuento por segundo, por lo que DisplayRateTimeScale se establece en un segundo. La tasa de presentación se puede ajustar a diferentes tipos de contadores de tasa.

Nota:

dotnet-counters no utiliza DisplayRateTimeScale, y no es necesario que los escuchadores de eventos lo usen.

Hay más implementaciones de contador que se pueden usar como referencia en el repositorio del entorno de ejecución de .NET.

Concurrencia

Sugerencia

La API de EventCounters no garantiza la seguridad entre subprocesos. Cuando varios hilos llaman a los delegados pasados a instancias PollingCounter o IncrementingPollingCounter, es su responsabilidad garantizar la seguridad de subprocesos de los delegados.

Por ejemplo, considere el siguiente EventSource para realizar un seguimiento de las solicitudes.

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

Se puede llamar al método AddRequest() desde un controlador de solicitudes y RequestRateCounter sondea el valor en el intervalo especificado por el consumidor del contador. Pero varios subprocesos pueden llamar al método AddRequest() a la vez, con lo que se coloca una condición de carrera sobre _requestCount. Una alternativa segura para hilos de incrementar _requestCount es usar Interlocked.Increment.

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

Para evitar lecturas rasgadas (en arquitecturas de 32 bits) en el campo long de tipo _requestCount, utilice Interlocked.Read.

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

Uso de EventCounters

Hay dos formas principales de usar EventCounters: en proceso o fuera de proceso. El uso de EventCounters se puede clasificar en tres capas de distintas tecnologías de uso.

  • Transporte de eventos en una transmisión sin procesar a través de ETW o EventPipe.

    Las API de ETW están incluidas en el sistema operativo Windows y se puede acceder a EventPipe como una API de .NET o el protocolo IPC de diagnóstico.

  • Descodificación del flujo de eventos binario en eventos:

    La biblioteca TraceEvent controla los formatos de flujo de ETW y EventPipe.

  • Herramientas de línea de comandos y GUI:

    Herramientas como PerfView (ETW o EventPipe), dotnet-counters (solo EventPipe) y dotnet-monitor (solo EventPipe).

Uso fuera de proceso

El uso de EventCounters fuera de proceso es un método habitual. Puede usar dotnet-counters para consumirlos a modo multiplataforma a través de un EventPipe. La herramienta dotnet-counters es una herramienta global de CLI para dotnet que es multiplataforma y se puede usar para supervisar los valores de los contadores. Para obtener información sobre cómo usar dotnet-counters para supervisar los contadores, vea dotnet-counters o trabaje con el tutorial Medición del rendimiento mediante EventCounters.

Azure Application Insights

En Azure Monitor, específicamente en Azure Application Insights, se pueden utilizar EventCounters. Los contadores se pueden agregar y quitar; además, el usuario puede especificar contadores personalizados o contadores conocidos. Para obtener más información, vea Personalización de los contadores que se van a recopilar.

dotnet-monitor

La herramienta dotnet-monitor facilita el acceso a los diagnósticos desde un proceso de .NET de forma remota y automatizada. Además de ofrecer seguimientos, permite supervisar métricas, así como recopilar volcados de memoria y de memoria GC. Se distribuye tanto como herramienta de la CLI como imagen de Docker. Expone una API de REST, y la recopilación de artefactos de diagnóstico se produce a través de llamadas REST.

Para obtener más información, consulte dotnet-monitor.

Consumir en proceso

Puede consumir los valores del contador por medio de la API EventListener. EventListener es una manera en-proceso de consumir cualquier evento escrito por todas las instancias de un EventSource en tu aplicación. Para obtener más información sobre cómo usar la API EventListener, vea EventListener.

En primer lugar, es necesario habilitar el EventSource que genera el valor del contador. Invalide el método EventListener.OnEventSourceCreated para obtener una notificación cuando se cree un EventSource y, si es el EventSource correcto con los EventCounters, puede llamar a EventListener.EnableEvents en él. Aquí tienes un ejemplo de sobrescritura:

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

Código de ejemplo

Esta es una clase EventListener de ejemplo que imprime todos los nombres y valores de contador de EventSource del entorno de ejecución de .NET para publicar sus contadores internos (System.Runtime) cada segundo.

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

Como se ha mostrado anteriormente, debe asegurarse de que el argumento "EventCounterIntervalSec" esté establecido en el argumento filterPayload al llamar a EnableEvents. De lo contrario, los contadores no podrán eliminar los valores porque no saben en qué intervalo deben ser eliminados.

Consulte también