Partager via


Code d’instrument pour créer des événements EventSource

Cet article s’applique à : ✔️ .NET Core 3.1 et versions ultérieures✔️ .NET Framework 4.5 et versions ultérieures

Le guide de prise en main vous a montré comment créer un EventSource minimal et collecter des événements dans un fichier de trace. Ce tutoriel décrit plus en détail la création d’événements à l’aide de System.Diagnostics.Tracing.EventSource.

Un EventSource minimal

[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 structure de base d’un EventSource dérivé est toujours la même. En particulier :

  • La classe hérite de System.Diagnostics.Tracing.EventSource
  • Pour chaque type d’événement que vous souhaitez générer, une méthode doit être définie. Cette méthode doit être nommée à l’aide du nom de l’événement en cours de création. Si l’événement contient des données supplémentaires, celles-ci doivent être passées à l’aide d’arguments. Ces arguments d’événement doivent être sérialisés afin que seuls certains types soient autorisés.
  • Chaque méthode a un corps qui appelle WriteEvent en lui passant un ID (valeur numérique qui représente l’événement) et les arguments de la méthode d’événement. L’ID doit être unique dans EventSource. L’ID est explicitement attribué à l’aide du System.Diagnostics.Tracing.EventAttribute
  • Les EventSources sont destinés à être des instances singleton. Il est donc pratique de définir une variable statique, par convention appelée Log, qui représente ce singleton.

Règles de définition des méthodes d’événement

  1. Toute méthode d’instance, non virtuelle, de retour void définie dans une classe EventSource est par défaut une méthode de journalisation des événements.
  2. Les méthodes virtuelles ou non retournées par void sont incluses uniquement si elles sont marquées avec System.Diagnostics.Tracing.EventAttribute
  3. Pour marquer une méthode éligible comme non journalisante, vous devez la décorer avec System.Diagnostics.Tracing.NonEventAttribute
  4. Des ID d’événement sont associés aux méthodes de journalisation des événements. Cela peut être fait explicitement en décorant la méthode avec System.Diagnostics.Tracing.EventAttribute ou implicitement par le numéro ordinal de la méthode dans la classe. Par exemple, en utilisant la numérotation implicite, la première méthode de la classe a l’ID 1, la deuxième a l’ID 2, et ainsi de suite.
  5. Les méthodes de journalisation des événements doivent appeler une surcharge WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId ou WriteEventWithRelatedActivityIdCore.
  6. L’ID d’événement, qu’il soit implicite ou explicite, doit correspondre au premier argument passé à l’API WriteEvent* qu’il appelle.
  7. Le nombre, les types et l’ordre des arguments passés à la méthode EventSource doivent s’aligner sur la façon dont ils sont transmis aux API WriteEvent*. Pour WriteEvent, les arguments suivent l’ID d’événement ; pour WriteEventWithRelatedActivityId, les arguments suivent l’ID d’activité relatedActivityId. Pour les méthodes WriteEvent*Core, les arguments doivent être sérialisés manuellement dans le paramètre data.
  8. Les noms d’événements ne peuvent pas contenir de caractères < ou >. Bien que les méthodes définies par l’utilisateur ne puissent pas non plus contenir ces caractères, les méthodes async seront réécrites par le compilateur pour les contenir. Pour être sûr que ces méthodes générées ne deviennent pas des événements, marquez toutes les méthodes hors événement sur un EventSource avec NonEventAttribute.

Meilleures pratiques

  1. Les types qui dérivent d’EventSource n’ont généralement pas de types intermédiaires dans la hiérarchie ou implémentent des interfaces. Consultez Personnalisations avancées ci-dessous pour connaître certaines exceptions qui peuvent s’avérer utiles.
  2. En règle générale, le nom de la classe EventSource est un mauvais nom public pour EventSource. Les noms publics, les noms qui apparaîtront dans les configurations de journalisation et les visionneuses de journaux, doivent être globalement uniques. Il est donc recommandé de donner à votre EventSource un nom public avec System.Diagnostics.Tracing.EventSourceAttribute. Le nom « Demo » utilisé ci-dessus est court et peu susceptible d’être unique ; ce n’est donc pas un bon choix pour une utilisation en production. Une convention courante consiste à utiliser un nom hiérarchique avec . ou - comme séparateur, comme « MyCompany-Samples-Demo », ou le nom de l’assembly ou de l’espace de noms pour lequel EventSource fournit des événements. Il n’est pas recommandé d’inclure « EventSource » dans le nom public.
  3. Affectez des ID d’événement explicitement. De cette façon, les modifications apparemment bénignes apportées au code dans la classe source, comme la réorganisation ou l’ajout d’une méthode au milieu, ne modifient pas l’ID d’événement associé à chaque méthode.
  4. Lors de la création d’événements qui représentent le début et la fin d’une unité de travail, par convention, ces méthodes sont nommées avec les suffixes « Start » et « Stop ». Par exemple, « RequestStart » et « RequestStop ».
  5. Ne spécifiez pas de valeur explicite pour la propriété GUID d’EventSourceAttribute, sauf si vous en avez besoin pour des raisons de compatibilité descendante. La valeur de GUID par défaut est dérivée du nom de la source, ce qui permet aux outils d’accepter le nom plus lisible par l’homme et de dériver le même GUID.
  6. Appelez IsEnabled() avant d’effectuer un travail gourmand en ressources lié au déclenchement d’un événement, comme le calcul d’un argument d’événement coûteux qui ne sera pas nécessaire si l’événement est désactivé.
  7. Essayez de maintenir la compatibilité de l’objet EventSource et choisissez la version appropriée. La version par défaut d’un événement est 0. La version peut être modifiée en définissant EventAttribute.Version. Modifiez la version d’un événement chaque fois que vous modifiez les données sérialisées avec celui-ci. Ajoutez toujours de nouvelles données sérialisées à la fin de la déclaration d’événement, c’est-à-dire à la fin de la liste des paramètres de méthode. Si cela n’est pas possible, créez un événement avec un nouvel ID pour remplacer l’ancien.
  8. Lorsque vous déclarez des méthodes d’événements, spécifiez les données de charge utile de taille fixe avant les données de taille variable.
  9. N’utilisez pas de chaînes contenant des caractères Null. Lors de la génération du manifeste pour ETW, EventSource déclare toutes les chaînes comme terminées par null, même s’il est possible d’avoir un caractère null dans une chaîne C#. Si une chaîne contient un caractère null, la chaîne entière est écrite dans la charge utile de l’événement, mais n’importe quel analyseur traite le premier caractère null comme la fin de la chaîne. S’il existe des arguments de charge utile après la chaîne, le reste de la chaîne est analysé au lieu de la valeur prévue.

Personnalisations d’événements classiques

Définition des niveaux de détail de l’événement

Chaque événement a un niveau de détail, et les abonnés aux événements activent souvent tous les événements sur un EventSource jusqu’à un certain niveau de détail. Les événements déclarent leur niveau de détail à l’aide de la propriété Level. Par exemple, dans l’EventSource ci-dessous, un abonné qui demande des événements de niveau Informational et inférieur ne journalisera pas l’événement DebugMessage détaillé.

[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 le niveau de détail d’un événement n’est pas spécifié dans EventAttribute, la valeur par défaut est Informational.

Bonne pratique

Utilisez des niveaux inférieurs à Informational pour les avertissements ou erreurs relativement rares. En cas de doute, respectez la valeur par défaut Informational et utilisez Verbose pour les événements qui se produisent plus de 1 000 fois/s.

Définition des mots clés d’événement

Certains systèmes de suivi d’événements prennent en charge les mots clés comme mécanisme de filtrage supplémentaire. Contrairement à la verbosité, qui catégorise les événements par niveau de détail, les mots clés sont destinés à catégoriser les événements en fonction d’autres critères, comme des domaines de fonctionnalités de code, ou l’utilité pour diagnostiquer certains problèmes. Les mots clés sont des indicateurs de bits nommés, et chaque événement peut être associé à n’importe quelle combinaison de mots clés. Par exemple, l’EventSource ci-dessous définit certains événements liés au traitement des requêtes et d’autres événements liés au démarrage. Si un développeur souhaite analyser les performances du démarrage, il peut uniquement activer la journalisation des événements marqués avec le mot clé startup.

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

Les mots clés doivent être définis à l’aide d’une classe imbriquée appelée Keywords, et chaque mot clé individuel est défini par un membre typé public const EventKeywords.

Bonne pratique

Les mots clés sont plus importants lors de la distinction entre les événements à volume élevé. Cela permet à un consommateur d’événements d’élever le niveau de détail à un niveau élevé, tout en gérant la surcharge de performances et la taille du journal en activant uniquement des sous-ensembles étroits d’événements. Les événements déclenchés plus de 1 000 fois/s sont de bons candidats pour un mot clé unique.

Types de paramètres pris en charge

EventSource nécessite que tous les paramètres d’événement puissent être sérialisés ; il accepte donc uniquement un ensemble limité de types. Ces règles sont les suivantes :

  • Primitives : bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr et UIntPtr, Guid decimal, string, DateTime, DateTimeOffset, TimeSpan
  • Énumérations
  • Structures attribuées avec System.Diagnostics.Tracing.EventDataAttribute. Seules les propriétés d’instance publique avec des types sérialisables sont sérialisées.
  • Types anonymes où toutes les propriétés publiques sont des types sérialisables
  • Tableaux de types sérialisables
  • Nullable<T>, où T est un type sérialisable
  • KeyValuePair<T, U>, où T et U sont tous deux des types sérialisables
  • Types qui implémentent IEnumerable<T> pour exactement un type T, et où T est un type sérialisable

Résolution des problèmes

La classe EventSource a été conçue pour ne jamais lever d’exception par défaut. Il s’agit d’une propriété utile, car la journalisation est souvent traitée comme facultative, et vous ne souhaitez généralement pas qu’une erreur d’écriture d’un message de journal provoque l’échec de votre application. Toutefois, cela rend difficile la recherche d’une erreur dans votre EventSource. Voici plusieurs techniques qui peuvent vous aider à résoudre les problèmes :

  1. Le constructeur EventSource a des surcharges qui prennent EventSourceSettings. Essayez d’activer temporairement l’indicateur ThrowOnEventWriteErrors.
  2. La propriété EventSource.ConstructionException stocke toute exception générée lors de la validation des méthodes de journalisation des événements. Cela peut révéler diverses erreurs de création.
  3. EventSource enregistre les erreurs à l’aide de l’ID d’événement 0, et cet événement d’erreur a une chaîne décrivant l’erreur.
  4. Lors du débogage, cette même chaîne d’erreur est également enregistrée à l’aide de Debug.WriteLine() et s’affiche dans la fenêtre de sortie de débogage.
  5. EventSource lève en interne, puis intercepte les exceptions lorsque des erreurs se produisent. Pour observer quand ces exceptions se produisent, activez les exceptions de première chance dans un débogueur, ou utilisez le suivi d’événements avec les événements d’exception du runtime .NET activés.

Personnalisations avancées

Définition des opCodes et des tâches

ETW a des concepts de tâches et opCodes, qui sont d’autres mécanismes pour l’étiquetage et le filtrage des événements. Vous pouvez associer des événements à des tâches et des OpCodes spécifiques à l’aide des propriétés Task et Opcode. Voici un exemple :

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

Vous pouvez créer implicitement des objets EventTask en déclarant deux méthodes d’événement avec les ID d’événement suivants qui ont le modèle d’affectation de noms <EventName>Start et <EventName>Stop. Ces événements doivent être déclarés l’un à côté de l’autre dans la définition de classe, et la méthode <EventName>Start doit passer en premier.

Formats d’événements autodescriptifs (journalisation de trace) et de manifeste

Ce concept n’est important que lors de l’abonnement à EventSource à partir d’ETW. ETW peut enregistrer les événements de deux manières différentes : le format de manifeste et le format autodescriptif (parfois appelé journalisation de trace). Les objets EventSource basés sur un manifeste génèrent et consignent un document XML représentant les événements définis sur la classe lors de l’initialisation. Cela nécessite qu’EventSource se reflète sur lui-même pour générer les métadonnées du fournisseur et de l’événement. Dans le format autodescriptif, les métadonnées pour chaque événement sont transmises inline avec les données d’événement plutôt qu’en amont. L’approche autodescriptive prend en charge les méthodes Write plus flexibles qui peuvent envoyer des événements arbitraires sans avoir créé une méthode de journalisation des événements prédéfinie. Elle est également légèrement plus rapide au démarrage, car elle évite la réflexion. Toutefois, les métadonnées supplémentaires émises avec chaque événement ajoutent une petite surcharge de performances, ce qui peut ne pas être souhaitable lors de l’envoi d’un volume élevé d’événements.

Pour utiliser le format d’événement autodescriptif, construisez votre EventSource à l’aide du constructeur EventSource(String), du constructeur EventSource(String, EventSourceSettings) ou en définissant l’indicateur EtwSelfDescribingEventFormat sur EventSourceSettings.

Types EventSource implémentant des interfaces

Un type EventSource peut implémenter une interface afin de s’intégrer en toute transparence dans différents systèmes de journalisation avancés qui utilisent des interfaces pour définir une cible de journalisation commune. Voici un exemple d’utilisation possible :

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

Vous devez spécifier l’attribut EventAttribute sur les méthodes d’interface, sinon (pour des raisons de compatibilité) la méthode n’est pas traitée comme une méthode de journalisation. L’implémentation explicite de la méthode d’interface n’est pas autorisée afin d’éviter les collisions de noms.

Hiérarchies de classe EventSource

Dans la plupart des cas, vous serez en mesure d’écrire des types qui dérivent directement de la classe EventSource. Toutefois, il est parfois utile de définir des fonctionnalités qui seront partagées par plusieurs types EventSource dérivés, comme les surcharges WriteEvent personnalisées (voir Optimisation des performances pour les événements à volume élevé ci-dessous).

Les classes de base abstraites peuvent être utilisées tant qu’elles ne définissent pas de mots clés, de tâches, d’OpCodes, de canaux ou d’événements. Voici un exemple où la classe UtilBaseEventSource définit une surcharge WriteEvent optimisée nécessaire à plusieurs EventSources dérivées dans le même composant. L’un de ces types dérivés est illustré ci-dessous sous la forme 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
}

Optimisation des performances pour les événements à volume élevé

La classe EventSource a un certain nombre de surcharges pour WriteEvent, dont une pour le nombre variable d’arguments. Quand aucune des autres surcharges ne correspond, la méthode params est appelée. Malheureusement, la surcharge params est relativement coûteuse. En particulier, elle :

  1. Alloue un tableau pour contenir les arguments de variable.
  2. Convertit chaque paramètre en objet, ce qui entraîne des allocations pour les types valeur.
  3. Affecte ces objets au tableau.
  4. Appelle la fonction.
  5. Déduit le type de chaque élément de tableau pour déterminer comment le sérialiser.

C’est probablement 10 à 20 fois plus coûteux que l’utilisation de types spécialisés. Cela n’a pas beaucoup d’importance pour les cas à faible volume, mais pour les événements à volume élevé, cela peut être conséquent. Il existe deux cas importants pour garantir que la surcharge params n’est pas utilisée :

  1. Vérifiez que les types énumérés sont convertis en « int » afin qu’ils correspondent à l’une des surcharges rapides.
  2. Créez de nouvelles surcharges WriteEvent rapides pour les charges utiles à volume élevé.

Voici un exemple d’ajout d’une surcharge WriteEvent qui prend quatre arguments entiers

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