Instrumentcode zum Erstellen von EventSource-Ereignissen
Dieser Artikel bezieht sich auf: ✔️ .NET Core 3.1 und höhere Versionen ✔️ .NET Framework 4.5 und neuere Versionen
Im Einstiegsleitfaden wurde Ihnen gezeigt, wie Sie eine minimale EventSource erstellen und Ereignisse in einer Protokolldatei sammeln. Dieses Lernprogramm enthält ausführlichere Informationen zum Erstellen von Ereignissen mithilfe von System.Diagnostics.Tracing.EventSource.
Eine minimale EventSource-Instanz
[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);
}
Die grundlegende Struktur einer abgeleiteten EventSource ist immer identisch. Besonders:
- Die Klasse erbt von System.Diagnostics.Tracing.EventSource
- Für jeden unterschiedlichen Ereignistyp, den Sie generieren möchten, muss eine Methode definiert werden. Diese Methode sollte mit dem Namen des erstellten Ereignisses benannt werden. Wenn das Ereignis zusätzliche Daten enthält, sollten diese mithilfe von Argumenten übergeben werden. Diese Ereignisargumente müssen serialisiert werden, damit nur bestimmte Typen zulässig sind.
- Jede Methode hat einen Rumpf, der WriteEvent aufruft, wobei eine ID (ein numerischer Wert, der das Ereignis darstellt) und die Argumente der Ereignismethode übergeben werden. Die ID muss innerhalb der EventSource eindeutig sein. Die ID wird explizit mithilfe der System.Diagnostics.Tracing.EventAttribute zugewiesen.
- EventSources sollen Singletoninstanzen sein. Daher ist es praktisch, eine statische Variable zu definieren, die gemäß der Konvention als
Log
bezeichnet wird, die dieses Singleton darstellt.
Regeln zum Definieren von Ereignismethoden
- Jede in einer EventSource-Klasse definierte nicht virtuelle Methode mit dem Rückgabetyp „void“ ist standardmäßig eine Methode für die Ereignisprotokollierung.
- Virtuelle Methoden oder Methoden ohne den Rückgabetyp „void“ werden nur einbezogen, wenn sie mit dem System.Diagnostics.Tracing.EventAttribute markiert sind.
- Um eine qualifizierende Methode als nicht protokollierend zu kennzeichnen, müssen Sie sie mit „System.Diagnostics.Tracing.NonEventAttribute“ dekorieren.
- Ereignisprotokollierungsmethoden verfügen über Ereignis-IDs, die ihnen zugeordnet sind. Diese Zuordnung kann entweder explizit erfolgen, indem die Methode mit einem System.Diagnostics.Tracing.EventAttribute versehen wird, oder implizit durch die Ordnungszahl der Methode in der Klasse. Bei Verwendung der impliziten Nummerierung erhält die erste Methode in der Klasse die ID 1, die zweite Methode die ID 2, und so weiter.
- Methoden für die Ereignisprotokollierung müssen eine WriteEvent-, WriteEventCore-, WriteEventWithRelatedActivityId- oder WriteEventWithRelatedActivityIdCore-Überladung aufrufen.
- Die Ereignis-ID, sei sie implizit oder explizit, muss mit dem ersten Argument übereinstimmen, das an die aufgerufene WriteEvent*-API übergeben wird.
- Die Anzahl, Typen und Reihenfolge von Argumenten, die an die EventSource-Methode übergeben werden, müssen mit der Art und Weise übereinstimmen, wie sie an die WriteEvent*-APIs übergeben werden. Für WriteEvent folgen die Argumente der Ereignis-ID, für WriteEventWithRelatedActivityId folgen die Argumente der relatedActivityId. Für die WriteEvent*Core-Methoden müssen die Argumente manuell in den parameter
data
serialisiert werden. - Ereignisnamen dürfen keine
<
oder>
Zeichen enthalten. Auch wenn benutzerdefinierte Methoden diese Zeichen nicht enthalten können, werdenasync
Methoden vom Compiler umgeschrieben, um sie zu enthalten. Um sicherzustellen, dass diese generierten Methoden nicht zu Ereignissen werden, markieren Sie alle Nicht-Ereignismethoden für eine EventSource-Instanz mit NonEventAttribute.
Bewährte Methoden
- Typen, die von EventSource abgeleitet werden, weisen in der Regel keine Zwischentypen in der Hierarchie auf oder implementieren Schnittstellen. Unter erweiterten Anpassungen unten finden Sie einige Ausnahmen, bei denen dies hilfreich sein kann.
- Im Allgemeinen ist der Name der EventSource-Klasse ein schlechter öffentlicher Name für die EventSource. Öffentliche Namen, die Namen, die in Protokollierungskonfigurationen und Protokollanzeigen angezeigt werden, sollten global eindeutig sein. Daher empfiehlt es sich, Ihrer EventSource einen öffentlichen Namen mit dem System.Diagnostics.Tracing.EventSourceAttributezu geben. Der oben verwendete Name "Demo" ist kurz und unwahrscheinlich, dass er eindeutig ist, also nicht eine gute Wahl für den Produktionseinsatz. Eine gängige Konvention besteht darin, einen hierarchischen Namen mit
.
oder-
als Trennzeichen wie "MyCompany-Samples-Demo" oder den Namen der Assembly oder des Namespaces zu verwenden, für den die EventSource Ereignisse bereitstellt. Es wird nicht empfohlen, "EventSource" als Teil des öffentlichen Namens einzuschließen. - Weisen Sie Ereignis-IDs explizit zu, damit scheinbar gutartige Änderungen am Code in der Quellklasse wie das Neuanordnen oder Hinzufügen einer Methode in der Mitte die Ereignis-ID, die jeder Methode zugeordnet ist, nicht ändern.
- Beim Erstellen von Ereignissen, die den Anfang und das Ende einer Arbeitseinheit darstellen, werden diese Methoden standardmäßig mit Suffixen "Start" und "Stop" benannt. Beispiel: "RequestStart" und "RequestStop".
- Geben Sie keinen expliziten Wert für die Guid-Eigenschaft von EventSourceAttribute an, es sei denn, Sie benötigen ihn aus Gründen der Abwärtskompatibilität. Der Standard-GUID-Wert wird vom Namen der Quelle abgeleitet, wodurch Tools den besser lesbaren Namen akzeptieren und dieselbe GUID ableiten können.
- Rufen Sie IsEnabled() auf, bevor Sie ressourcenintensive Arbeit im Zusammenhang mit dem Auslösen eines Ereignisses ausführen, z. B. das Berechnen eines kostspieligen Ereignisarguments, das nicht benötigt wird, wenn das Ereignis deaktiviert ist.
- Versuchen Sie, das EventSource-Objekt wieder kompatibel zu halten und diese entsprechend zu versionieren. Die Standardversion für ein Ereignis ist 0. Die Version kann durch Festlegen EventAttribute.Versiongeändert werden. Ändern Sie die Version eines Ereignisses, wenn Sie die daten ändern, die mit ihr serialisiert werden. Fügen Sie immer am Ende der Ereignisdeklaration neue serialisierte Daten hinzu, d. h. am Ende der Liste der Methodenparameter. Wenn dies nicht möglich ist, erstellen Sie ein neues Ereignis mit einer neuen ID, um die alte zu ersetzen.
- Geben Sie beim Deklarieren von Ereignismethoden Nutzdaten mit fester Größe vor Daten mit variabler Größe an.
- Verwenden Sie keine Zeichenfolgen, die NULL-Zeichen enthalten. Beim Generieren des Manifests für ETW EventSource werden alle Zeichenfolgen als NULL beendet deklariert, obwohl es möglich ist, in einer C#-Zeichenfolge ein Nullzeichen zu haben. Wenn eine Zeichenfolge ein NULL-Zeichen enthält, wird die gesamte Zeichenfolge in die Ereignisnutzlast geschrieben, aber jeder Parser behandelt das erste Nullzeichen als Ende der Zeichenfolge. Wenn nach der Zeichenfolge Nutzlastargumente vorhanden sind, wird der rest der Zeichenfolge anstelle des vorgesehenen Werts analysiert.
Typische Ereignisanpassungen
Festlegen des Ausführlichkeitsgrads von Ereignissen
Jedes Ereignis hat eine Ausführlichkeitsstufe, und Ereignisabonnenten aktivieren häufig alle Ereignisse auf einer EventSource bis zu einer bestimmten Ausführlichkeitsstufe. Ereignisse deklarieren ihren Ausführlichkeitsgrad mit der Eigenschaft Level. Beispielsweise wird für Abonnent*innen, die im unten aufgeführte EventSource-Instanz Ereignisse der Stufe „Informational“ oder niedriger anfordern, das DebugMessage-Ereignis vom Typ „Verbose“ nicht protokolliert.
[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);
}
Wenn die Ausführlichkeitsebene eines Ereignisses nicht im EventAttribute angegeben ist, wird standardmäßig "Informational" verwendet.
Beste Praxis
Verwenden Sie Ebenen kleiner als "Informational" für relativ seltene Warnungen oder Fehler. Behalten Sie im Zweifelsfall die Standardeinstellung „Informational“ bei, und verwenden Sie für Ereignisse mit einer Häufigkeit von mehr als 1.000 Ereignissen pro Sekunde die Einstellung „Verbose“.
Festlegen von Ereignisstichwörtern
Einige Ereignisablaufverfolgungssysteme unterstützen Schlüsselwörter als zusätzlichen Filtermechanismus. Im Gegensatz zur Ausführlichkeit, die Ereignisse nach Detailebene kategorisiert, sollen Schlüsselwörter Ereignisse basierend auf anderen Kriterien wie Codefunktionen kategorisieren oder für die Diagnose bestimmter Probleme nützlich sein. Schlüsselwörter sind benannte Bitkennzeichnungen, und auf jedes Ereignis kann eine beliebige Kombination von Schlüsselwörtern angewendet werden. Die nachstehende EventSource definiert beispielsweise einige Ereignisse, die sich auf die Anforderungsverarbeitung und andere Ereignisse beziehen, die sich auf den Start beziehen. Wenn ein Entwickler die Leistung des Startvorgangs analysieren wollte, aktiviert er möglicherweise nur die Protokollierung der ereignisse, die mit dem Startschlüsselwort gekennzeichnet sind.
[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;
}
}
Schlüsselwörter müssen mithilfe einer geschachtelten Klasse definiert werden, die Keywords
genannt wird, und jedes einzelne Schlüsselwort wird durch ein Mitglied des Typs public const EventKeywords
definiert.
Beste Praxis
Schlüsselwörter sind wichtiger, wenn zwischen Ereignissen mit hohem Volumen unterschieden wird. Dadurch kann ein Ereignisanwender die Ausführlichkeit auf ein hohes Niveau erhöhen, aber den Leistungsaufwand und die Protokollgröße verwalten, indem nur schmale Teilmengen der Ereignisse aktiviert werden. Ereignisse, die häufiger als 1.000 Mal pro Sekunde ausgelöst werden, sind gute Kandidaten für ein einzigartiges Schlüsselwort.
Unterstützte Parametertypen
EventSource erfordert, dass alle Ereignisparameter serialisiert werden können, sodass nur eine begrenzte Gruppe von Typen akzeptiert wird. Dies sind:
- Primitive: bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr, and UIntPtr, Guid decimal, string, DateTime, DateTimeOffset, TimeSpan
- Enumerationen
- Strukturen, die mit System.Diagnostics.Tracing.EventDataAttribute versehen sind. Nur die Eigenschaften der öffentlichen Instanz mit serialisierbaren Typen werden serialisiert.
- Anonyme Typen, bei denen alle öffentlichen Eigenschaften serialisierbar sind
- Arrays mit serialisierbaren Typen
- Nullable<T->, wobei T ein serialisierbarer Typ ist
- KeyValuePair<T, U>, wobei T und U beide serialisierbare Typen sind
- Typen, die IEnumerable<T> für genau einen Typ T implementieren und dabei T ein serialisierbarer Typ ist
Fehlerbehebung
Die EventSource-Klasse wurde so konzipiert, dass sie niemals standardmäßig eine Ausnahme auslösen würde. Dies ist eine nützliche Eigenschaft, da die Protokollierung häufig als optional behandelt wird, und Sie möchten in der Regel keinen Fehler beim Schreiben einer Protokollmeldung, damit Ihre Anwendung fehlschlägt. Dies macht jedoch das Auffinden eines Fehlers in Ihrer EventSource schwierig. Im Folgenden finden Sie verschiedene Techniken, die Ihnen bei der Problembehandlung helfen können:
- Der EventSource-Konstruktor umfasst Überladungen, die EventSourceSettings akzeptieren. Versuchen Sie, das ThrowOnEventWriteErrors-Flag vorübergehend zu aktivieren.
- Die EventSource.ConstructionException-Eigenschaft speichert jede Ausnahme, die beim Überprüfen der Ereignisprotokollierungsmethoden generiert wurde. Dies kann verschiedene Erstellungsfehler erkennen.
- EventSource protokolliert Fehler mithilfe der Ereignis-ID 0, und dieses Fehlerereignis weist eine Zeichenfolge auf, die den Fehler beschreibt.
- Beim Debuggen wird dieselbe Fehlerzeichenfolge auch mit Debug.WriteLine() protokolliert und im Debugausgabefenster angezeigt.
- EventSource löst intern Ausnahmen aus und fängt sie dann ab, wenn Fehler auftreten. Um zu beobachten, wann diese Ausnahmen auftreten, aktivieren Sie FirstChanceException-Ereignisse im Debugger, oder verwenden Sie die Ereignisablaufverfolgung mit aktivierten Exception-Ereignissen der .NET-Runtime.
Erweiterte Anpassungen
Festlegen von OpCodes und Aufgaben
ETW verfügt über Konzepte für Tasks und OpCodes, die weitere Mechanismen zur Tagging und Filterung von Ereignissen sind. Mithilfe der eigenschaften Task und Opcode können Sie Ereignisse bestimmten Aufgaben und Opcodes zuordnen. Hier ist ein Beispiel:
[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;
}
}
Sie können EventTask-Objekte implizit erstellen, indem Sie zwei Ereignismethoden mit nachfolgenden Ereignis-IDs deklarieren, die das Benennungsmuster <EventName>Start und <EventName>Stop aufweisen. Diese Ereignisse müssen in der Klassendefinition nebeneinander deklariert werden, und die <EventName->Startmethode muss zuerst erfolgen.
Selbstbeschreibung (Tracelogging) im Vergleich zu Manifestereignisformaten
Dieses Konzept ist nur dann wichtig, wenn Sie EventSource von ETW abonnieren. ETW verfügt über zwei verschiedene Möglichkeiten, Ereignisse, Manifestformat und selbst beschreibendes (manchmal als Tracelogging bezeichnetes) Format zu protokollieren. Manifestbasierte EventSource-Objekte generieren und protokollieren ein XML-Dokument, das die für die Klasse bei der Initialisierung definierten Ereignisse darstellt. Dies erfordert, dass EventSource über sich selbst reflektiert, um die Anbieter- und Ereignismetadaten zu generieren. In dem selbstbeschreibenden Format werden die Metadaten für jedes Ereignis zusammen mit den Ereignisdaten und nicht im Vorfeld übertragen. Der selbst beschreibende Ansatz unterstützt die flexibleren Write Methoden, die beliebige Ereignisse senden können, ohne eine vordefinierte Ereignisprotokollierungsmethode erstellt zu haben. Dieser Ansatz ist beim Start zudem etwas schneller, da eine sofortige Reflexion vermieden wird. Die zusätzlichen Metadaten, die mit jedem Ereignis ausgegeben werden, fügen jedoch einen geringen Leistungsaufwand hinzu, der beim Senden einer hohen Anzahl von Ereignissen möglicherweise nicht wünschenswert ist.
Um das selbst beschreibende Ereignisformat zu verwenden, erstellen Sie Ihre EventSource mithilfe des EventSource(String)-Konstruktors, des EventSource(String, EventSourceSettings)-Konstruktors oder durch Festlegen des EtwSelfDescribingEventFormat-Flags für EventSourceSettings.
EventSource-Typen, die Schnittstellen implementieren
Ein EventSource-Typ kann eine Schnittstelle implementieren, um nahtlos in verschiedene erweiterte Protokollierungssysteme zu integrieren, die Schnittstellen zum Definieren eines gemeinsamen Protokollierungsziels verwenden. Hier ist ein Beispiel für eine mögliche Verwendung:
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); }
}
Sie müssen "EventAttribute" für die Schnittstellenmethoden angeben, andernfalls (aus Kompatibilitätsgründen) wird die Methode nicht als Protokollierungsmethode behandelt. Die explizite Implementierung der Schnittstellenmethode ist unzulässig, um Namenskonflikte zu verhindern.
EventSource-Klassenhierarchien
In den meisten Fällen können Sie Typen schreiben, die direkt von der EventSource-Klasse abgeleitet sind. Manchmal ist es jedoch hilfreich, Funktionen zu definieren, die von mehreren abgeleiteten EventSource-Typen freigegeben werden, z. B. angepasste WriteEvent-Überladungen (siehe Optimieren der Leistung für Ereignisse mit hohem Volumen unten).
Abstrakte Basisklassen können verwendet werden, solange sie keine Schlüsselwörter, Aufgaben, Opcodes, Kanäle oder Ereignisse definieren. Hier ist ein Beispiel, in dem die UtilBaseEventSource-Klasse eine optimierte WriteEvent-Überladung definiert, die von mehreren abgeleiteten EventSources in derselben Komponente benötigt wird. Einer dieser abgeleiteten Typen wird unten als OptimizedEventSource veranschaulicht.
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
}
Optimieren der Leistung für Ereignisse mit hohem Volumen
Die EventSource-Klasse weist eine Reihe von Überladungen für WriteEvent auf, einschließlich einer für die variable Anzahl von Argumenten. Wenn keine der anderen Überladungen passt, wird die params-Methode aufgerufen. Leider ist die Parameterüberladung ziemlich teuer. Im einzelnen bewirkt sie Folgendes:
- Weist ein Array zu, das die Variablenargumente enthält.
- Wandelt jeden Parameter in ein Objekt um, wodurch Zuordnungen für Werttypen verursacht werden.
- Weist diese Objekte dem Array zu.
- Ruft die Funktion auf.
- Ermittelt den Typ jedes Arrayelements, um zu bestimmen, wie es serialisiert werden soll.
Dies ist wahrscheinlich 10 bis 20 Mal so teuer wie spezialisierte Typen. Dies spielt keine Rolle für Fälle mit geringem Volumen, aber für Ereignisse mit hohem Volumen kann es wichtig sein. Es gibt zwei wichtige Fälle, in denen sichergestellt werden muss, dass die params-Überladung nicht verwendet wird:
- Stellen Sie sicher, dass Enumerationstypen in „int“ umgewandelt werden, damit sie einer der schnellen Überladungen entsprechen.
- Erstellen Sie neue leistungsstarke WriteEvent-Überladungen für große Volumen-Payloads.
Hier ist ein Beispiel für das Hinzufügen einer WriteEvent-Überladung, die vier ganzzahlige Argumente akzeptiert.
[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);
}