Teilen über


Instrumentcode zum Erstellen von EventSource-Ereignissen

Dieser Artikel gilt für: ✔️ .NET Core 3.1 und höhere Versionen ✔️ .NET Framework 4.5 und höhere Versionen

Im Leitfaden für erste Schritte haben Sie erfahren, wie Sie eine minimale EventSource-Instanz erstellen und Ereignisse in einer Ablaufverfolgungsdatei erfassen. In diesem Tutorial erfahren Sie mehr über die Erstellung von Ereignissen mit 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-Instanz ist immer die gleiche. Dies gilt insbesondere für:

  • Die Klasse erbt von System.Diagnostics.Tracing.EventSource.
  • Für jede Art von Ereignis, das Sie generieren möchten, muss eine Methode definiert werden. Diese Methode sollte nach dem zu erstellenden Ereignis 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 umfasst einen Textkörper, der „WriteEvent“ aufruft und eine ID (einen numerischen Wert, der das Ereignis repräsentiert) und die Argumente der Ereignismethode übergibt. Die ID muss innerhalb der EventSource-Instanz eindeutig sein. Die ID wird explizit mithilfe von System.Diagnostics.Tracing.EventAttribute zugewiesen.
  • EventSource-Instanzen sind als Singletoninstanzen vorgesehen. Daher ist es zweckmäßig, eine statische Variable mit dem Namen Log zu definieren, die dieses Singleton repräsentiert.

Regeln zum Definieren von Ereignismethoden

  1. Jede in einer EventSource-Klasse definierte nicht virtuelle Methode mit dem Rückgabetyp „void“ ist standardmäßig eine Methode für die Ereignisprotokollierung.
  2. Virtuelle Methoden oder Methoden ohne den Rückgabetyp „void“ werden nur einbezogen, wenn sie mit dem System.Diagnostics.Tracing.EventAttribute markiert sind.
  3. Um eine berechtigte Methode als nicht protokollierend zu kennzeichnen, müssen Sie sie mit dem System.Diagnostics.Tracing.NonEventAttribute versehen.
  4. Methoden für die Ereignisprotokollierung wird eine Ereignis-ID zugeordnet. 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. Wenn Sie beispielsweise eine implizite Nummerierung verwenden, erhält die erste Methode in der Klasse die ID 1, die zweite die ID 2 usw.
  5. Methoden für die Ereignisprotokollierung müssen eine WriteEvent-, WriteEventCore-, WriteEventWithRelatedActivityId- oder WriteEventWithRelatedActivityIdCore-Überladung aufrufen.
  6. Die Ereignis-ID, ob implizit oder explizit, muss mit dem ersten Argument übereinstimmen, das an die aufgerufene WriteEvent*-API übergeben wird.
  7. Die Anzahl, die Typen und die Reihenfolge der Argumente, die an die EventSource-Methode übergeben werden, müssen mit den Werten übereinstimmen, die an die WriteEvent*-APIs übergeben werden. Bei „WriteEvent“ folgen die Argumente auf die Ereignis-ID, bei „WriteEventWithRelatedActivityId“ folgen die Argumente auf die „relatedActivityId“. Bei den WriteEvent*Core-Methoden müssen die Argumente manuell im Parameter data serialisiert werden.
  8. Ereignisnamen dürfen keine <- oder >-Zeichen enthalten. Auch in benutzerdefinierten Methoden können diese Zeichen nicht enthalten sein, aber async-Methoden werden vom Compiler so umgeschrieben, dass sie sie 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

  1. Typen, die von EventSource abgeleitet sind, weisen normalerweise keine Zwischentypen in der Hierarchie auf und implementieren keine Schnittstellen. Im Abschnitt Erweiterte Anpassungen weiter unten finden Sie einige Ausnahmen, bei denen dies nützlich sein kann.
  2. Im Allgemeinen ist der Name der EventSource-Klasse ein ungünstiger öffentlicher Name für die EventSource-Instanz. Öffentliche Namen, d. h. die in Protokollkonfigurationen und in der Protokollanzeige erscheinenden Namen, sollten weltweit eindeutig sein. Daher ist es eine bewährte Methode, der EventSource-Instanz mithilfe von System.Diagnostics.Tracing.EventSourceAttribute einen öffentlichen Namen zu geben. Der oben verwendete Name „Demo“ ist kurz und wahrscheinlich nicht eindeutig, weshalb er für die Produktion keine gute Wahl darstellt. Eine gängige Konvention ist die Verwendung eines hierarchischen Namens mit . oder - als Trennzeichen, wie z. B. „MyCompany-Samples-Demo“, oder der Name der Assembly oder des Namespaces, für den die EventSource-Instanz Ereignisse bereitstellt. Es wird nicht empfohlen, „EventSource“ als Teil des öffentlichen Namens zu verwenden.
  3. Weisen Sie Ereignis-IDs explizit zu. Auf diese Weise führen vermeintlich harmlose Codeänderungen in der Ausgangsklasse, wie z. B. das Neuanordnen des Codes oder das Hinzufügen einer Methode im Mittelteil, nicht zu einer Änderung der mit jeder Methode verbundenen Ereignis-ID.
  4. Wenn Sie Ereignisse erstellen, die den Beginn und das Ende einer Arbeitseinheit darstellen, werden diese Methoden üblicherweise mit den Suffixen „Start“ und „Stop“ benannt. Beispiel: „RequestStart“ und „RequestStop“.
  5. Geben Sie keinen expliziten Wert für die Guid-Eigenschaft von „EventSourceAttribute“ an, es sei denn, dies ist aus Gründen der Abwärtskompatibilität erforderlich. Der Standardwert für die GUID wird aus dem Namen der Quelle abgeleitet, damit Tools den besser lesbaren Namen akzeptieren und die gleiche GUID ableiten können.
  6. Rufen Sie IsEnabled() auf, bevor Sie einen ressourcenintensiven Vorgang im Zusammenhang mit dem Auslösen eines Ereignisses durchführen, z. B. die Berechnung eines kostenintensiven Ereignisarguments, das bei deaktiviertem Ereignis nicht benötigt wird.
  7. Gestalten Sie EventSource-Objekte nach Möglichkeit abwärtskompatibel, und versionieren Sie sie entsprechend. Die Standardversion für ein Ereignis lautet 0. Die Version kann durch Festlegung von EventAttribute.Version geändert werden. Ändern Sie die Version eines Ereignisses immer dann, wenn Sie die Daten ändern, die mit ihm serialisiert werden. Fügen Sie neue serialisierte Daten immer am Ende der Ereignisdeklaration 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 das alte zu ersetzen.
  8. Geben Sie beim Deklarieren von Ereignismethoden Nutzdaten mit fester Größe vor Daten mit variabler Größe an.
  9. Verwenden Sie keine Zeichenfolgen, die NULL-Zeichen enthalten. Beim Generieren des Manifests für ETW EventSource werden alle Zeichenfolgen als NULL-terminiert deklariert, selbst wenn ein NULL-Zeichen in einer C#-Zeichenfolge vorkommen kann. Wenn eine Zeichenfolge ein NULL-Zeichen enthält, wird die gesamte Zeichenfolge in die Ereignisnutzdaten geschrieben, aber jeder Parser wird das erste NULL-Zeichen als Ende der Zeichenfolge betrachten. Wenn nach der Zeichenfolge Nutzdatenargumente vorhanden sind, wird anstelle des beabsichtigten Werts der restliche Teil der Zeichenfolge geparst.

Typische Ereignisanpassungen

Festlegen des Ausführlichkeitsgrads von Ereignissen

Jedes Ereignis weist einen Ausführlichkeitsgrad auf, und Ereignisabonnenten aktivieren häufig alle Ereignisse für eine EventSource-Instanz bis zu einem bestimmten Ausführlichkeitsgrad. 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 der Ausführlichkeitsgrad eines Ereignisses nicht in „EventAttribute“ angegeben ist, wird standardmäßig der Wert „Informational“ verwendet.

Empfehlung

Verwenden Sie für relativ seltene Warnungen oder Fehler einen niedrigeren Ausführlichkeitsgrad als „Informational“. 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 Ereignisschlüsselwörtern

Einige Systeme für die Ereignisablaufverfolgung unterstützen Schlüsselwörter als zusätzlichen Filtermechanismus. Im Gegensatz zur Ausführlichkeit, mit der Ereignisse nach Detailebene kategorisiert werden, sind Schlüsselwörter dazu gedacht, Ereignisse nach anderen Kriterien zu kategorisieren – z. B. nach Bereichen der Codefunktionalität oder nach ihrer Nützlichkeit für die Diagnose bestimmter Probleme. Schlüsselwörter werden als Bitflags bezeichnet, und auf jedes Ereignis kann eine beliebige Kombination von Schlüsselwörtern angewendet werden. Die folgende EventSource-Instanz definiert zum Beispiel einige Ereignisse, die sich auf die Anforderungsverarbeitung beziehen, sowie andere Ereignisse im Zusammenhang mit dem Start. Wenn Entwickler*innen die Leistung des Startvorgangs analysieren möchten, könnten sie mit dem Schlüsselwort „startup“ nur die Protokollierung von Ereignissen aktivieren, die mit diesem Schlü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 namens Keywords definiert werden, und jedes einzelne Schlüsselwort wird durch einen Member mit der Bezeichnung public const EventKeywords definiert.

Empfehlung

Schlüsselwörter sind besonders wichtig, wenn zwischen Ereignissen mit hohem Aufkommen unterschieden werden soll. So können Ereignisconsumer einen hohen Ausführlichkeitsgrad festlegen, aber gleichzeitig den Leistungsmehraufwand und die Protokollgröße kontrollieren, indem nur kleine Teilmengen der Ereignisse aktiviert werden. Ereignisse mit einer Auslösungshäufigkeit von mehr als 1.000/Sek. sind gute Kandidaten für ein eindeutiges Schlüsselwort.

Unterstützte Parametertypen

EventSource erfordert, dass alle Ereignisparameter serialisiert werden können, daher wird nur eine begrenzte Anzahl von Typen akzeptiert. Diese lauten wie folgt:

  • 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. Es werden nur die öffentlichen Instanzeigenschaften mit serialisierbaren Typen serialisiert.
  • Anonyme Typen, bei denen alle öffentlichen Eigenschaften serialisierbare Typen 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 wobei T ein serialisierbarer Typ ist

Problembehandlung

Die EventSource-Klasse ist so konzipiert, dass sie standardmäßig nie eine Ausnahme auslöst. Dies ist eine nützliche Eigenschaft, da die Protokollierung oft als optional betrachtet wird und Sie im Normalfall nicht möchten, dass ein Fehler beim Schreiben einer Protokollnachricht einen Anwendungsfehler verursacht. Das macht es jedoch schwierig, Fehler in Ihrer EventSource-Instanz zu finden. Nachfolgend finden Sie einige Techniken, die Sie bei der Fehlersuche unterstützen können:

  1. Der EventSource-Konstruktor umfasst Überladungen, die EventSourceSettings akzeptieren. Aktivieren Sie vorübergehend das Flag „ThrowOnEventWriteErrors“.
  2. Die Eigenschaft EventSource.ConstructionException speichert alle Ausnahmen, die bei der Validierung der Ereignisprotokollierungsmethoden generiert werden. Dadurch können verschiedene Erstellungsfehler aufgedeckt werden.
  3. EventSource protokolliert Fehler mit der Ereignis-ID 0, und dieses Fehlerereignis enthält eine Zeichenfolge, die den Fehler beschreibt.
  4. Beim Debuggen wird die gleiche Fehlerzeichenfolge auch mit „Debug.WriteLine()“ protokolliert und im Debugausgabefenster angezeigt.
  5. 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 umfasst Konzepte für Aufgaben und OpCodes, die weitere Mechanismen zur Kennzeichnung und Filterung von Ereignissen bieten. Mithilfe der Eigenschaften Task und Opcode können Sie Ereignisse bestimmten Aufgaben und Opcodes zuordnen. Hier sehen Sie 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 implizit EventTask-Objekte erstellen, indem Sie zwei Ereignismethoden mit aufeinanderfolgenden Ereignis-IDs deklarieren, die das Namensmuster <EventName>Start und <EventName>Stop aufweisen. Diese Ereignisse müssen in der Klassendefinition nebeneinander deklariert werden, und die Methode <EventName>Start muss an erster Stelle stehen.

Selbstbeschreibende Formate (TraceLogging) und Manifestereignisformate im Vergleich

Dieses Konzept ist nur von Bedeutung, wenn Sie EventSource aus ETW abonniert haben. ETW kann Ereignisse auf zwei verschiedene Arten protokollieren: im Manifestformat und im selbstbeschreibenden Format (gelegentlich als TraceLogging bezeichnet). Manifest-basierte EventSource-Objekte generieren und protokollieren ein XML-Dokument, das die bei der Initialisierung für die Klasse definierten Ereignisse repräsentiert. Dazu muss die EventSource-Instanz per Reflexion die Anbieter- und Ereignis-Metadaten generieren. Im selbstbeschreibenden Format werden die Metadaten für jedes Ereignis nicht im Voraus, sondern inline mit den Ereignisdaten übertragen. Der selbstbeschreibende 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 mit jedem Ereignis ausgegebenen zusätzlichen Metadaten führen jedoch zu einem gewissen Leistungsmehraufwand, der beim Senden einer großen Anzahl von Ereignissen nicht unbedingt wünschenswert ist.

Um das selbstbeschreibende Ereignisformat zu verwenden, erstellen Sie Ihre EventSource-Instanz mit dem EventSource(String)- oder dem EventSource(String, EventSourceSettings)-Konstruktor, oder Sie legen das Flag „EtwSelfDescribingEventFormat“ für „EventSourceSettings“ fest.

EventSource-Typen mit Implementierung von Schnittstellen

Ein EventSource-Typ kann eine Schnittstelle implementieren, um eine nahtlose Integration in verschiedene erweiterte Protokollierungssysteme zu ermöglichen, die Schnittstellen zur Definition eines gemeinsamen Protokollierungsziels verwenden. Hier sehen Sie 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 für die Schnittstellenmethoden „EventAttribute“ angeben, da die Methode (aus Kompatibilitätsgründen) ansonsten nicht als Protokollierungsmethode behandelt wird. Die explizite Implementierung von Schnittstellenmethoden ist nicht zulässig, um Namenskonflikte zu vermeiden.

EventSource-Klassenhierarchien

In den meisten Fällen können Sie Typen schreiben, die direkt von der EventSource-Klasse abgeleitet sind. Manchmal ist es jedoch sinnvoll, Funktionen zu definieren, die von mehreren abgeleiteten EventSource-Typen gemeinsam genutzt werden, so z. B. angepasste WriteEvent-Überladungen (siehe Optimierung der Leistung bei hohem Ereignisaufkommen unten).

Abstrakte Basisklassen können verwendet werden, solange sie keine Schlüsselwörter, Aufgaben, Opcodes, Kanäle oder Ereignisse definieren. Das folgende Beispiel zeigt, wie die UtilBaseEventSource-Klasse eine optimierte WriteEvent-Überladung definiert, die von mehreren abgeleiteten EventSource-Instanzen in derselben Komponente benötigt wird. Einer dieser abgeleiteten Typen ist unten als „OptimizedEventSource“ dargestellt.

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
}

Optimierung der Leistung bei hohem Ereignisaufkommen

Die EventSource-Klasse umfasst eine Reihe von Überladungen für „WriteEvent“, darunter eine Überladung für eine variable Anzahl von Argumenten. Wenn keine der anderen Überladungen passt, wird die params-Methode aufgerufen. Leider ist die params-Überladung relativ kostenintensiv. Im einzelnen bewirkt sie Folgendes:

  1. Zuweisen eines Arrays zur Aufnahme der variablen Argumente
  2. Umwandeln der einzelnen Parameter in ein Objekt, wodurch Zuweisungen für Werttypen erfolgen
  3. Zuwiesen dieser Objekte zum Array
  4. Aufrufen der Funktion
  5. Ermitteln des Typs für jedes Arrayelement, um zu bestimmen, wie es serialisiert werden soll

Dies führt vermutlich zu Kosten, die 10- bis 20-mal so hoch liegen wie bei spezialisierten Typen. Bei einem geringen Ereignisaufkommen spielt dies keine große Rolle, aber bei einem hohen Aufkommen kann es wichtig sein. Es gibt zwei wichtige Fälle, in denen sichergestellt werden muss, dass die params-Überladung nicht verwendet wird:

  1. Stellen Sie sicher, dass Enumerationstypen in „int“ umgewandelt werden, damit sie einer der schnellen Überladungen entsprechen.
  2. Erstellen Sie für umfangreiche Nutzdaten neue schnelle WriteEvent-Überladungen.

Hier sehen Sie ein Beispiel für das Hinzufügen einer WriteEvent-Überladung, die vier int-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);
}