Compartilhar via


Instrumentar o código para criar eventos EventSource

Este artigo se aplica 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 de como criar eventos usando 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. Especialmente:

  • A classe é herdada 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, eles deverão ser passados usando argumentos. Esses argumentos de evento precisam ser serializados para que apenas determinados tipos sejam permitidos.
  • Cada método tem um corpo que chama WriteEvent passando uma ID (um valor numérico que representa o evento) e os argumentos do método de evento. A ID precisa ser exclusiva no EventSource. A ID é atribuída explicitamente usando o System.Diagnostics.Tracing.EventAttribute
  • Os EventSources devem ser instâncias singleton. Portanto, convém definir uma variável estática, por convenção chamada de Log, que represente esse singleton.

Regras para definir métodos de evento

  1. Qualquer método que retorne nulo de instância, não virtual, definido em uma classe EventSource é, por padrão, um método de registro em log de eventos.
  2. Os métodos virtuais ou que não retornam nulos serão incluídos somente se estiverem marcados com o System.Diagnostics.Tracing.EventAttribute
  3. Para marcar um método qualificado indicando que ele não é de registro em log, acrescente System.Diagnostics.Tracing.NonEventAttribute a ele
  4. Os métodos de registro em log de eventos têm IDs de evento associadas. Isso pode ser feito explicitamente acrescentando ao método um System.Diagnostics.Tracing.EventAttribute ou implicitamente pelo número ordinal do método na classe. Por exemplo, usando a numeração implícita, o primeiro método na classe tem a ID 1, o segundo tem a ID 2 e assim por diante.
  5. Os métodos de registro em log de eventos precisam chamar uma sobrecarga WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId ou WriteEventWithRelatedActivityIdCore.
  6. A ID do evento, seja implícita ou explícita, precisa corresponder ao primeiro argumento passado à API WriteEvent* que é chamada.
  7. O número, os tipos e a ordem dos argumentos passados ao método EventSource precisam se alinhar à forma como eles são passados às APIs WriteEvent*. Para WriteEvent, os argumentos seguem a ID do evento, para WriteEventWithRelatedActivityId, os argumentos seguem a relatedActivityId. Para os métodos WriteEvent*Core, os argumentos precisam ser serializados manualmente no parâmetro data.
  8. Os nomes de eventos não podem conter caracteres < ou >. Embora os métodos definidos pelo usuário também não possam conter esses caracteres, os métodos async serão reescritos pelo compilador para que os contenham. Para garantir que esses métodos gerados não se tornem eventos, marque todos os métodos que não são de eventos em um EventSource com NonEventAttribute.

Práticas recomendadas

  1. Os tipos derivados de EventSource geralmente não têm tipos intermediários na hierarquia nem implementam interfaces. Confira Personalizações avançadas abaixo para ver algumas exceções em que isso pode ser útil.
  2. Geralmente, o nome da classe EventSource é um nome público inválido para o EventSource. Os nomes públicos, ou seja, aqueles que aparecerão em configurações de log e visualizadores de log, devem ser exclusivos globalmente. Portanto, uma boa prática é dar ao EventSource um nome público usando o System.Diagnostics.Tracing.EventSourceAttribute. O nome "Demo" usado acima é curto e provavelmente não exclusivo, portanto, não é uma boa opção 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 do namespace para o qual o EventSource fornece eventos. Não é recomendado incluir "EventSource" dentro do nome público.
  3. A atribuição explícita de IDs de Evento causando alterações aparentemente válidas ao código na classe de origem, como a reorganização ou a adição de um método no meio, não altera a ID do 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 ela seja necessária para manter a compatibilidade com versões anteriores. O valor padrão do Guid é derivado do nome da origem, que permite que as ferramentas aceitem o nome mais legível para pessoas e derivem o mesmo Guid.
  6. Chame IsEnabled() antes de executar trabalhos com uso intensivo de recursos relacionados ao disparo de um evento, como a computação de um argumento de evento dispendioso que não é necessário quando o evento está desabilitado.
  7. Tente manter o objeto EventSource compatível com versões anteriores e defina a versão dele adequadamente. A versão padrão de um evento é 0. A versão pode ser alterada configurando EventAttribute.Version. Altere a versão de um evento sempre que você alterar os dados serializados com ele. Sempre adicione novos dados serializados ao final da declaração de evento, ou seja, ao final da lista de parâmetros de método. Se isso não for possível, crie um evento com outra ID para substituir a antiga.
  8. Ao declarar métodos de eventos, especifique dados de conteúdo de tamanho fixo antes dos 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 em nulo, mesmo que seja possível ter um caractere nulo em uma cadeia de caracteres C#. Se uma cadeia de caracteres contiver um caractere nulo, toda a cadeia de caracteres será gravada no conteúdo do evento, mas qualquer analisador tratará o primeiro caractere nulo como o final da cadeia de caracteres. Se houver argumentos de conteúdo após a cadeia de caracteres, as outras cadeias de caracteres serão analisadas em vez do valor pretendido.

Personalizações típicas de eventos

Como definir níveis de detalhamento de evento

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

[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, ele será assumido como informativo.

Melhor prática

Use níveis inferiores aos informativos para avisos ou erros relativamente raros. Quando estiver em dúvida, use o padrão informativo e use o detalhamento para eventos que ocorrem com mais frequência do que 1000 eventos/s.

Como definir palavras-chave de evento

Alguns sistemas de rastreamento de eventos dão suporte a palavras-chave como um mecanismo de filtragem adicional. Ao contrário do detalhamento que categoriza eventos por nível de detalhes, as palavras-chave categorizam eventos com base em outros critérios, como áreas de funcionalidade de código ou quais seriam úteis para diagnosticar determinados problemas. As palavras-chave são sinalizadores de bit nomeados e cada evento pode ter qualquer combinação de palavras-chave aplicada. Por exemplo, o EventSource abaixo define alguns eventos relacionados ao processamento de solicitações e outros relacionados à inicialização. Se um desenvolvedor quiser analisar o desempenho da inicialização, ele só poderia habilitar o registro em log 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 precisam ser definidas usando uma classe aninhada chamada Keywords e cada palavra-chave individual é definida por um membro do tipo public const EventKeywords.

Melhor prática

As palavras-chave tem maior importância para distinguir eventos de alto volume. Isso permite que um consumidor do evento eleve o detalhamento para um nível alto, mas gerencie a sobrecarga de desempenho e o tamanho do log habilitando apenas subconjuntos menores dos eventos. Os eventos que são disparados mais de 1.000/s são bons candidatos para uma palavra-chave exclusiva.

Tipos de parâmetros com suporte

O EventSource requer que todos os parâmetros de evento possam ser serializados para que ele aceite apenas um conjunto limitado de tipos. Eles são:

  • Primitivas: booliano, 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
  • Nullable<T> em que T é um tipo serializável
  • KeyValuePair<T, U> em que T e U são tipos serializáveis
  • Tipos que implementam IEnumerable<T> para exatamente um tipo T, em que T é um tipo serializável

Solução de problemas

A classe EventSource foi projetada para nunca gerar uma Exceção por padrão. Essa é uma propriedade útil, pois o registro em log geralmente é tratado como opcional e um erro ao escrever uma mensagem de log não poderia fazer com que o aplicativo falhasse. No entanto, isso dificulta a localização de erros no EventSource. Veja várias técnicas que podem ajudar a solucionar o problema:

  1. O construtor do EventSource tem sobrecargas que usam EventSourceSettings. Tente habilitar temporariamente o sinalizador ThrowOnEventWriteErrors.
  2. A propriedade EventSource.ConstructionException armazena qualquer Exceção gerada ao validar os métodos de registro em log de eventos. Isso pode revelar vários erros de criação.
  3. O EventSource registra os erros em log usando a ID do evento 0 e esse evento de erro tem uma cadeia de caracteres que descreve o erro.
  4. Durante a depuração, essa mesma cadeia de caracteres de erro também é registrada usando Debug.WriteLine() e aparece na janela de saída de depuração.
  5. O EventSource gera e depois captura internamente as 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 runtime do .NET habilitados.

Personalizações avançadas

Como configurar OpCodes e Tarefas

O ETW tem os conceitos de Tarefas e OpCodes que são mecanismos adicionais para marcar e filtrar eventos. Você pode associar eventos a tarefas e opcodes específicos usando as propriedades Task e Opcode. Aqui está 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 objetos EventTask implicitamente declarando dois métodos de evento com IDs de evento subsequentes que tenham o padrão de nomenclatura <EventName>Start e <EventName>Stop. Esses eventos precisam ser declarados um após o outro na definição de classe e o método <EventName>Start precisa ser o primeiro.

Autodescrição (tracelogging) versus formatos de evento de manifesto

Esse conceito só importa quando o EventSource é assinado por meio do ETW. O ETW tem duas maneiras diferentes de registrar eventos, formato de manifesto e autodescrição (às vezes chamada de tracelogging). Os objetos do 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 autodescritivo, os metadados de cada evento são transmitidos embutidos com os dados do evento em vez de antecipadamente. A abordagem autodescritiva dá suporte a métodos Write mais flexíveis que podem enviar eventos arbitrários sem exigir a criação de um método de registro em log de eventos predefinido. Ele também é um pouco mais rápido na inicialização porque evita uma reflexão adiantada. 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 o EventSource usando o construtor EventSource(String) ou o construtor EventSource(String, EventSourceSettings) ou definindo o sinalizador EtwSelfDescribingEventFormat em EventSourceSettings.

Tipos de EventSource que implementam interfaces

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

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ê precisa especificar 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 registro em log. A implementação explícita do método de interface não é permitida para evitar colisões de nomenclatura.

Hierarquias da classe EventSource

Na maioria dos casos, você pode escrever tipos derivados diretamente da classe EventSource. Mas, às vezes, é útil definir a funcionalidade que será compartilhada por vários tipos de EventSource derivados, como sobrecargas personalizadas de WriteEvent (confira Como otimizar o desempenho para eventos de alto volume abaixo).

Classes base abstratas podem ser usadas desde que não definam palavras-chave, tarefas, opcodes, canais ou eventos. Veja um exemplo em que a classe UtilBaseEventSource define uma sobrecarga de 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
}

Como otimizar o desempenho para eventos de alto volume

A classe EventSource tem uma série de sobrecargas de 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 dispendiosa. Em particular:

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

Isso provavelmente é de 10 a 20 vezes dispendioso que os tipos especializados. Isso não importa muito para casos de baixo volume, mas para eventos de alto volume, pode ser importante. Há dois casos importantes para assegurar que a sobrecarga de params não seja usada:

  1. Verificar se os tipos enumerados são convertidos em 'int' para que correspondam a uma das sobrecargas rápidas.
  2. Criar sobrecargas rápidas de WriteEvent para conteúdos de alto volume.

Veja um exemplo para adicionar uma sobrecarga de 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);
}