Contadores de eventos no .NET

Este artigo aplica-se a: ✔️ SDK do .NET Core 3.0 e versões posteriores

Nota

Para desenvolver novos projetos .NET, a Microsoft recomenda o uso das APIs System.Diagnostics.Metrics mais recentes. As APIs System.Diagnostics.Metrics oferecem maior funcionalidade, padronização e integração com um ecossistema mais amplo de ferramentas. Consulte a comparação da API de métricas para obter mais informações.

EventCounters são APIs .NET usadas para coleta de métricas de desempenho leve, entre plataformas e quase em tempo real. EventCounters foram adicionados como uma alternativa multiplataforma para os "contadores de desempenho" do .NET Framework no Windows. Neste artigo, você aprenderá o que são EventCounters, como implementá-los e como consumi-los.

O tempo de execução do .NET e algumas bibliotecas .NET publicam informações básicas de diagnóstico usando EventCounters a partir do .NET Core 3.0. Além dos EventCounters que são fornecidos pelo tempo de execução do .NET, você pode optar por implementar seus próprios EventCounters. Os contadores de eventos podem ser usados para controlar várias métricas. Saiba mais sobre eles em EventCounters bem conhecidos no .NET

Os EventCounters vivem como parte de um EventSource, e são automaticamente enviados para ferramentas de ouvinte regularmente. Como todos os outros eventos em um EventSource, eles podem ser consumidos dentro e fora do proc via EventListener e EventPipe. Este artigo se concentra nos recursos de plataforma cruzada de EventCounters e exclui intencionalmente PerfView e ETW (Event Tracing for Windows) - embora ambos possam ser usados com EventCounters.

Imagem do diagrama EventCounters in-proc e out-of-proc

Visão geral da API EventCounter

Há duas categorias principais de EventCounters. Alguns contadores são para valores de "taxa", como número total de exceções, número total de GCs e número total de solicitações. Outros contadores são valores de "instantâneo", como uso de pilha, uso da CPU e tamanho do conjunto de trabalho. Dentro de cada uma dessas categorias de contadores, existem dois tipos de contadores que variam de acordo com a forma como obtêm seu valor. Os contadores de sondagem recuperam seu valor por meio de um retorno de chamada, e os contadores que não são de sondagem têm seus valores definidos diretamente na instância do contador.

Os contadores são representados pelas seguintes implementações:

Um ouvinte de eventos especifica quanto tempo os intervalos de medição são. No final de cada intervalo, um valor é transmitido ao ouvinte para cada contador. As implementações de um contador determinam quais APIs e cálculos são usados para produzir o valor de cada intervalo.

  • O EventCounter registra um conjunto de valores. O EventCounter.WriteMetric método adiciona um novo valor ao conjunto. Com cada intervalo, um resumo estatístico para o conjunto é calculado, como o min, max e média. A ferramenta dotnet-counters sempre exibirá o valor médio. O EventCounter é útil para descrever um conjunto discreto de operações. O uso comum pode incluir o monitoramento do tamanho médio em bytes de operações recentes de E/S ou do valor monetário médio de um conjunto de transações financeiras.

  • O IncrementingEventCounter registra um total de execução para cada intervalo de tempo. O IncrementingEventCounter.Increment método aumenta o total. Por exemplo, se Increment() for chamado três vezes durante um intervalo com valores 1, 2e 5, então o total de execução de será relatado como o valor do 8 contador para esse intervalo. A ferramenta dotnet-counters exibirá a taxa como o total / tempo gravado. O IncrementingEventCounter é útil para medir a frequência com que uma ação está ocorrendo, como o número de solicitações processadas por segundo.

  • O PollingCounter usa um retorno de chamada para determinar o valor que é relatado. Com cada intervalo de tempo, a função de retorno de chamada fornecida pelo usuário é invocada e o valor de retorno é usado como o valor do contador. A PollingCounter pode ser usado para consultar uma métrica de uma fonte externa, por exemplo, obtendo os bytes livres atuais em um disco. Ele também pode ser usado para relatar estatísticas personalizadas que podem ser calculadas sob demanda por um aplicativo. Os exemplos incluem relatar o percentil 95 de latências de solicitação recentes ou a taxa atual de acertos ou erros de um cache.

  • O IncrementingPollingCounter usa um retorno de chamada para determinar o valor de incremento relatado. Com cada intervalo de tempo, o retorno de chamada é invocado e, em seguida, a diferença entre a chamada atual e a última chamada é o valor relatado. A ferramenta dotnet-counters sempre exibirá a diferença como uma taxa, o valor / tempo relatado. Esse contador é útil quando não é viável chamar uma API em cada ocorrência, mas é possível consultar o número total de ocorrências. Por exemplo, você pode relatar o número de bytes gravados em um arquivo por segundo, mesmo sem uma notificação cada vez que um byte é gravado.

Implementar um EventSource

O código a seguir implementa um exemplo EventSource exposto como o provedor nomeado "Sample.EventCounter.Minimal" . Essa fonte contém um tempo de EventCounter processamento de solicitação representativo. Esse contador tem um nome (ou seja, seu ID exclusivo na fonte) e um nome de exibição, ambos usados por ferramentas de escuta, como contadores de pontos.

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

Você usa dotnet-counters ps para exibir uma lista de processos .NET que podem ser monitorados:

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

Passe o EventSource nome para a opção para começar a --counters monitorar seu contador:

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

O exemplo a seguir mostra a saída do monitor:

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

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

Pressione q para parar o comando de monitoramento.

Contadores condicionais

Ao implementar um EventSource, os contadores que contêm podem ser instanciados condicionalmente quando o EventSource.OnEventCommand método é chamado com um Command valor de EventCommand.Enable. Para instanciar com segurança uma instância de contador somente se ela for null, use o operador de atribuição de coalescência nula. Além disso, os métodos personalizados podem avaliar o IsEnabled método para determinar se a fonte de eventos atual está habilitada ou não.

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

Gorjeta

Contadores condicionais são contadores que são instanciados condicionalmente, uma micro-otimização. O tempo de execução adota esse padrão para cenários em que os contadores normalmente não são usados, para salvar uma fração de milissegundo.

Contadores de exemplo de tempo de execução do .NET Core

Há muitos exemplos excelentes de implementações no tempo de execução do .NET Core. Aqui está a implementação de tempo de execução para o contador que rastreia o tamanho do conjunto de trabalho do aplicativo.

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

O PollingCounter relata a quantidade atual de memória física mapeada para o processo (conjunto de trabalho) do aplicativo, uma vez que captura uma métrica em um momento no tempo. O retorno de chamada para sondar um valor é a expressão lambda fornecida, que é apenas uma chamada para a System.Environment.WorkingSet API. DisplayName e DisplayUnits são propriedades opcionais que podem ser definidas para ajudar o lado do consumidor do contador a exibir o valor com mais clareza. Por exemplo, dotnet-counters usa essas propriedades para exibir a versão mais amigável de exibição dos nomes de contadores.

Importante

As DisplayName propriedades não estão localizadas.

Para o PollingCounter, e para o IncrementingPollingCounter, nada mais precisa ser feito. Ambos pesquisam os próprios valores em um intervalo solicitado pelo consumidor.

Aqui está um exemplo de um contador de tempo de execução implementado usando IncrementingPollingCountero .

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

O IncrementingPollingCounter usa a Monitor.LockContentionCount API para relatar o incremento da contagem total de contenção de bloqueio. A DisplayRateTimeScale propriedade é opcional, mas quando usada pode fornecer uma dica para qual intervalo de tempo o contador é melhor exibido. Por exemplo, a contagem de contenção de bloqueio é melhor exibida como contagem por segundo, portanto, é DisplayRateTimeScale definida como um segundo. A taxa de exibição pode ser ajustada para diferentes tipos de contadores de taxa.

Nota

O DisplayRateTimeScale não é usado por contadores de pontos e os ouvintes de eventos não são obrigados a usá-lo.

Há mais implementações de contador para usar como referência no repositório de tempo de execução do .NET.

Simultaneidade

Gorjeta

A API EventCounters não garante a segurança do thread. Quando os delegados passados ou PollingCounter IncrementingPollingCounter instâncias são chamados por vários threads, é sua responsabilidade garantir a segurança dos threads dos delegados.

Por exemplo, considere o seguinte EventSource para acompanhar as solicitações.

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

O AddRequest() método pode ser chamado a partir de um manipulador de solicitação, e as RequestRateCounter sondagens o valor no intervalo especificado pelo consumidor do contador. No entanto, o AddRequest() método pode ser chamado por vários threads ao mesmo tempo, colocando uma condição de corrida em _requestCount. Uma maneira alternativa segura para threads para incrementar o _requestCount é usar Interlocked.Incremento .

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

Para evitar leituras rasgadas (em arquiteturas de 32 bits) do -field_requestCount, longuse Interlocked.Read.

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

Consumir EventCounters

Há duas maneiras principais de consumir EventCounters: in-proc e out-of-proc. O consumo de EventCounters pode ser distinguido em três camadas de várias tecnologias de consumo.

  • Transporte de eventos em um fluxo bruto via ETW ou EventPipe:

    As APIs ETW vêm com o sistema operacional Windows e o EventPipe é acessível como uma API .NET ou o protocolo IPC de diagnóstico.

  • Decodificando o fluxo de eventos binários em eventos:

    A biblioteca TraceEvent lida com os formatos de fluxo ETW e EventPipe.

  • Ferramentas de linha de comando e GUI:

    Ferramentas como PerfView (ETW ou EventPipe), dotnet-counters (somente EventPipe) e dotnet-monitor (somente EventPipe).

Consumir fora do processo

Consumir EventCounters fora do proc é uma abordagem comum. Você pode usar contadores de pontos para consumi-los de forma multiplataforma por meio de um EventPipe. A dotnet-counters ferramenta é uma ferramenta global da CLI dotnet multiplataforma que pode ser usada para monitorar os valores do contador. Para saber como usar dotnet-counters para monitorar seus contadores, consulte contadores de pontos ou trabalhe no tutorial Medir desempenho usando Contadores de Eventos.

Azure Application Insights

Os Contadores de Eventos podem ser consumidos pelo Azure Monitor, especificamente pelo Azure Application Insights. Contadores podem ser adicionados e removidos, e você é livre para especificar contadores personalizados ou contadores conhecidos. Para obter mais informações, consulte Personalizando contadores a serem coletados.

Dotnet-Monitor

A dotnet-monitor ferramenta facilita o acesso ao diagnóstico de um processo .NET de forma remota e automatizada. Além de rastreamentos, ele pode monitorar métricas, coletar despejos de memória e coletar despejos de GC. Ele é distribuído como uma ferramenta CLI e uma imagem docker. Ele expõe uma API REST e a coleção de artefatos de diagnóstico ocorre por meio de chamadas REST.

Para obter mais informações, consulte dotnet-monitor.

Consumir in-proc

Você pode consumir os valores do contador por meio da EventListener API. An EventListener é uma maneira in-proc de consumir quaisquer eventos escritos por todas as instâncias de um EventSource em seu aplicativo. Para obter mais informações sobre como usar a EventListener API, consulte EventListener.

Primeiro, o EventSource que produz o valor do contador precisa ser habilitado. Substitua o EventListener.OnEventSourceCreated método para obter uma notificação quando uma EventSource é criada e, se isso for o correto EventSource com seus EventCounters, você poderá chamá-la EventListener.EnableEvents . Aqui está um exemplo de substituição:

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 exemplo

Aqui está uma classe de exemplo EventListener que imprime todos os nomes de contadores e valores do tempo de execução EventSourcedo .NET, para publicar seus contadores internos (System.Runtime) a 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 mostrado acima, você deve certificar-se de que o "EventCounterIntervalSec" filterPayload argumento está definido no argumento ao chamar EnableEvents. Caso contrário, os contadores não serão capazes de liberar os valores, uma vez que não sabe em que intervalo ele deve ser liberado.

Consulte também