Sdílet prostřednictvím


Nástroje kódu pro vytváření událostí EventSource

Tento článek se vztahuje na: ✔️ .NET Core 3.1 a novější verze ✔️ .NET Framework 4.5 a novější verze

Příručka Začínáme vám ukázala, jak vytvořit minimální zdroj událostí a shromažďovat události v trasovacím souboru. Tento kurz se podrobněji seznámí s vytvářením událostí pomocí System.Diagnostics.Tracing.EventSource.

Minimální zdroj událostí

[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);
}

Základní struktura odvozeného EventSource je vždy stejná. Zejména:

  • Třída dědí z System.Diagnostics.Tracing.EventSource
  • Pro každý jiný typ události, kterou chcete vygenerovat, je potřeba definovat metodu. Tato metoda by měla být pojmenována pomocí názvu události, která se vytváří. Pokud má událost další data, měla by se předat pomocí argumentů. Tyto argumenty události je potřeba serializovat, takže jsou povoleny pouze určité typy.
  • Každá metoda má tělo, které volá WriteEvent a předává ho ID (číselná hodnota představující událost) a argumenty metody události. ID musí být jedinečné v rámci EventSource. ID je explicitně přiřazeno pomocí System.Diagnostics.Tracing.EventAttribute
  • EventSources by měly být singletonové instance. Proto je vhodné definovat statickou proměnnou podle konvence označované jako Log, která představuje tento singleton.

Pravidla pro definování metod událostí

  1. Libovolná instance, která není virtuální, void vracející metodu definovanou ve třídě EventSource je ve výchozím nastavení metodou protokolování událostí.
  2. Virtuální nebo nevracící metody jsou zahrnuty pouze v případě, že jsou označené System.Diagnostics.Tracing.EventAttribute
  3. Chcete-li označit kvalifikující metodu jako nezaznamenávanou, musíte ji opatřit tagem System.Diagnostics.Tracing.NonEventAttribute.
  4. Metody protokolování událostí mají přidružená ID událostí. To lze provést buď explicitním označením metody pomocí System.Diagnostics.Tracing.EventAttribute, nebo implicitně podle pořadového čísla metody ve třídě. Například při použití implicitního číslování má první metoda ve třídě ID 1, druhá metoda má ID 2 a tak dále.
  5. Metody protokolování událostí musí volat přetížené funkce WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId nebo WriteEventWithRelatedActivityIdCore.
  6. ID události, ať už implicitní nebo explicitní, musí odpovídat prvnímu argumentu předaného rozhraní API WriteEvent*, které volá.
  7. Číslo, typy a pořadí argumentů předaných metodě EventSource musí odpovídat způsobu jejich předání do rozhraní API WriteEvent*. Pro WriteEvent následují argumenty za ID události, pro WriteEventWithRelatedActivityId následují argumenty za relatedActivityId. Pro metody WriteEvent*Core musí být argumenty serializovány ručně do parametru data.
  8. Názvy událostí nesmí obsahovat < ani > znaky. I když uživatelem definované metody také nemohou tyto znaky obsahovat, async metody kompilátor přepíše, aby je obsahoval. Abyste měli jistotu, že se tyto vygenerované metody nestanou událostmi, označte všechny metody, které nejsou událostmi, na EventSource pomocí NonEventAttribute.

Osvědčené postupy

  1. Typy odvozené z EventSource obvykle nemají přechodné typy v hierarchii nebo implementují rozhraní. Některé výjimky, které můžou být užitečné, najdete v části Pokročilé přizpůsobení níže.
  2. Obecně název třídy EventSource je špatný veřejný název pro EventSource. Veřejné názvy, názvy, které se zobrazí v konfiguracích protokolování a prohlížečích protokolů, by měly být globálně jedinečné. Proto je vhodné dát zdroji událostí veřejný název pomocí System.Diagnostics.Tracing.EventSourceAttribute. Název "Demo" použitý výše je krátký a není pravděpodobné, že by byl jedinečný, takže není dobrou volbou pro použití v produkčním prostředí. Běžnou konvencí je použít hierarchický název s . nebo - jako oddělovač, například MyCompany-Samples-Demo, nebo název sestavení nebo oboru názvů, pro který EventSource poskytuje události. Nedoporučujeme zahrnout "EventSource" jako součást veřejného názvu.
  3. Přiřaďte ID událostí explicitně, tímto způsobem zdánlivě neškodné změny kódu ve zdrojové třídě, jako je změna uspořádání nebo přidání metody uprostřed, nezmění ID události přidružené k jednotlivým metodám.
  4. Při vytváření událostí, které představují začátek a konec pracovní jednotky, jsou tyto metody pojmenovány příponami Start a Stop. Například RequestStart a RequestStop.
  5. Nezadávejte explicitní hodnotu pro vlastnost Guid EventSourceAttribute, pokud ji nepotřebujete z důvodu zpětné kompatibility. Výchozí hodnota GUID je odvozena od názvu zdroje, což umožňuje nástrojům přijmout lidmi čitelnější název a odvodit stejný GUID.
  6. Před provedením jakékoliv práce náročné na zdroje, která souvisí s vyvoláním události, volejte IsEnabled(). Například při výpočtu nákladného argumentu události, který nebude potřeba, pokud je událost zakázaná.
  7. Pokuste se zachovat objekt EventSource zpětně kompatibilní a odpovídajícím způsobem je verzovat. Výchozí verze události je 0. Verzi lze změnit nastavením EventAttribute.Version. Při každé změně dat serializovaných pomocí této události změňte verzi události. Vždy přidejte nová serializovaná data na konec deklarace události, tj. na konci seznamu parametrů metody. Pokud to není možné, vytvořte novou událost s novým ID, kterým nahradíte starou událost.
  8. Při deklarování metod událostí uveďte data s pevnou velikostí před daty proměnlivé velikosti.
  9. Nepoužívejte řetězce obsahující znaky null. Při generování manifestu pro ETW EventSource deklaruje všechny řetězce jako ukončené nulovým znakem, přestože je možné mít v řetězci C# znak null. Pokud řetězec obsahuje znak null, celý řetězec se zapíše do datové části události, ale každý analyzátor bude považovat první znak null za konec řetězce. Pokud jsou za řetězcem argumenty datové části, zbytek řetězce se analyzuje namísto předpokládané hodnoty.

Typické přizpůsobení událostí

Nastavení úrovní podrobností událostí

Každá událost má úroveň podrobností a odběratelé událostí často umožňují všechny události na EventSource až do určité úrovně podrobností. Události deklarují úroveň podrobností pomocí vlastnosti Level. Například v tomto EventSource odběratel, který požaduje události na úrovni Informational a nižší, nezaznamená událost 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);
}

Pokud úroveň podrobností události není zadána v EventAttribute, pak je výchozí hodnota Informational.

Nejlepší praxe

Pro relativně vzácná upozornění nebo chyby používejte úrovně menší než informační. Pokud pochybujete, držte se výchozí úrovně Informational a použijte úroveň Verbose pro události, ke kterým dochází častěji než 1000 událostí za sekundu.

Nastavení klíčových slov události

Některé systémy trasování událostí podporují klíčová slova jako další mechanismus filtrování. Na rozdíl od výřečnosti, která kategorizuje události podle úrovně detailů, jsou klíčová slova určena k kategorizaci událostí na základě jiných kritérií, jako jsou oblasti funkčnosti kódu, nebo která by byla užitečná pro diagnostiku určitých problémů. Klíčová slova jsou pojmenovaná bitové příznaky a každá událost může mít libovolnou kombinaci klíčových slov. Například následující EventSource definuje některé události, které se týkají zpracování požadavků a dalších událostí, které souvisejí se spuštěním. Pokud vývojář chtěl analyzovat výkon spuštění, může povolit protokolování pouze událostí označených klíčovým slovem po spuštění.

[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;
    }
}

Klíčová slova musí být definována pomocí vnořené třídy s názvem Keywords a každé jednotlivé klíčové slovo je definováno pomocí členu typd public const EventKeywords.

Osvědčená praxe

Klíčová slova jsou důležitější při rozlišování mezi velkými objemovými událostmi. To umožňuje příjemci událostí zvýšit verbóznost na vysokou úroveň. Zároveň může spravovat režii výkonu a velikost protokolu tím, že povolí pouze úzké podmnožiny událostí. Události spuštěné více než 1 000krát za sekundu jsou vhodnými kandidáty pro jedinečné klíčové slovo.

Podporované typy parametrů

EventSource vyžaduje, aby všechny parametry události bylo možné serializovat, takže přijímá pouze omezenou sadu typů. Toto jsou:

  • Primitiva: bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr a UIntPtr, Guid, decimal, string, DateTime, DateTimeOffset, TimeSpan
  • Výčty
  • Struktury s vlastností System.Diagnostics.Tracing.EventDataAttribute. Serializovány budou pouze vlastnosti veřejné instance s serializovatelnými typy.
  • Anonymní typy, ve kterých jsou všechny veřejné vlastnosti serializovatelné typy
  • Pole serializovatelných typů
  • Nullable<T> kde T je serializovatelný typ
  • KeyValuePair<T, U> kde T a U jsou oba serializovatelné typy
  • Typy, které implementují IEnumerable<T> pro přesně jeden typ T a kde T je serializovatelný typ

Řešení problémů

Třída EventSource byla navržena tak, aby ve výchozím nastavení nikdy nevyvolala výjimku. Jedná se o užitečnou vlastnost, protože protokolování je často považováno za volitelné a obvykle nechcete, aby při zápisu zprávy protokolu došlo k chybě, která by způsobila selhání vaší aplikace. To ale znesnadňuje nalezení jakékoli chyby ve zdroji událostí. Tady je několik technik, které vám můžou pomoct s řešením potíží:

  1. Konstruktor EventSource má přetížení, které přebírají EventSourceSettings. Zkuste dočasně povolit příznak ThrowOnEventWriteErrors.
  2. Vlastnost EventSource.ConstructionException ukládá všechny výjimky, které byly generovány při ověřování metod protokolování událostí. To může odhalit různé chyby vytváření.
  3. EventSource protokoluje chyby pomocí ID události 0 a tato chybová událost obsahuje řetězec popisující chybu.
  4. Při ladění se stejný řetězec chyby zaprotokoluje také pomocí Debug.WriteLine() a zobrazí se v okně výstupu ladění.
  5. EventSource interně vyvolá a potom zachytí výjimky, když dojde k chybám. Chcete-li zjistit, kdy k těmto výjimkám dochází, povolte v ladicím programu výjimky první šance, nebo použijte sledování událostí s povolenými událostmi výjimky modulu runtime .NET .

Pokročilá přizpůsobení

Nastavení operačních kódů a úloh

EtW má koncepty Tasks a OpCodes, což jsou další mechanismy pro označování a filtrování událostí. Události můžete přidružit ke konkrétním úlohám a operačním kódům pomocí vlastností Task a Opcode. Tady je příklad:

[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;
    }
}

Objekty EventTask můžete implicitně vytvářet deklarací dvou metod událostí s následnými ID událostí, které mají vzor pojmenování <EventName>Start a <EventName>Stop. Tyto události musí být deklarovány vedle sebe v definici třídy a <EventName>Start metoda musí přijít jako první.

Vlastní popis (tracelogging) vs. formáty událostí manifestu

Tento koncept je důležitý jen při přihlášení k odběru EventSource z ETW. ETW má dva různé způsoby, jak protokolovat události: formát manifestu a samo-popisující (někdy označovaný jako tracelogging) formát. Objekty EventSource založené na manifestu generují a protokolují dokument XML představující události definované ve třídě při inicializaci. To vyžaduje, aby služba EventSource odrážela sama sebe, aby vygenerovala zprostředkovatel a metadata událostí. Ve formátu metadat, která sama sebe popisují, se pro každou událost data přenášejí spolu s daty události, nikoli předem. Samoobslužný přístup podporuje flexibilnější metody Write, které mohou odesílat libovolné události bez vytvoření předem definované metody protokolování událostí. Při spuštění je to také o něco rychlejší, protože se vyhýbá časné reflexi. Avšak další metadata, která se vygenerují s každou událostí, přidávají malou režii na výkon, což při odesílání velkého objemu událostí nemusí být žádoucí.

Pokud chcete použít vlastní formát události, vytvořte eventSource pomocí konstruktoru EventSource(String), konstruktoru EventSource(String, EventSourceSettings) nebo nastavením příznaku EtwSelfDescribingEventFormat na EventSourceSettings.

Typy EventSource implementují ta rozhraní.

Typ EventSource může implementovat rozhraní, aby bylo možné bezproblémově integrovat do různých pokročilých systémů protokolování, které používají rozhraní k definování společného cíle protokolování. Tady je příklad možného použití:

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); }
}

U metod rozhraní musíte zadat EventAttribute, jinak (z důvodu kompatibility) nebude metoda považována za metodu protokolování. Explicitní implementace metody rozhraní je zakázána, aby se zabránilo kolizím názvů.

Hierarchie tříd EventSource

Ve většině případů budete moct psát typy, které jsou přímo odvozeny od třídy EventSource. Někdy je ale užitečné definovat funkce, které budou sdíleny více odvozenými typy EventSource, jako jsou přizpůsobené přetížení WriteEvent (viz optimalizace výkonu pro události s velkým objemem níže).

Abstraktní základní třídy lze použít, pokud nedefinují žádná klíčová slova, úkoly, opcode, kanály nebo události. Zde je příklad, kde třída UtilBaseEventSource definuje optimalizované přetížení WriteEvent, které je potřeba pro více odvozených EventSource ve stejné komponentě. Jeden z těchto odvozených typů je znázorněn níže jako 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
}

Optimalizace výkonu pro události s velkým objemem

Třída EventSource má řadu přetížení pro WriteEvent, včetně jednoho pro proměnný počet argumentů. Pokud se žádná z ostatních přetížení neshoduje, volá se metoda params. Přetížení parametrů je bohužel poměrně drahé. Konkrétně:

  1. Přidělí pole pro uložení argumentů proměnné.
  2. Přetypuje každý parametr na objekt, což způsobuje alokaci pro hodnotové typy.
  3. Přiřadí tyto objekty k poli.
  4. Volá funkci.
  5. Zjistí typ každého prvku pole, aby bylo možné určit, jak je serializovat.

Pravděpodobně je to 10 až 20krát tak drahé jako specializované typy. U případů s nízkým objemem to nezáleží, ale u událostí s velkým objemem to může být důležité. Existují dva důležité případy, kdy je možné zajistit, že se nepoužívá přetížení parametrů:

  1. Ujistěte se, že jsou výčtové typy přetypované na "int", aby odpovídaly jednomu z rychlých přetížení.
  2. Vytvořte nová, rychlá přetížení WriteEvent pro vysoké objemy dat.

Tady je příklad přidání přetížení WriteEvent, které přebírá čtyři celočíselné argumenty.

[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);
}