Partilhar via


Código de instrumento para criar eventos EventSource

Este artigo aplica-se a: ✔️ .NET Core 3.1 e versões ✔️ posteriores .NET Framework 4.5 e versões posteriores

O guia de Introdução mostrou como criar um EventSource mínimo e coletar eventos em um arquivo de rastreamento. Este tutorial entra em mais detalhes sobre como criar eventos usando o System.Diagnostics.Tracing.EventSource.

Um EventSource mínimo

[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
    public static DemoEventSource Log { get; } = new DemoEventSource();

    [Event(1)]
    public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
}

A estrutura básica de um EventSource derivado é sempre a mesma. Em particular:

  • A classe herda de System.Diagnostics.Tracing.EventSource
  • Para cada tipo diferente de evento que você deseja gerar, um método precisa ser definido. Esse método deve ser nomeado usando o nome do evento que está sendo criado. Se o evento tiver dados adicionais, estes devem ser passados usando argumentos. Esses argumentos de evento precisam ser serializados para que apenas certos tipos sejam permitidos.
  • Cada método tem um corpo que chama WriteEvent passando-lhe uma ID (um valor numérico que representa o evento) e os argumentos do método de evento. A ID precisa ser exclusiva dentro do EventSource. O ID é explicitamente atribuído usando o System.Diagnostics.Tracing.EventAttribute
  • EventSources destinam-se a ser instâncias singleton. Assim, é conveniente definir uma variável estática, por convenção chamada Log, que representa este singleton.

Regras para definir métodos de evento

  1. Qualquer instância, método de retorno não virtual, void definido em uma classe EventSource é, por padrão, um método de log de eventos.
  2. Os métodos virtuais ou de não devolução de nulos são incluídos apenas se estiverem marcados com o System.Diagnostics.Tracing.EventAttribute
  3. Para marcar um método qualificado como não registrando, você deve decorá-lo com o System.Diagnostics.Tracing.NonEventAttribute
  4. Os métodos de log de eventos têm IDs de evento associadas a eles. Isso pode ser feito explicitamente decorando o método com um System.Diagnostics.Tracing.EventAttribute ou implicitamente pelo número ordinal do método na classe. Por exemplo, usando numeração implícita, o primeiro método na classe tem ID 1, o segundo tem ID 2 e assim por diante.
  5. Os métodos de log de eventos devem chamar um WriteEvent, WriteEventCoreWriteEventWithRelatedActivityId ou WriteEventWithRelatedActivityIdCore sobrecarga.
  6. A ID do evento, implícita ou explícita, deve corresponder ao primeiro argumento passado para a API WriteEvent* que ele chama.
  7. O número, os tipos e a ordem dos argumentos passados para o método EventSource devem estar alinhados com a forma como são passados para as APIs WriteEvent*. Para WriteEvent os argumentos seguem a ID do evento, para WriteEventWithRelatedActivityId os argumentos seguem o relatedActivityId. Para os métodos WriteEvent*Core, os argumentos devem ser serializados manualmente no data parâmetro.
  8. Os nomes dos eventos não podem conter < caracteres nem > caracteres. Embora os métodos definidos pelo usuário também não possam conter esses caracteres, async os métodos serão reescritos pelo compilador para contê-los. Para ter certeza de que esses métodos gerados não se tornam eventos, marque todos os métodos que não são eventos em um EventSource com o NonEventAttribute.

Melhores práticas

  1. Os tipos que derivam de EventSource geralmente não têm tipos intermediários na hierarquia ou interfaces de implementação. Consulte Personalizações avançadas abaixo para algumas exceções em que isso pode ser útil.
  2. Geralmente o nome da classe EventSource é um nome público incorreto para o EventSource. Os nomes públicos, os nomes que aparecerão nas configurações de log e visualizadores de log, devem ser globalmente exclusivos. Portanto, é uma boa prática dar ao seu EventSource um nome público usando o System.Diagnostics.Tracing.EventSourceAttribute. O nome "Demo" usado acima é curto e improvável de ser único, portanto, não é uma boa escolha para uso em produção. Uma convenção comum é usar um nome hierárquico com . ou - como separador, como "MyCompany-Samples-Demo", ou o nome do Assembly ou namespace para o qual o EventSource fornece eventos. Não é recomendado incluir "EventSource" como parte do nome público.
  3. Atribua IDs de Evento explicitamente, desta forma Alterações aparentemente benignas no código na classe de origem, como reorganizá-lo ou adicionar um método no meio, não alterarão a ID de evento associada a cada método.
  4. Ao criar eventos que representam o início e o fim de uma unidade de trabalho, por convenção, esses métodos são nomeados com sufixos 'Start' e 'Stop'. Por exemplo, 'RequestStart' e 'RequestStop'.
  5. Não especifique um valor explícito para a propriedade Guid de EventSourceAttribute, a menos que você precise dele por motivos de compatibilidade com versões anteriores. O valor Guid padrão é derivado do nome da fonte, o que permite que as ferramentas aceitem o nome mais legível por humanos e derivem o mesmo Guid.
  6. Ligue IsEnabled() antes de executar qualquer trabalho intensivo de recursos relacionado ao disparo de um evento, como calcular um argumento de evento caro que não será necessário se o evento estiver desabilitado.
  7. Tente manter o objeto EventSource novamente compatível e a versão apropriada. A versão padrão para um evento é 0. A versão pode ser alterada pela configuração EventAttribute.Version. Altere a versão de um evento sempre que você alterar os dados que são serializados com ele. Sempre adicione novos dados serializados ao final da declaração de evento, ou seja, no final da lista de parâmetros do método. Se isso não for possível, crie um novo evento com uma nova ID para substituir a antiga.
  8. Ao declarar métodos de eventos, especifique dados de carga útil de tamanho fixo antes de dados de tamanho variável.
  9. Não use cadeias de caracteres que contenham caracteres nulos. Ao gerar o manifesto para ETW EventSource declarará todas as cadeias de caracteres como terminadas nulas, mesmo que seja possível ter um caractere nulo em uma String C#. Se uma cadeia de caracteres contiver um caractere nulo, toda a cadeia de caracteres será gravada na carga útil do evento, mas qualquer analisador tratará o primeiro caractere nulo como o final da cadeia de caracteres. Se houver argumentos de carga útil após a cadeia de caracteres, o restante da cadeia de caracteres será analisado em vez do valor pretendido.

Personalizações típicas de eventos

Definindo níveis de verbosidade de eventos

Cada evento tem um nível de verbosidade e os assinantes do evento geralmente habilitam todos os eventos em um EventSource até um determinado nível de verbosidade. Os eventos declaram seu nível de verbosidade usando a Level propriedade. Por exemplo, neste EventSource abaixo, um assinante que solicita eventos de nível informativo e inferior não registrará o evento DebugMessage detalhado.

[EventSource(Name = "MyCompany-Samples-Demo")]
class DemoEventSource : EventSource
{
    public static DemoEventSource Log { get; } = new DemoEventSource();

    [Event(1, Level = EventLevel.Informational)]
    public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
    [Event(2, Level = EventLevel.Verbose)]
    public void DebugMessage(string message) => WriteEvent(2, message);
}

Se o nível de detalhamento de um evento não for especificado no EventAttribute, o padrão será Informational.

Melhor prática

Use níveis inferiores ao informativo para avisos ou erros relativamente raros. Em caso de dúvida, mantenha o padrão de Informativo e use Detalhado para eventos que ocorrem com mais frequência do que 1000 eventos/seg.

Definindo palavras-chave de eventos

Alguns sistemas de rastreamento de eventos suportam palavras-chave como um mecanismo de filtragem adicional. Ao contrário da verbosidade que categoriza eventos por nível de detalhe, as palavras-chave destinam-se a categorizar eventos com base em outros critérios, como áreas de funcionalidade de código ou que seriam úteis para diagnosticar certos problemas. As palavras-chave são chamadas de sinalizadores de bits e cada evento pode ter qualquer combinação de palavras-chave aplicadas a ele. Por exemplo, o EventSource abaixo define alguns eventos relacionados ao processamento de solicitações e outros eventos relacionados à inicialização. Se um desenvolvedor quisesse analisar o desempenho da inicialização, ele poderia habilitar apenas o registro dos eventos marcados com a palavra-chave de inicialização.

[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
    public static DemoEventSource Log { get; } = new DemoEventSource();

    [Event(1, Keywords = Keywords.Startup)]
    public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
    [Event(2, Keywords = Keywords.Requests)]
    public void RequestStart(int requestId) => WriteEvent(2, requestId);
    [Event(3, Keywords = Keywords.Requests)]
    public void RequestStop(int requestId) => WriteEvent(3, requestId);

    public class Keywords   // This is a bitvector
    {
        public const EventKeywords Startup = (EventKeywords)0x0001;
        public const EventKeywords Requests = (EventKeywords)0x0002;
    }
}

As palavras-chave devem ser definidas usando uma classe aninhada chamada Keywords e cada palavra-chave individual é definida por um membro digitado public const EventKeywords.

Melhor prática

As palavras-chave são mais importantes quando se distingue entre eventos de alto volume. Isso permite que um consumidor de eventos aumente a verbosidade para um alto nível, mas gerencie a sobrecarga de desempenho e o tamanho do log habilitando apenas subconjuntos estreitos dos eventos. Eventos acionados a mais de 1.000/s são bons candidatos a uma palavra-chave exclusiva.

Tipos de parâmetros suportados

EventSource requer que todos os parâmetros de evento possam ser serializados para que aceite apenas um conjunto limitado de tipos. São as seguintes:

  • Primitivos: bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr, e UIntPtr, Guid decimal, string, DateTime, DateTimeOffset, TimeSpan
  • Enumerações
  • Estruturas atribuídas com System.Diagnostics.Tracing.EventDataAttribute. Somente as propriedades de instância pública com tipos serializáveis serão serializadas.
  • Tipos anônimos em que todas as propriedades públicas são tipos serializáveis
  • Matrizes de tipos serializáveis
  • T anulável<onde> T é um tipo serializável
  • KeyValuePair<T, U> , onde T e você são ambos tipos serializáveis
  • Tipos que implementam IEnumerable<T> para exatamente um tipo T e onde T é um tipo serializável

Resolução de Problemas

A classe EventSource foi projetada para que nunca lançasse uma exceção por padrão. Essa é uma propriedade útil, pois o registro em log geralmente é tratado como opcional, e você geralmente não quer que um erro ao escrever uma mensagem de log faça com que seu aplicativo falhe. No entanto, isso torna difícil encontrar qualquer erro em seu EventSource. Aqui estão várias técnicas que podem ajudar a solucionar problemas:

  1. O construtor EventSource tem sobrecargas que levam EventSourceSettings. Tente ativar o sinalizador ThrowOnEventWriteErrors temporariamente.
  2. A EventSource.ConstructionException propriedade armazena qualquer Exception que foi gerada ao validar os métodos de log de eventos. Isso pode revelar vários erros de criação.
  3. EventSource registra erros usando a ID de evento 0, e esse evento de erro tem uma cadeia de caracteres descrevendo o erro.
  4. Durante a depuração, essa mesma cadeia de caracteres de erro também será registrada usando Debug.WriteLine() e aparecerá na janela de saída de depuração.
  5. EventSource lança internamente e, em seguida, captura exceções quando ocorrem erros. Para observar quando essas exceções estão ocorrendo, habilite exceções de primeira chance em um depurador ou use o rastreamento de eventos com os eventos de exceção do tempo de execução do .NET habilitados.

Personalizações avançadas

Definindo OpCodes e tarefas

O ETW tem conceitos de Tarefas e OpCodes , que são outros mecanismos para marcar e filtrar eventos. Você pode associar eventos a tarefas e opcodes específicos usando as Task propriedades e Opcode . Eis um exemplo:

[EventSource(Name = "Samples-EventSourceDemos-Customized")]
public sealed class CustomizedEventSource : EventSource
{
    static public CustomizedEventSource Log { get; } = new CustomizedEventSource();

    [Event(1, Task = Tasks.Request, Opcode=EventOpcode.Start)]
    public void RequestStart(int RequestID, string Url)
    {
        WriteEvent(1, RequestID, Url);
    }

    [Event(2, Task = Tasks.Request, Opcode=EventOpcode.Info)]
    public void RequestPhase(int RequestID, string PhaseName)
    {
        WriteEvent(2, RequestID, PhaseName);
    }

    [Event(3, Keywords = Keywords.Requests,
           Task = Tasks.Request, Opcode=EventOpcode.Stop)]
    public void RequestStop(int RequestID)
    {
        WriteEvent(3, RequestID);
    }

    public class Tasks
    {
        public const EventTask Request = (EventTask)0x1;
    }
}

Você pode criar implicitamente objetos EventTask declarando dois métodos de evento com IDs de evento subsequentes que têm o padrão <de nomenclatura EventName>Start e <EventName>Stop. Esses eventos devem ser declarados um ao lado do outro na definição de classe e o <método EventName>Start deve vir primeiro.

Autodescrição (rastreamento) vs. formatos de evento manifesto

Esse conceito só é importante ao assinar o EventSource do ETW. O ETW tem duas maneiras diferentes de registrar eventos: formato de manifesto e formato de autodescrição (às vezes chamado de tracelogging). Os objetos EventSource baseados em manifesto geram e registram um documento XML que representa os eventos definidos na classe após a inicialização. Isso requer que o EventSource reflita sobre si mesmo para gerar o provedor e os metadados do evento. No formato de autodescrição, os metadados de cada evento são transmitidos em linha com os dados do evento, em vez de antecipadamente. A abordagem de autodescrição suporta os métodos mais flexíveis Write que podem enviar eventos arbitrários sem ter criado um método de log de eventos predefinido. Também é um pouco mais rápido na inicialização porque evita uma reflexão ansiosa. No entanto, os metadados extras emitidos com cada evento adicionam uma pequena sobrecarga de desempenho, o que pode não ser desejável ao enviar um grande volume de eventos.

Para usar o formato de evento autodescritivo, construa seu EventSource usando o construtor EventSource(String), o construtor EventSource(String, EventSourceSettings) ou definindo o sinalizador EtwSelfDescribingEventFormat em EventSourceSettings.

Tipos EventSource implementando interfaces

Um tipo EventSource pode implementar uma interface para se integrar perfeitamente em vários sistemas de log avançados que usam interfaces para definir um destino de log comum. Aqui está um exemplo de um possível uso:

public interface IMyLogging
{
    void Error(int errorCode, string msg);
    void Warning(string msg);
}

[EventSource(Name = "Samples-EventSourceDemos-MyComponentLogging")]
public sealed class MyLoggingEventSource : EventSource, IMyLogging
{
    public static MyLoggingEventSource Log { get; } = new MyLoggingEventSource();

    [Event(1)]
    public void Error(int errorCode, string msg)
    { WriteEvent(1, errorCode, msg); }

    [Event(2)]
    public void Warning(string msg)
    { WriteEvent(2, msg); }
}

Você deve especificar o EventAttribute nos métodos de interface, caso contrário (por motivos de compatibilidade) o método não será tratado como um método de log. A implementação do método de interface explícita não é permitida para evitar colisões de nomenclatura.

Hierarquias de classe EventSource

Na maioria dos casos, você poderá escrever tipos que derivam diretamente da classe EventSource. Às vezes, no entanto, é útil definir a funcionalidade que será compartilhada por vários tipos derivados de EventSource, como sobrecargas WriteEvent personalizadas (consulte Otimizando o desempenho para eventos de alto volume abaixo).

As classes base abstratas podem ser usadas desde que não definam palavras-chave, tarefas, opcodes, canais ou eventos. Aqui está um exemplo em que a classe UtilBaseEventSource define uma sobrecarga WriteEvent otimizada que é necessária para vários EventSources derivados no mesmo componente. Um desses tipos derivados é ilustrado abaixo como OptimizedEventSource.

public abstract class UtilBaseEventSource : EventSource
{
    protected UtilBaseEventSource()
        : base()
    { }
    protected UtilBaseEventSource(bool throwOnEventWriteErrors)
        : base(throwOnEventWriteErrors)
    { }

    protected unsafe void WriteEvent(int eventId, int arg1, short arg2, long arg3)
    {
        if (IsEnabled())
        {
            EventSource.EventData* descrs = stackalloc EventSource.EventData[2];
            descrs[0].DataPointer = (IntPtr)(&arg1);
            descrs[0].Size = 4;
            descrs[1].DataPointer = (IntPtr)(&arg2);
            descrs[1].Size = 2;
            descrs[2].DataPointer = (IntPtr)(&arg3);
            descrs[2].Size = 8;
            WriteEventCore(eventId, 3, descrs);
        }
    }
}

[EventSource(Name = "OptimizedEventSource")]
public sealed class OptimizedEventSource : UtilBaseEventSource
{
    public static OptimizedEventSource Log { get; } = new OptimizedEventSource();

    [Event(1, Keywords = Keywords.Kwd1, Level = EventLevel.Informational,
           Message = "LogElements called {0}/{1}/{2}.")]
    public void LogElements(int n, short sh, long l)
    {
        WriteEvent(1, n, sh, l); // Calls UtilBaseEventSource.WriteEvent
    }

    #region Keywords / Tasks /Opcodes / Channels
    public static class Keywords
    {
        public const EventKeywords Kwd1 = (EventKeywords)1;
    }
    #endregion
}

Otimizando o desempenho para eventos de alto volume

A classe EventSource tem várias sobrecargas para WriteEvent, incluindo uma para o número variável de argumentos. Quando nenhuma das outras sobrecargas corresponde, o método params é chamado. Infelizmente, a sobrecarga de params é relativamente cara. Em especial:

  1. Aloca uma matriz para armazenar os argumentos da variável.
  2. Converte cada parâmetro em um objeto, o que causa alocações para tipos de valor.
  3. Atribui esses objetos à matriz.
  4. Chama a função.
  5. Descobre o tipo de cada elemento de matriz para determinar como serializá-lo.

Isto é provavelmente 10 a 20 vezes mais caro do que os tipos especializados. Isso não importa muito para casos de baixo volume, mas para eventos de alto volume pode ser importante. Existem dois casos importantes para garantir que a sobrecarga de params não seja usada:

  1. Certifique-se de que os tipos enumerados são convertidos para 'int' para que correspondam a uma das sobrecargas rápidas.
  2. Crie novas sobrecargas rápidas de WriteEvent para cargas úteis de alto volume.

Aqui está um exemplo para adicionar uma sobrecarga WriteEvent que usa quatro argumentos inteiros

[NonEvent]
public unsafe void WriteEvent(int eventId, int arg1, int arg2,
                              int arg3, int arg4)
{
    EventData* descrs = stackalloc EventProvider.EventData[4];

    descrs[0].DataPointer = (IntPtr)(&arg1);
    descrs[0].Size = 4;
    descrs[1].DataPointer = (IntPtr)(&arg2);
    descrs[1].Size = 4;
    descrs[2].DataPointer = (IntPtr)(&arg3);
    descrs[2].Size = 4;
    descrs[3].DataPointer = (IntPtr)(&arg4);
    descrs[3].Size = 4;

    WriteEventCore(eventId, 4, (IntPtr)descrs);
}