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 explique 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 de sorte que seuls certains types sont autorisés.
- Chaque méthode a un corps qui appelle WriteEvent en lui transmettant 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 de l'System.Diagnostics.Tracing.EventAttribute
- EventSources est destiné à être des instances singleton. Par conséquent, il est 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 évènementielles
- Toute instance, non virtuelle, méthode de retour void définie dans une classe EventSource est par défaut une méthode de journalisation des événements.
- Les méthodes de retour virtuel ou non void sont uniquement incluses si elles sont marquées avec le System.Diagnostics.Tracing.EventAttribute
- Pour marquer une méthode éligible comme sans journalisation, vous devez la décorer avec le System.Diagnostics.Tracing.NonEventAttribute
- Les méthodes de journalisation des événements ont des ID d’événement associés. Cette opération peut être effectuée explicitement en décorant la méthode avec une System.Diagnostics.Tracing.EventAttribute ou implicitement par le nombre ordinal de la méthode dans la classe. Par exemple, avec 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.
- Les méthodes de journalisation des événements doivent appeler une surcharge WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId ou WriteEventWithRelatedActivityIdCore.
- L’ID d’événement, implicite ou explicite, doit correspondre au premier argument passé à l’API WriteEvent* qu’il appelle.
- 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 passés aux API WriteEvent*. Pour WriteEvent, les arguments suivent l’ID d’événement, pour WriteEventWithRelatedActivityId, les arguments suivent relatedActivityId. Pour les méthodes WriteEvent*Core, les arguments doivent être sérialisés manuellement dans le paramètre
data
. - 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,async
méthodes 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 non-événement sur un EventSource avec le NonEventAttribute.
Meilleures pratiques
- Les types dérivés 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 certaines exceptions où cela peut être utile.
- En règle générale, le nom de la classe EventSource est un nom public incorrect pour EventSource. Les noms publics, les noms qui s’affichent dans les configurations de journalisation et les visionneuses de journaux doivent être globalement uniques. Par conséquent, il est recommandé de donner à votre EventSource un nom public à l’aide de la System.Diagnostics.Tracing.EventSourceAttribute. Le nom « Demo » utilisé ci-dessus est court et peu probable être unique, donc pas un bon choix pour une utilisation en production. Une convention courante consiste à utiliser un nom hiérarchique avec
.
ou-
comme séparateur, tel que « 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 cadre du nom public. - Affectez explicitement des ID d’événement, de cette façon, les modifications apparemment bénignes apportées au code dans la classe source, telles que la réorganisation ou l’ajout d’une méthode au milieu, ne modifient pas l’ID d’événement associé à chaque méthode.
- 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 ».
- 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 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.
- Appelez IsEnabled() avant d’effectuer un travail gourmand en ressources liés au déclenchement d’un événement, par exemple en calculant un argument d’événement coûteux qui ne sera pas nécessaire si l’événement est désactivé.
- Essayez de rendre l’objet EventSource compatible et de les versionr correctement. 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 celle-ci. Ajoutez toujours de nouvelles données sérialisées à la fin de la déclaration d’événement, autrement dit, à la fin de la liste des paramètres de méthode. Si ce n’est pas possible, créez un événement avec un nouvel ID pour remplacer l’ancien.
- Lors de la déclaration de méthodes d’événements, spécifiez les données à charge utile fixes avant les données variables.
- N’utilisez pas de chaînes contenant des caractères Null. Lors de la génération du manifeste pour ETW EventSource, toutes les chaînes sont déclarées comme étant 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 tout 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 cette dernière est analysé au lieu de la valeur prévue.
Personnalisations d’événements classiques
Définition des niveaux de détail d’un événement
Chaque événement a un niveau de détail et les abonnés aux événements activent souvent tous les événements d’un EventSource jusqu’à un certain niveau de détail. Les événements déclarent leur niveau de verbosité à l'aide de la propriété Level. Par exemple, dans cet EventSource ci-dessous, un abonné qui demande des événements de niveau Informational et inférieur ne journalisera pas l’événement 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 le niveau de détail d’un événement n’est pas spécifié dans EventAttribute, il est défini par défaut sur Informational.
Bonne pratique
Utilisez des niveaux inférieurs à Informational pour des avertissements ou des erreurs relativement rares. En cas de doute, respectez la valeur par défaut d’Informational et utilisez Verbose pour les événements qui se produisent plus fréquemment que 1 000 événements/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 aux verbes qui catégorisent 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 tels que des zones de fonctionnalité de code ou qui seraient utiles pour diagnostiquer certains problèmes. Les mots clés sont des indicateurs de bits nommés et chaque événement peut avoir n’importe quelle combinaison de mots clés qui lui sont appliqués. Par exemple, EventSource ci-dessous définit certains événements liés au traitement des demandes et à d’autres événements liés au démarrage. Si un développeur souhaitait analyser les performances du démarrage, il peut uniquement activer la journalisation des événements marqués avec le mot clé de démarrage.
[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’augmenter le niveau de détail mais de gérer les performances et la taille du journal en n’activant que des sous-ensembles restreints d’événements. Les événements déclenchés plus de 1 000/s sont de bons candidats pour un mot-clé unique.
Types de paramètres pris en charge
EventSource exige que tous les paramètres d’événement puissent être sérialisés afin qu’il accepte uniquement un ensemble limité de types. Voici les éléments suivants :
- Primitives : bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr, and 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
Dépannage
La classe EventSource a été conçue afin qu’elle ne lève jamais 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 écrive un message de journal pour provoquer 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 suivants :
- Le constructeur EventSource a des surcharges qui prennent EventSourceSettings. Essayez d’activer temporairement l’indicateur ThrowOnEventWriteErrors.
- La propriété EventSource.ConstructionException stocke toutes les exceptions générées lors de la validation des méthodes de journalisation des événements. Cela peut révéler différentes erreurs de création.
- EventSource journalise 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.
- Lors du débogage, cette même chaîne d’erreur est également journalisée à l’aide de Debug.WriteLine() et s’affiche dans la fenêtre de sortie de débogage.
- EventSource soulève en interne, puis intercepte des 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 des événements avec les événements d’exception du runtime .NET activés.
Personnalisations avancées
Définition des opCodes et des tâches
L’ETW a des concepts de Tâches et OpCodes, qui sont des mécanismes supplémentaires pour le balisage 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 des ID d’événement successifs qui suivent le modèle de nommage <EventName>Début et <EventName>Fin. Ces événements doivent être déclarés à la suite l'un de l'autre dans la définition de la classe et la méthode <EventName>Start doit venir en premier.
Auto-description (tracelogging) et formats d’événement manifeste
Ce concept est uniquement important lors de l’abonnement à EventSource à partir d’ETW. ETW a deux façons différentes de consigner les événements, le format manifeste et le format auto-décrivant (parfois appelé tracelogging). 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 que EventSource se reflète sur lui-même pour générer le fournisseur et les métadonnées d’événement. Les métadonnées au format auto-décrivant pour chaque événement sont transmises en ligne avec les données de l'événement plutôt que de manière anticipée. L’approche auto-décrivante prend en charge les méthodes Write plus flexibles qui peuvent envoyer des événements arbitraires sans avoir créé de méthode de journalisation d’événements prédéfinie. Il est également légèrement plus rapide au démarrage, car il évite de réfléchir avec impatience. 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 auto-décrivant, 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 d’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’EventAttribute sur les méthodes d’interface, sinon (pour des raisons de compatibilité), la méthode ne sera pas traitée comme une méthode de journalisation. L’implémentation de méthode d’interface explicite n’est pas autorisée afin d’empêcher les collisions d’affectation de noms.
Hiérarchies de classes EventSource
Dans la plupart des cas, vous pourrez é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, tels que les surcharges WriteEvent personnalisées (voir optimiser les performances pour les événements en 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 par plusieurs EventSources dérivés 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 de volume élevé
La classe EventSource a plusieurs surcharges pour WriteEvent, y compris un pour le nombre variable d’arguments. Quand aucune des autres surcharges ne correspond, la méthode params est appelée. Malheureusement, la surcharge des paramètres est relativement coûteuse. En particulier, il :
- Alloue un tableau pour contenir les arguments variables.
- Convertit chaque paramètre en objet, ce qui entraîne des allocations pour les types de valeurs.
- Attribue ces objets au tableau.
- Appelle la fonction.
- Détermine le type de chaque élément de tableau pour déterminer comment le sérialiser.
C’est probablement 10 à 20 fois plus cher que les types spécialisés. Cela n’a pas beaucoup d’importance pour les cas de volume faible, mais pour les événements de volume élevé, il peut être important. Il y a deux cas importants pour assurer que la surcharge de paramètres n’est pas utilisée :
- Assurez-vous que les types énumérés soient convertis en « int », afin qu’ils correspondent à l’une des surcharges rapides.
- Créez de nouvelles surcharges WriteEvent rapides pour les charges utiles de 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);
}