Compartir a través de


Instrumentación de código para crear eventos de EventSource

Este artículo se aplica a: ✔️ .NET Core 3.1 y versiones posteriores ✔️ .NET Framework 4.5 y versiones posteriores

En la guía de introducción se muestra cómo crear un EventSource mínimo y recopilar eventos en un archivo de seguimiento. En este tutorial se explica más detalladamente cómo crear eventos mediante System.Diagnostics.Tracing.EventSource.

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

La estructura básica de un EventSource derivado es siempre la misma. En particular:

  • La clase hereda de System.Diagnostics.Tracing.EventSource
  • Para cada tipo de evento que quiera generar, es necesario definir un método. Este método debe denominarse con el nombre del evento que se va a crear. Si el evento tiene datos adicionales, se deben pasar mediante argumentos. Estos argumentos de evento deben serializarse, por lo que solo se permiten determinados tipos.
  • Cada método tiene un cuerpo que llama a WriteEvent pasando un identificador (un valor numérico que representa el evento) y los argumentos del método de evento. El identificador debe ser único dentro de EventSource. El identificador se asigna explícitamente mediante el System.Diagnostics.Tracing.EventAttribute
  • Las EventSources están pensadas para ser instancias singleton. Por lo tanto, es conveniente definir una variable estática, por convención denominada Log, que representa este singleton.

Reglas para definir métodos de evento

  1. Cualquier método de instancia, no virtual y sin valor de retorno definido en una clase EventSource es, por defecto, un método de registro de eventos.
  2. Los métodos virtuales o que no devuelven void solo se incluyen si están marcados con el System.Diagnostics.Tracing.EventAttribute
  3. Para marcar un método calificado como de no registro, debe indicarlo con System.Diagnostics.Tracing.NonEventAttribute
  4. Los métodos de registro de eventos tienen identificadores de evento asociados. Esto se puede hacer explícitamente mediante la decoración del método con un System.Diagnostics.Tracing.EventAttribute o implícitamente por el número ordinal del método en la clase . Por ejemplo, al utilizar numeración implícita, el primer método de la clase tiene ID 1, el segundo tiene ID 2, etc.
  5. Los métodos de registro de eventos deben comunicarse con WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId o la sobrecarga WriteEventWithRelatedActivityIdCore.
  6. El identificador de evento, ya sea implícito o explícito, debe coincidir con el primer argumento pasado a la API WriteEvent* a la que llama.
  7. El número, los tipos y el orden de los argumentos pasados al método EventSource deben alinearse con la forma en que se pasan a las API WriteEvent*. Para WriteEvent, los argumentos van después del identificador de evento; para WriteEventWithRelatedActivityId, los argumentos van después de relatedActivityId. Para los métodos WriteEvent*Core, los argumentos se deben serializar manualmente en el parámetro data.
  8. Los nombres de evento no pueden contener caracteres < ni >. Aunque los métodos definidos por el usuario tampoco pueden contener estos caracteres, el compilador volverá a escribir los métodos async para contenerlos. Para asegurarse de que estos métodos generados no se convierten en eventos, marque todos los métodos que no son de evento en un EventSource con el NonEventAttribute.

Procedimientos recomendados

  1. Los tipos que derivan de EventSource normalmente no tienen tipos intermedios en la jerarquía ni implementan interfaces. Consulte Personalizaciones avanzadas a continuación para ver algunas excepciones en las que esto puede resultar útil.
  2. Por lo general, el nombre de la clase EventSource no es un nombre público adecuado para EventSource. Los nombres públicos, los nombres que se mostrarán en las configuraciones de registro y los visores de registros, deben ser únicos globalmente. Por lo tanto, se recomienda asignar un nombre público a EventSource mediante el System.Diagnostics.Tracing.EventSourceAttribute. El nombre "Demo" usado anteriormente es corto y es poco probable que sea único, por lo que no es una buena opción para su uso en producción. Una convención común consiste en usar un nombre jerárquico con . o - como separador, como "MyCompany-Samples-Demo", o el nombre del ensamblado o el espacio de nombres para el que eventSource proporciona eventos. No se recomienda incluir "EventSource" como parte del nombre público.
  3. Asignar identificadores de evento explícitamente, de esta manera, los cambios aparentemente benignos en el código de la clase fuente, como reorganizarlo o agregar un método en el medio, no cambiarán el identificador de evento asociado a cada método.
  4. Al crear eventos que representan el inicio y el final de una unidad de trabajo, por convención, estos métodos se denominan con sufijos "Start" y "Stop". Por ejemplo, "RequestStart" y "RequestStop".
  5. No especifique un valor explícito para la propiedad Guid de EventSourceAttribute, a menos que lo necesite por motivos de compatibilidad con versiones anteriores. El valor GUID predeterminado se deriva del nombre de origen, lo que permite a las herramientas aceptar el nombre más legible por humanos y derivar el mismo GUID.
  6. Llame a IsEnabled() antes de realizar cualquier trabajo intensivo de recursos relacionado con la activación de un evento, como calcular un argumento de evento costoso que no será necesario si el evento está deshabilitado.
  7. Intente hacer que el objeto EventSource vuelva a ser compatible y versionarlos adecuadamente. La versión predeterminada de un evento es 0. La versión se puede cambiar estableciendo EventAttribute.Version. Cambie la versión de un evento cada vez que cambie los datos que se serializan con él. Agregue siempre nuevos datos serializados al final de la declaración de evento, es decir, al final de la lista de parámetros de método. Si esto no es posible, cree un nuevo evento con un nuevo identificador para reemplazar el anterior.
  8. Al declarar métodos de eventos, especifique los datos de carga de tamaño fijo antes de los datos de tamaño variable.
  9. No use cadenas que contengan caracteres NULL. Al generar el manifiesto para ETW EventSource, se declararán todas las cadenas como terminadas en null, aunque es posible tener un carácter NULL en una cadena de C#. Si una cadena contiene un carácter NULL, toda la cadena se escribirá en la carga del evento, pero cualquier analizador tratará el primer carácter NULL como final de la cadena. Si hay argumentos de carga después de la cadena, el resto de la cadena se analizará en lugar del valor previsto.

Personalizaciones de eventos típicas

Establecimiento de los niveles de detalle del evento

Cada evento tiene un nivel de verbosidad y los suscriptores de eventos suelen habilitar todos los eventos en un EventSource hasta un determinado nivel de verbosidad. Los eventos declaran su nivel de verbosidad mediante la propiedad Level. Por ejemplo, en el siguiente EventSource, un suscriptor que solicite eventos de nivel Informativo o inferior no registrará el evento Verbose DebugMessage.

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

Si el nivel de detalle de un evento está especificado en el EventAttribute, el valor predeterminado es Informational (Informativo).

Procedimiento recomendado

Use niveles inferiores a Informativos para advertencias o errores relativamente poco frecuentes. En caso de duda, siga con el valor predeterminado de Informational y use Verbose para los eventos que se producen con más frecuencia que 1000 eventos por segundo.

Establecer palabras clave de evento

Algunos sistemas de seguimiento de eventos admiten palabras clave como un mecanismo de filtrado adicional. A diferencia de la verbosidad que clasifica los eventos por nivel de detalle, las palabras clave están pensadas para clasificar los eventos según otros criterios, como áreas de funcionalidad del código o aquellos que serían útiles para diagnosticar determinados problemas. Las palabras clave se denominan marcas de bits y cada evento puede tener cualquier combinación de palabras clave aplicadas. Por ejemplo, EventSource siguiente define algunos eventos relacionados con el procesamiento de solicitudes y otros eventos relacionados con el inicio. Si un desarrollador quisiera analizar el rendimiento del inicio, podría habilitar solo el registro de los eventos marcados con la palabra clave de inicio.

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

Las palabras clave deben definirse mediante una clase anidada denominada Keywords y cada palabra clave individual se define mediante un miembro con tipo public const EventKeywords.

Procedimiento recomendado

Las palabras clave son más importantes al distinguir entre eventos de gran volumen. Esto permite a un consumidor de eventos elevar el nivel de detalle a un nivel alto, pero administrar la sobrecarga de rendimiento y el tamaño del registro habilitando solo subconjuntos estrechos de los eventos. Los eventos que se desencadenan más de 1000 por segundo son buenos candidatos para una palabra clave única.

Tipos de parámetros admitidos

EventSource requiere que todos los parámetros de evento se puedan serializar para que solo acepte un conjunto limitado de tipos. Estos son:

  • Primitivos: bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr y UIntPtr, Guid decimal, string, DateTime, DateTimeOffset, TimeSpan
  • Enumeraciones
  • Estructuras con atributos con System.Diagnostics.Tracing.EventDataAttribute. Solo se serializarán las propiedades de instancia pública con tipos serializables.
  • Tipos anónimos donde todas las propiedades públicas son tipos serializables
  • Matrices de tipos serializables
  • <T> que admite un valor NULL es un tipo serializable
  • KeyValuePair<T, U> donde T y ambos son tipos serializables
  • Tipos que implementan IEnumerable<T> para exactamente un tipo T y donde T es un tipo serializable

Solución de problemas

La clase EventSource se diseñó para que nunca lanzara una excepción de forma predeterminada. Se trata de una propiedad útil, ya que el registro suele tratarse como opcional y normalmente no quiere que se produzca un error al escribir un mensaje de registro para provocar un error en la aplicación. Sin embargo, esto dificulta la búsqueda de cualquier error en EventSource. Estas son varias técnicas que pueden ayudar a solucionar problemas:

  1. El constructor EventSource tiene sobrecargas que toman EventSourceSettings. Intente habilitar de forma temporal la marca ThrowOnEventWriteErrors.
  2. La propiedad EventSource.ConstructionException almacena cualquier excepción que se generó al validar los métodos de registro de eventos. Esto puede revelar varios errores de creación.
  3. EventSource registra errores mediante el identificador de evento 0 y este evento de error tiene una cadena que describe el error.
  4. Al depurar, esa misma cadena de error también se registrará con Debug.WriteLine() y se mostrará en la ventana de salida de depuración.
  5. EventSource se inicia de forma interna y, a continuación, detecta excepciones cuando se producen errores. Para observar cuando se producen estas excepciones, habilite las excepciones de primera oportunidad en un depurador o use el seguimiento de evento habilitando los eventos de excepción de .NET runtime.

Personalizaciones avanzadas

Establecimiento de códigos de operación y tareas

ETW tiene conceptos de Tasks y OpCodes, que son mecanismos adicionales para etiquetar y filtrar eventos. Puede asociar eventos con tareas y códigos de operación específicos mediante las propiedades Task y Opcode. Este es un ejemplo:

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

Puede crear implícitamente objetos EventTask declarando dos métodos de evento con identificadores de evento posteriores que tienen el patrón de nomenclatura <EventName>Start y <EventName>Stop. Estos eventos deben declararse junto a otros en la definición de clase y el <eventName>método Start debe venir primero.

Diferencias entre autodescripción (registro de seguimiento) y formatos de eventos de manifiesto

Este concepto solo importa al suscribirse a EventSource desde ETW. ETW tiene dos maneras diferentes de registrar eventos, formato de manifiesto y formato autodescripto (a veces denominado registro de seguimiento). Los objetos EventSource basados en manifiesto generan y registran un documento XML que representa los eventos definidos en la clase tras la inicialización. Esto requiere que EventSource se refleje sobre sí mismo para generar los metadatos del proveedor y del evento. En los metadatos de formato de autodescripción para cada evento se transmite en línea con los datos de evento en lugar de por adelantado. El enfoque autodescriptivo admite los métodos de Write más flexibles que pueden enviar eventos arbitrarios sin haber creado un método de registro de eventos predefinido. También es ligeramente más rápido al iniciarse porque evita la reflexión diligente. Sin embargo, los metadatos adicionales emitidos con cada evento agregan una pequeña sobrecarga de rendimiento, lo que puede no ser deseable al enviar un gran volumen de eventos.

Para usar el formato de evento autodescriptor, construya EventSource mediante el constructor EventSource(String), el constructor EventSource(String, EventSourceSettings) o estableciendo la marca EtwSelfDescribingEventFormat en EventSourceSettings.

Tipos de EventSource que implementan interfaces

Un tipo EventSource puede implementar una interfaz para integrarse sin problemas en varios sistemas de registro avanzados que usan interfaces para definir un destino de registro común. Este es un ejemplo de un posible 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); }
}

Debe especificar EventAttribute en los métodos de interfaz; de lo contrario, (por motivos de compatibilidad), el método no se tratará como un método de registro. No se permite la implementación explícita del método de interfaz para evitar colisiones de nomenclatura.

Jerarquías de clases de EventSource

En la mayoría de los casos, podrá escribir tipos que deriven directamente de la clase EventSource. A veces, sin embargo, es útil definir funcionalidades que serán compartidas por varios tipos derivados de EventSource, como personalizar las sobrecargas de WriteEvent (ver más abajo en sobre la optimización del rendimiento para eventos de gran volumen).

Las clases base abstractas se pueden usar siempre y cuando no definan palabras clave, tareas, códigos de operación, canales o eventos. Este es un ejemplo en el que la clase UtilBaseEventSource define una sobrecarga WriteEvent optimizada que necesita varios EventSources derivados en el mismo componente. Uno de estos tipos derivados se muestra a continuación 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
}

Optimización del rendimiento para eventos de gran volumen

La clase EventSource tiene una serie de sobrecargas para WriteEvent, incluido uno para el número variable de argumentos. Cuando ninguna de las otras sobrecargas coincide, se invoca el método `params`. Desafortunadamente, la sobrecarga de parámetros es relativamente costosa. En concreto:

  1. Asigna una matriz para contener los argumentos de variable.
  2. Convierte cada parámetro en un objeto , lo que provoca asignaciones para los tipos de valor.
  3. Asigna estos objetos a la matriz.
  4. Llama a la función.
  5. Determina el tipo de cada elemento de matriz para determinar cómo serializarlo.

Esto probablemente es de 10 a 20 veces tan caro como tipos especializados. Esto no importa mucho para los casos de bajo volumen, pero para eventos de gran volumen puede ser importante. Hay dos casos importantes para asegurarse de que no se usa la sobrecarga de parámetros:

  1. Asegúrese de que los tipos enumerados se convierten en «int» para que coincidan con una de las sobrecargas rápidas.
  2. Cree nuevas sobrecargas de WriteEvent rápidas para cargas de gran volumen.

Este es un ejemplo para agregar una sobrecarga WriteEvent que toma cuatro argumentos enteros.

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