Gebeurtenisbronactiviteit-id's
Dit artikel is van toepassing op: ✔️ .NET Core 3.1 en latere versies ✔️ .NET Framework 4.5 en latere versies
In deze handleiding worden activiteit-id's uitgelegd, een optionele id die kan worden geregistreerd met elke gebeurtenis die wordt gegenereerd met behulp van System.Diagnostics.Tracing.EventSource. Zie Aan de slag met EventSource voor een inleiding.
De uitdaging van gelijktijdige logboekregistratie
Lang geleden is een typische toepassing mogelijk eenvoudig en één threaded, waardoor logboekregistratie eenvoudig is. U kunt elke stap in een logboekbestand op volgorde schrijven en het logboek vervolgens exact lezen in de volgorde waarin het is geschreven om te begrijpen wat er is gebeurd. Als de app aanvragen heeft verwerkt, is er slechts één aanvraag tegelijk verwerkt. Alle logboekberichten voor aanvraag A worden op volgorde afgedrukt, vervolgens alle berichten voor B, enzovoort. Wanneer apps meerdere threads worden, werkt die strategie niet meer omdat meerdere aanvragen tegelijkertijd worden verwerkt. Als elke aanvraag echter wordt toegewezen aan één thread die deze volledig verwerkt, kunt u het probleem oplossen door een thread-id voor elk logboekbericht op te nemen. Een app met meerdere threads kan bijvoorbeeld het volgende registreren:
Thread Id Message
--------- -------
12 BeginRequest A
12 Doing some work
190 BeginRequest B
12 uh-oh error happened
190 Doing some work
Door de thread-id's te lezen weet u dat thread 12 aanvraag A verwerkt en thread 190 aanvraag B verwerkt, waardoor het bericht 'uh-oh fout is opgetreden' met betrekking tot aanvraag A. De gelijktijdigheid van toepassingen is echter steeds geavanceerder geworden. Het is nu gebruikelijk om te gebruiken async
en await
zodat één aanvraag gedeeltelijk kan worden verwerkt op veel verschillende threads voordat het werk is voltooid. Thread-id's zijn niet langer voldoende om alle berichten die voor één aanvraag worden geproduceerd, te correleren. Met activiteits-id's wordt dit probleem opgelost. Ze bieden een nauwkeurigere id waarmee afzonderlijke aanvragen of delen van aanvragen kunnen worden bijgehouden, ongeacht of het werk wordt verspreid over verschillende threads.
Notitie
Het hier genoemde concept activiteits-id is niet hetzelfde als het System.Diagnostics.Tracing.Activity, ondanks de vergelijkbare naamgeving.
Werk bijhouden met behulp van een activiteits-id
U kunt de onderstaande code uitvoeren om activiteiten bij te houden in actie.
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();
}
}
}
}
Wanneer deze code wordt uitgevoerd, wordt de uitvoer afgedrukt:
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
Notitie
Er is een bekend probleem waarbij visual Studio-foutopsporingsprogramma ertoe kan leiden dat ongeldige activiteits-id's worden gegenereerd. Voer dit voorbeeld niet uit onder het foutopsporingsprogramma of stel een onderbrekingspunt in aan het begin van Main en evalueer de expressie System.Threading.Tasks.TplEventSource.Log.TasksSetActivityIds = false in het directe venster voordat u verdergaat met het probleem.
Met behulp van de activiteit-id's kunt u zien dat alle berichten voor werkitem A id hebben 00000011-...
en alle berichten voor werkitem B id hebben 00000012-...
. Beide werkitems werkten eerst aan thread 21256, maar daarna hebben ze hun werk voortgezet op afzonderlijke threadpoolthreads 11348 en 14728, dus het bijhouden van de aanvraag alleen met thread-id's zou niet hebben gewerkt.
EventSource heeft een automatische heuristiek waarbij het definiëren van een gebeurtenis met de naam _Something_Start onmiddellijk gevolgd door een andere gebeurtenis met de naam _Something_Stop wordt beschouwd als het begin en einde van een werkeenheid. Als u de begingebeurtenis voor een nieuwe werkeenheid aanmeldt, wordt een nieuwe activiteits-id gemaakt en worden alle gebeurtenissen in dezelfde thread geregistreerd met die activiteits-id totdat de stopgebeurtenis is geregistreerd. De id volgt ook automatisch de asynchrone controlestroom bij gebruik async
en await
. Hoewel u wordt aangeraden de achtervoegsels voor naamgeving starten/stoppen te gebruiken, kunt u de gebeurtenissen een naam geven die u leuk vindt door ze expliciet aantekeningen te maken met behulp van de EventAttribute.Opcode eigenschap. Stel de eerste gebeurtenis in op EventOpcode.Start
en de tweede op EventOpcode.Stop
.
Logboekaanvragen die parallel werken
Soms kan één aanvraag verschillende onderdelen van het werk parallel uitvoeren en wilt u alle logboekgebeurtenissen en de subonderdelen groeperen. In het onderstaande voorbeeld wordt een aanvraag gesimuleerd die twee databasequery's parallel uitvoert en vervolgens een verwerking uitvoert op het resultaat van elke query. U wilt het werk voor elke query isoleren, maar ook begrijpen welke query's deel uitmaken van dezelfde aanvraag wanneer veel gelijktijdige aanvragen kunnen worden uitgevoerd. Dit is gemodelleerd als een structuur waarin elke aanvraag op het hoogste niveau een hoofdmap is en vervolgens subporties van werk vertakkingen zijn. Elk knooppunt in de structuur krijgt een eigen activiteits-id en de eerste gebeurtenis die is geregistreerd met de nieuwe onderliggende activiteit-id registreert een extra veld met de naam Gerelateerde activiteits-id om het bovenliggende item te beschrijven.
Voer de volgende code uit:
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();
}
}
}
}
In dit voorbeeld wordt uitvoer afgedrukt, zoals:
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 dit voorbeeld is slechts één aanvraag op het hoogste niveau uitgevoerd, waaraan activiteits-id 00000011-...
is toegewezen. Vervolgens begon elke QueryStart
gebeurtenis een nieuwe vertakking van de aanvraag met respectievelijk activiteit-id's 00001011-...
00002011-...
. U kunt deze id's identificeren als onderliggende items van de oorspronkelijke aanvraag, omdat beide begingebeurtenissen hun bovenliggende 00000011-...
item hebben geregistreerd in het veld Gerelateerde activiteits-id.
Notitie
Mogelijk hebt u gezien dat de numerieke waarden van de id's enkele duidelijke patronen hebben tussen bovenliggende en onderliggende waarden en niet willekeurig zijn. Hoewel het kan helpen bij het visueel herkennen van de relatie in eenvoudige gevallen, is het het beste voor hulpprogramma's die niet op deze manier kunnen vertrouwen en de id's als ondoorzichtige id's kunnen behandelen. Naarmate het nestniveau dieper groeit, verandert het bytepatroon. Het gebruik van het veld Gerelateerde activiteits-id is de beste manier om ervoor te zorgen dat hulpprogramma's betrouwbaar werken, ongeacht het nestniveau.
Omdat aanvragen met complexe structuur van subwerkitems veel verschillende activiteits-id's genereren, worden deze id's meestal het beste geparseerd door hulpprogramma's in plaats van de structuur met de hand te reconstrueren. PerfView is één hulpprogramma dat weet hoe gebeurtenissen met een aantekening met deze id's moeten worden gecorreleerd.