EventSource-Aktivitäts-IDs
Dieser Artikel gilt für: ✔️ .NET Core 3.1 und höhere Versionen ✔️ .NET Framework 4.5 und höhere Versionen
In diesem Leitfaden werden Aktivitäts-IDs erläutert, optionale Bezeichner, die mit jedem Ereignis protokolliert werden können, das mit System.Diagnostics.Tracing.EventSource generiert wird. Eine Einführung finden Sie unter Erste Schritte mit EventSource.
Die Herausforderung, gleichzeitige Vorgänge zu protokollieren
Vor langer Zeit waren Anwendungen häufig simpel und arbeiteten mit nur einem Thread – was die Protokollierung vereinfacht. Sie konnten jeden Schritt in eine Protokolldatei schreiben und dann das Protokoll genau in der Reihenfolge lesen, in der es geschrieben wurde, um zu verstehen, was passiert war. Die App verarbeitete jeweils immer nur eine Anforderung. Alle Protokollnachrichten für Anforderung A wurden ausgegeben, dann alle Nachrichten für Anforderung B usw. Bei Apps mit mehreren Threads funktioniert diese Strategie nicht mehr, da mehrere Anforderungen gleichzeitig verarbeitet werden müssen. Wenn jedoch jede Anforderung einem einzelnen Thread zugewiesen ist, der diese vollständig verarbeitet, können Sie das Problem lösen, indem Sie eine Thread-ID für jede Protokollnachricht aufzeichnen. Beispielsweise kann eine Multithread-App Folgendes protokollieren:
Thread Id Message
--------- -------
12 BeginRequest A
12 Doing some work
190 BeginRequest B
12 uh-oh error happened
190 Doing some work
Anhand der Thread-IDs können Sie erkennen, dass Thread 12 Anforderung A und Thread 190 Anforderung B verarbeitet hat, daher steht die Meldung „uh-oh error happened“ im Zusammenhang mit Anforderung A. Die Parallelitätsanforderungen für Anwendungen werden jedoch immer komplexer. Heute ist es gängige Praxis, async
und await
zu verwenden, damit eine einzelne Anforderung teilweise in mehreren verschiedenen Threads verarbeitet werden kann, bevor die Arbeit abgeschlossen ist. Thread-IDs reichen nicht mehr aus, um alle Nachrichten zu korrelieren, die für eine einzige Anforderung erzeugt werden. Aktivitäts-IDs lösen dieses Problem. Sie stellen einen differenzierteren Bezeichner bereit, der einzelne Anforderungen oder Teile von Anforderungen nachverfolgen kann, unabhängig davon, ob die Arbeit auf verschiedene Threads verteilt ist.
Hinweis
Das hier beschriebene Konzept der Aktivitäts-IDs ist trotz der ähnlichen Benennung nicht mit System.Diagnostics.Tracing.Activity identisch.
Nachverfolgen der Verarbeitung mithilfe einer Aktivitäts-ID
Sie können den folgenden Code ausführen, um die Aktivitätsnachverfolgung in Aktion anzuzeigen.
using System.Diagnostics.Tracing;
public static class Program
{
public static async Task Main(string[] args)
{
ConsoleWriterEventListener listener = new ConsoleWriterEventListener();
Task a = ProcessWorkItem("A");
Task b = ProcessWorkItem("B");
await Task.WhenAll(a, b);
}
private static async Task ProcessWorkItem(string requestName)
{
DemoEventSource.Log.WorkStart(requestName);
await HelperA();
await HelperB();
DemoEventSource.Log.WorkStop();
}
private static async Task HelperA()
{
DemoEventSource.Log.DebugMessage("HelperA");
await Task.Delay(100); // pretend to do some work
}
private static async Task HelperB()
{
DemoEventSource.Log.DebugMessage("HelperB");
await Task.Delay(100); // pretend to do some work
}
}
[EventSource(Name ="Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log = new DemoEventSource();
[Event(1)]
public void WorkStart(string requestName) => WriteEvent(1, requestName);
[Event(2)]
public void WorkStop() => WriteEvent(2);
[Event(3)]
public void DebugMessage(string message) => WriteEvent(3, message);
}
class ConsoleWriterEventListener : EventListener
{
protected override void OnEventSourceCreated(EventSource eventSource)
{
if(eventSource.Name == "Demo")
{
Console.WriteLine("{0,-5} {1,-40} {2,-15} {3}", "TID", "Activity ID", "Event", "Arguments");
EnableEvents(eventSource, EventLevel.Verbose);
}
else if(eventSource.Name == "System.Threading.Tasks.TplEventSource")
{
// Activity IDs aren't enabled by default.
// Enabling Keyword 0x80 on the TplEventSource turns them on
EnableEvents(eventSource, EventLevel.LogAlways, (EventKeywords)0x80);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
lock (this)
{
Console.Write("{0,-5} {1,-40} {2,-15} ", eventData.OSThreadId, eventData.ActivityId, eventData.EventName);
if (eventData.Payload.Count == 1)
{
Console.WriteLine(eventData.Payload[0]);
}
else
{
Console.WriteLine();
}
}
}
}
Dieser Code gibt bei der Ausführung Folgendes aus:
TID Activity ID Event Arguments
21256 00000011-0000-0000-0000-00006ab99d59 WorkStart A
21256 00000011-0000-0000-0000-00006ab99d59 DebugMessage HelperA
21256 00000012-0000-0000-0000-00006bb99d59 WorkStart B
21256 00000012-0000-0000-0000-00006bb99d59 DebugMessage HelperA
14728 00000011-0000-0000-0000-00006ab99d59 DebugMessage HelperB
11348 00000012-0000-0000-0000-00006bb99d59 DebugMessage HelperB
11348 00000012-0000-0000-0000-00006bb99d59 WorkStop
14728 00000011-0000-0000-0000-00006ab99d59 WorkStop
Hinweis
Es gibt ein bekanntes Problem, bei dem der Visual Studio-Debugger die Generierung von ungültigen Aktivitäts-IDs verursachen kann. Problemumgehung: Entweder führen Sie dieses Beispiel nicht im Debugger aus, oder Sie legen am Anfang von „Main“ einen Haltepunkt fest und werten den Ausdruck „System.Threading.Tasks.TplEventSource.Log.TasksSetActivityIds = false“ im Direktfenster aus, bevor Sie die Verarbeitung fortsetzen.
Mithilfe von Aktivitäts-IDs können Sie sehen, dass alle Nachrichten für Arbeitselement A die ID 00000011-...
und alle Nachrichten für Arbeitselement B die ID 00000012-...
haben. Beide Arbeitselemente haben zunächst einige Aufgaben in Thread 21256 ausgeführt, dann aber die Verarbeitung in den separaten Threadpoolthreads 11348 und 14728 fortgesetzt, sodass der Versuch, die Anforderung nur mit Thread-IDs nachzuverfolgen, nicht funktioniert hätte.
EventSource verfügt über eine automatische Heuristik, bei der das Definieren eines Ereignisses mit dem Namen „_Something_Start“ gefolgt von einem anderen Ereignis mit dem Namen „_Something_Stop“ als Beginn und Ende einer Arbeitseinheit betrachtet wird. Durch das Protokollieren des Start-Ereignisses für eine neue Arbeitseinheit wird eine neue Aktivitäts-ID erstellt. Dann beginnt die Protokollierung aller Ereignisse im Thread mit dieser Aktivitäts-ID, bis das Stop-Ereignis protokolliert wird. Die ID folgt bei Verwendung von async
und await
auch automatisch einer asynchronen Ablaufsteuerung. Es wird zwar empfohlen, die Namenssuffixe „Start“ und „Stop“ zu verwenden, aber Sie können beliebige Namen für die Ereignisse angeben, indem Sie mithilfe der EventAttribute.Opcode-Eigenschaft explizit eine Anmerkung hinzufügen. Legen Sie das erste Ereignis auf EventOpcode.Start
und das zweite auf EventOpcode.Stop
fest.
Protokollieren von Anforderungen, die parallele Aufgaben ausführen
Manchmal führt eine einzelne Anforderung verschiedene Teile ihrer Verarbeitung parallel aus, und Sie möchten alle Protokollereignisse und die untergeordneten Teile gruppieren. Das folgende Beispiel simuliert eine Anforderung simuliert, die zwei Datenbankabfragen parallel ausführt und anschließend Verarbeitungsaufgaben für die Ergebnisses jeder Abfrage durchführt. Sie möchten die Aufgaben für jede Abfrage isolieren, aber auch verstehen, welche Abfragen zu ein und derselben Anforderung gehören, wenn viele gleichzeitige Anforderungen ausgeführt werden. Die Modellierung erfolgt als Baumstruktur, in der jede Anforderung auf oberster Ebene den Stamm darstellt und die einzelnen Unteraufgaben die Zweige repräsentieren. Jeder Knoten in der Struktur erhält eine eigene Aktivitäts-ID, und das erste Ereignis, das mit der neuen untergeordneten Aktivitäts-ID protokolliert wird, protokolliert ein zusätzliches Feld namens „Related Activity ID“, um das übergeordnete Element zu beschreiben.
Führen Sie den folgenden Code aus:
using System.Diagnostics.Tracing;
public static class Program
{
public static async Task Main(string[] args)
{
ConsoleWriterEventListener listener = new ConsoleWriterEventListener();
await ProcessWorkItem("A");
}
private static async Task ProcessWorkItem(string requestName)
{
DemoEventSource.Log.WorkStart(requestName);
Task query1 = Query("SELECT bowls");
Task query2 = Query("SELECT spoons");
await Task.WhenAll(query1, query2);
DemoEventSource.Log.WorkStop();
}
private static async Task Query(string query)
{
DemoEventSource.Log.QueryStart(query);
await Task.Delay(100); // pretend to send a query
DemoEventSource.Log.DebugMessage("processing query");
await Task.Delay(100); // pretend to do some work
DemoEventSource.Log.QueryStop();
}
}
[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log = new DemoEventSource();
[Event(1)]
public void WorkStart(string requestName) => WriteEvent(1, requestName);
[Event(2)]
public void WorkStop() => WriteEvent(2);
[Event(3)]
public void DebugMessage(string message) => WriteEvent(3, message);
[Event(4)]
public void QueryStart(string query) => WriteEvent(4, query);
[Event(5)]
public void QueryStop() => WriteEvent(5);
}
class ConsoleWriterEventListener : EventListener
{
protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Name == "Demo")
{
Console.WriteLine("{0,-5} {1,-40} {2,-40} {3,-15} {4}", "TID", "Activity ID", "Related Activity ID", "Event", "Arguments");
EnableEvents(eventSource, EventLevel.Verbose);
}
else if (eventSource.Name == "System.Threading.Tasks.TplEventSource")
{
// Activity IDs aren't enabled by default.
// Enabling Keyword 0x80 on the TplEventSource turns them on
EnableEvents(eventSource, EventLevel.LogAlways, (EventKeywords)0x80);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
lock (this)
{
Console.Write("{0,-5} {1,-40} {2, -40} {3,-15} ", eventData.OSThreadId, eventData.ActivityId, eventData.RelatedActivityId, eventData.EventName);
if (eventData.Payload.Count == 1)
{
Console.WriteLine(eventData.Payload[0]);
}
else
{
Console.WriteLine();
}
}
}
}
Dieses Beispiel führt zu folgender Ausgabe:
TID Activity ID Related Activity ID Event Arguments
34276 00000011-0000-0000-0000-000086af9d59 00000000-0000-0000-0000-000000000000 WorkStart A
34276 00001011-0000-0000-0000-0000869f9d59 00000011-0000-0000-0000-000086af9d59 QueryStart SELECT bowls
34276 00002011-0000-0000-0000-0000868f9d59 00000011-0000-0000-0000-000086af9d59 QueryStart SELECT spoons
32684 00002011-0000-0000-0000-0000868f9d59 00000000-0000-0000-0000-000000000000 DebugMessage processing query
18624 00001011-0000-0000-0000-0000869f9d59 00000000-0000-0000-0000-000000000000 DebugMessage processing query
18624 00002011-0000-0000-0000-0000868f9d59 00000000-0000-0000-0000-000000000000 QueryStop
32684 00001011-0000-0000-0000-0000869f9d59 00000000-0000-0000-0000-000000000000 QueryStop
32684 00000011-0000-0000-0000-000086af9d59 00000000-0000-0000-0000-000000000000 WorkStop
In diesem Beispiel wurde nur eine Anforderung der obersten Ebene ausgeführt, der die Aktivitäts-ID 00000011-...
zugewiesen wurde. Anschließend startete jedes QueryStart
-Ereignis eine neue Verzweigung der Anforderung mit den Aktivitäts-IDs 00001011-...
bzw. 00002011-...
. Dass diese IDs untergeordnete Elemente der ursprünglichen Anforderung sind, können Sie daran erkennen, dass beide Startereignisse ihre übergeordneten Aktivitäts-ID 00000011-...
im Feld „Related Activity ID“ protokolliert haben.
Hinweis
Sie haben sicherlich das Muster in den numerischen Werten der IDs für die übergeordneten und untergeordneten Elemente erkannt – diese Werte sind nicht zufällig. Das kann zwar in einfachen Fällen beim Erkennen von Beziehungen helfen, aber Tools sollten nicht darauf angewiesen sein und die IDs stattdessen als opake Bezeichner behandeln. Wenn weitere Schachtelungsebenen hinzukommen, ändert sich das Bytemuster. Die Verwendung des Felds „Related Activity ID“ ist die beste Methode, um sicherzustellen, dass Tools unabhängig von der Schachtelungsebene zuverlässig funktionieren.
Da Anforderungen mit komplexen Strukturen aus vielen Teilaufgaben viele verschiedene Aktivitäts-IDs generieren, sollten Sie diese IDs in der Regel von Tools parsen lassen, anstatt zu versuchen, die Struktur manuell zu rekonstruieren. Das Tool PerfView beispielsweise kann Ereignisse korrelieren, die Anmerkungen mit diesen IDs enthalten.