Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Ten artykuł dotyczy: ✔️ .NET Core 3.1 i nowsze wersje ✔️ .NET Framework 4.5 lub nowsze wersje
W przewodniku Wprowadzenie "Getting Started" pokazano, jak utworzyć minimalne źródło zdarzeń i zebrać zdarzenia w pliku śledzenia. Ten samouczek zawiera bardziej szczegółowe informacje na temat tworzenia zdarzeń przy użyciu System.Diagnostics.Tracing.EventSource.
Minimalna usługa EventSource
[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);
}
Podstawowa struktura pochodnego źródła zdarzeń jest zawsze taka sama. W szczególności:
- Klasa dziedziczy po System.Diagnostics.Tracing.EventSource
- Dla każdego innego typu zdarzenia, które chcesz wygenerować, należy zdefiniować metodę. Ta metoda powinna mieć nazwę przy użyciu nazwy tworzonego zdarzenia. Jeśli zdarzenie zawiera dodatkowe dane, należy przekazać je przy użyciu argumentów. Te argumenty zdarzeń muszą być serializowane, więc dozwolone są tylko niektórych typów.
- Każda metoda ma treść, która wywołuje metodę WriteEvent, przekazując jej identyfikator (wartość liczbową reprezentującą zdarzenie) i argumenty metody zdarzenia. Identyfikator musi być unikatowy w obrębie źródła zdarzeń. Identyfikator jest jawnie przypisywany przy użyciu System.Diagnostics.Tracing.EventAttribute
- EventSources powinny być singletonami. Dlatego wygodnie jest zdefiniować zmienną statyczną, zgodnie z konwencją o nazwie
Log, która reprezentuje ten singleton.
Reguły definiowania metod zdarzeń
- Każda niestatyczna, niewirtualna metoda void zdefiniowana w klasie EventSource jest domyślnie metodą rejestrowania zdarzeń.
- Metody wirtualne lub zwracające wartość są uwzględniane tylko wtedy, gdy są oznaczone System.Diagnostics.Tracing.EventAttribute
- Należy oznaczyć metodę kwalifikującą jako nielogującą poprzez oznaczenie jej za pomocą System.Diagnostics.Tracing.NonEventAttribute.
- Metody rejestrowania zdarzeń mają skojarzone identyfikatory zdarzeń. Można to zrobić albo jawnie, oznaczając metodę za pomocą System.Diagnostics.Tracing.EventAttribute, albo niejawnie, poprzez liczbę porządkową metody w klasie. Na przykład, przy użyciu niejawnego numerowania, pierwsza metoda w klasie ma identyfikator 1, druga ma identyfikator 2 i tak dalej.
- Metody rejestrowania zdarzeń muszą wywoływać przeciążenie WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId lub WriteEventWithRelatedActivityIdCore.
- Identyfikator zdarzenia, niezależnie od tego, czy jest dorozumiany, czy jawny, musi być zgodny z pierwszym argumentem przekazanym do API WriteEvent*, które wywołuje.
- Liczba, typy i kolejność argumentów przekazanych do metody EventSource muszą być zgodne ze sposobem przekazywania ich do interfejsów API WriteEvent*. W przypadku funkcji WriteEvent argumenty następują po identyfikatorze zdarzenia, a w przypadku funkcji WriteEventWithRelatedActivityId argumenty następują po identyfikatorze relatedActivityId. W przypadku metod WriteEvent*Core argumenty muszą być serializowane ręcznie do parametru
data. - Nazwy zdarzeń nie mogą zawierać znaków
<ani>. Chociaż metody zdefiniowane przez użytkownika również nie mogą zawierać tych znaków, kompilator przepisuje metodyasynctak, że zawierają te znaki. Aby upewnić się, że te wygenerowane metody nie stają się zdarzeniami, oznacz wszystkie metody inne niż zdarzenia na EventSource za pomocą NonEventAttribute.
Najlepsze rozwiązania
- Typy pochodzące z usługi EventSource zwykle nie mają typów pośrednich w hierarchii ani implementują interfejsów. Zobacz poniżej Zaawansowane dostosowania, aby zapoznać się z wyjątkami, w których może to być przydatne.
- Ogólnie rzecz biorąc, nazwa klasy EventSource jest złą nazwą publiczną źródła zdarzeń. Nazwy publiczne, nazwy, które będą wyświetlane w konfiguracjach rejestrowania i przeglądających dzienniki, powinny być globalnie unikatowe. W związku z tym dobrym rozwiązaniem jest nadanie usłudze EventSource nazwy publicznej przy użyciu System.Diagnostics.Tracing.EventSourceAttribute. Nazwa "Demo" używana powyżej jest krótka i mało prawdopodobne, aby była unikatowa, więc nie jest dobrym wyborem do użytku produkcyjnego. Typową konwencją jest użycie nazwy hierarchicznej, z separatorem takim jak
.lub-, jak na przykład "MyCompany-Samples-Demo", lub nazwy zestawu albo przestrzeni nazw, w której źródło zdarzeń udostępnia zdarzenia. Nie zaleca się dołączania elementu "EventSource" jako części nazwy publicznej. - Jawne przypisanie identyfikatorów zdarzeń, w ten sposób pozornie łagodne zmiany w kodzie w klasie źródłowej, takie jak zmiana jej rozmieszczania lub dodawanie metody w środku nie spowoduje zmiany identyfikatora zdarzenia skojarzonego z każdą metodą.
- Podczas tworzenia zdarzeń reprezentujących początek i koniec jednostki pracy, zgodnie z konwencją te metody mają nazwy z sufiksami "Start" i "Stop". Na przykład "RequestStart" i "RequestStop".
- Nie należy określać jawnej wartości właściwości Guid elementu EventSourceAttribute, chyba że jest ona potrzebna ze względów zgodności z poprzednimi wersjami. Domyślna wartość identyfikatora GUID jest wywodzona z nazwy źródła, co pozwala narzędziom na akceptowanie bardziej czytelnej dla człowieka nazwy i uzyskiwanie tego samego identyfikatora GUID.
- Wywołaj IsEnabled() przed wykonaniem jakiejkolwiek pracy intensywnie wykorzystującej zasoby związanej z wywołaniem zdarzenia, takiej jak obliczanie kosztownego argumentu zdarzenia, który nie będzie potrzebny, jeśli zdarzenie jest wyłączone.
- Spróbuj zachować zgodność wsteczną obiektu EventSource i odpowiednio go wersjonować. Domyślna wersja zdarzenia to 0. Wersję można zmienić, ustawiając EventAttribute.Version. Zmień wersję zdarzenia za każdym razem, gdy zmienisz dane serializowane. Zawsze dodaj nowe dane serializowane na końcu deklaracji zdarzenia, czyli na końcu listy parametrów metody. Jeśli nie jest to możliwe, utwórz nowe zdarzenie z nowym identyfikatorem, aby zastąpić stary.
- Podczas deklarowania metod zdarzeń określ dane ładunku o stałym rozmiarze przed zmiennym rozmiarem danych.
- Nie używaj ciągów zawierających znaki null. Podczas generowania manifestu dla elementu ETW EventSource zadeklaruje wszystkie ciągi jako zakończone znakiem null, nawet jeśli w ciągu języka C# można mieć znak null. Jeśli ciąg zawiera znak null, cały ciąg zostanie zapisany w ładunku zdarzenia, ale każdy analizator będzie traktować pierwszy znak null jako koniec ciągu. Jeśli po ciągu istnieją argumenty ładunku, pozostała część ciągu zostanie przeanalizowana zamiast zamierzonej wartości.
Typowe personalizacje zdarzeń
Ustawianie poziomów szczegółowości zdarzeń
Każde zdarzenie ma poziom szczegółowości, a subskrybenci zdarzeń często włączają wszystkie zdarzenia w usłudze EventSource do określonego poziomu szczegółowości. Zdarzenia deklarują poziom szczegółowości za pomocą właściwości Level. Na przykład w tym źródle zdarzeń subskrybent, który żąda zdarzeń poziomu informacyjnego i niższego, nie będzie rejestrował zdarzenia 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);
}
Jeśli poziom szczegółowości zdarzenia nie zostanie określony w elemecie EventAttribute, wartość domyślna to Informational.
Najlepsze rozwiązanie
Używaj poziomów mniejszych niż Informacyjny w przypadku stosunkowo rzadkich ostrzeżeń lub błędów. W razie wątpliwości należy trzymać się domyślnego poziomu informacyjnego i używać poziomu szczegółowego dla zdarzeń, które występują częściej niż 1000 razy na sekundę.
Ustawianie słów kluczowych zdarzenia
Niektóre systemy śledzenia zdarzeń obsługują słowa kluczowe jako dodatkowy mechanizm filtrowania. W przeciwieństwie do szczegółowości, która kategoryzuje zdarzenia według poziomu szczegółów, słowa kluczowe mają na celu kategoryzowanie zdarzeń na podstawie innych kryteriów, takich jak obszary funkcjonalności kodu lub które byłyby przydatne do diagnozowania niektórych problemów. Słowa kluczowe są nazwane flagami bitów, a każde zdarzenie może mieć dowolną kombinację słów kluczowych zastosowanych do niego. Na przykład poniższe źródło zdarzeń definiuje niektóre zdarzenia związane z przetwarzaniem żądań i innymi zdarzeniami, które odnoszą się do uruchamiania. Jeśli programista chciałby przeanalizować wydajność procesu uruchamiania, może włączyć rejestrowanie zdarzeń oznaczonych za pomocą słowa kluczowego startup.
[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;
}
}
Słowa kluczowe muszą być definiowane przy użyciu klasy zagnieżdżonej o nazwie Keywords, a każde pojedyncze słowo kluczowe jest definiowane przez członka typu public const EventKeywords.
Najlepsze rozwiązanie
Słowa kluczowe są ważniejsze podczas rozróżniania dużych zdarzeń. Dzięki temu odbiorca zdarzeń może podnieść szczegółowość do wysokiego poziomu, ale zarządzać obciążeniem wydajności i rozmiarem dziennika, włączając tylko wąskie podzestawy zdarzeń. Zdarzenia, które są wyzwalane ponad 1000 na sekundę, są odpowiednimi kandydatami na unikatowe słowo kluczowe.
Obsługiwane typy parametrów
Źródło zdarzeń wymaga serializacji wszystkich parametrów zdarzenia, więc akceptuje tylko ograniczony zestaw typów. Są to:
- Typy prymitywne: bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr, UIntPtr, decimal, Guid, string, DateTime, DateTimeOffset, TimeSpan
- Wyliczenia
- Struktury przypisane do System.Diagnostics.Tracing.EventDataAttribute. Tylko właściwości wystąpienia publicznego z typami z możliwością serializacji będą serializowane.
- Typy anonimowe, w których wszystkie właściwości publiczne są typami z możliwością serializacji
- Tablice typów możliwych do serializacji
- < > T dopuszczana do wartości null, gdzie T jest typem z możliwością serializacji
- KeyValuePair<T, U> gdzie zarówno T, jak i U są typami możliwymi do serializacji
- Typy implementujące typ IEnumerable<T> dla dokładnie jednego typu T i gdzie T jest typem z możliwością serializacji
Rozwiązywanie problemów
Klasa EventSource została zaprojektowana tak, aby nigdy nie zgłaszała wyjątku domyślnie. Jest to przydatna właściwość, ponieważ rejestrowanie jest często traktowane jako opcjonalne i zwykle nie chcesz, aby błąd podczas zapisywania komunikatu dziennika powodował niepowodzenie aplikacji. Jednak sprawia to, że znalezienie jakiegokolwiek błędu w usłudze EventSource jest trudne. Poniżej przedstawiono kilka technik, które mogą pomóc w rozwiązywaniu problemów:
- Konstruktor EventSource ma przeciążenia, które przyjmują EventSourceSettings. Spróbuj tymczasowo włączyć flagę ThrowOnEventWriteErrors.
- Właściwość EventSource.ConstructionException przechowuje wszystkie wyjątki wygenerowane podczas sprawdzania poprawności metod rejestrowania zdarzeń. Może to ujawnić różne błędy tworzenia.
- Zdarzenie EventSource rejestruje błędy przy użyciu identyfikatora zdarzenia 0, a to zdarzenie błędu zawiera ciąg opisujący błąd.
- Podczas debugowania ten sam ciąg błędu będzie również rejestrowany przy użyciu Debug.WriteLine() i pojawi się w oknie wyjściowym debugowania.
- Komponent EventSource wewnętrznie zgłasza, a następnie przechwytuje wyjątki, gdy wystąpią błędy. Aby zaobserwować, kiedy występują te wyjątki, włącz wyjątki pierwszej szansy w debugerze lub użyj śledzenia zdarzeń z włączonymi zdarzeniami wyjątków środowiska uruchomieniowego platformy .NET.
Dostosowania zaawansowane
Ustawianie kodów operacji i zadań
EtW ma pojęcia Tasks and OpCodes, które są kolejnymi mechanizmami tagowania i filtrowania zdarzeń. Zdarzenia można skojarzyć z określonymi zadaniami i kodami operacji przy użyciu właściwości Task i Opcode. Oto przykład:
[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;
}
}
Obiekty EventTask można utworzyć niejawnie, deklarując dwie metody zdarzeń z kolejnymi identyfikatorami zdarzeń, które mają wzorzec nazewnictwa <EventName>Start i <EventName>Stop. Te zdarzenia muszą być deklarowane obok siebie w definicji klasy, a metoda <EventName>Start powinna pojawić się jako pierwsza.
Samopisujące (tracelogging) vs. formaty zdarzeń manifestu
Ta koncepcja ma znaczenie tylko w przypadku subskrybowania EventSource z ETW. ETW ma dwa różne sposoby rejestrowania zdarzeń: format manifestu i format samoopisujący (czasami nazywany tracelogging). Obiekty EventSource oparte na manifeście generują i rejestrują dokument XML reprezentujący zdarzenia zdefiniowane w klasie podczas inicjowania. Wymaga to, aby EventSource przeglądało samo siebie w celu wygenerowania dostawcy i metadanych zdarzeń. W samookreślającym formacie metadane dla każdego zdarzenia są przesyłane razem z danymi zdarzenia, a nie wcześniej. Podejście samoopisujące obsługuje bardziej elastyczne metody Write, które mogą wysyłać dowolne zdarzenia bez konieczności tworzenia wstępnie zdefiniowanej metody rejestrowania zdarzeń. Jest to również nieco szybsze podczas uruchamiania, ponieważ unika wczesnej refleksji. Jednak dodatkowe metadane emitowane z każdym zdarzeniem zapewniają niewielkie obciążenie wydajności, które mogą nie być pożądane podczas wysyłania dużej liczby zdarzeń.
Aby użyć formatu zdarzenia opisującego samodzielnie, skonstruuj źródło zdarzeń przy użyciu konstruktora EventSource(String), konstruktora EventSource(String, EventSourceSettings) lub przez ustawienie flagi EtwSelfDescribingEventFormat w usłudze EventSourceSettings.
Typy EventSource implementujące interfejsy
Typ źródła zdarzeń może zaimplementować interfejs w celu bezproblemowego zintegrowania w różnych zaawansowanych systemach rejestrowania, które używają interfejsów do zdefiniowania wspólnego celu rejestrowania. Oto przykład możliwego użycia:
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); }
}
Należy określić atrybut EventAttribute w metodach interfejsu, w przeciwnym razie (ze względów zgodności) metoda nie będzie traktowana jako metoda rejestrowania. Jawna implementacja metody interfejsu jest niedozwolona w celu zapobiegania kolizjom nazewnictwa.
Hierarchie klasy EventSource
W większości przypadków będzie można zapisywać typy, które bezpośrednio pochodzą z klasy EventSource. Czasami jednak warto zdefiniować funkcje, które będą współużytkowane przez wiele pochodnych typów źródła zdarzeń, takich jak niestandardowe przeciążenia WriteEvent (zobacz optymalizowanie wydajności dla zdarzeń o dużej ilości poniżej).
Abstrakcyjne klasy bazowe mogą być używane tak długo, jak nie definiują żadnych słów kluczowych, zadań, kodów operacji, kanałów lub zdarzeń. Oto przykład, w którym klasa UtilBaseEventSource definiuje zoptymalizowane przeciążenie WriteEvent, które jest wymagane przez wiele pochodnych źródeł zdarzeń w tym samym składniku. Jeden z tych typów pochodnych przedstawiono poniżej 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
}
Optymalizowanie wydajności dla zdarzeń o dużej ilości
Klasa EventSource ma wiele przeciążeń dla metody WriteEvent, w tym jeden dla zmiennej liczby argumentów. Jeśli żadne z innych przeciążeń nie pasuje, wywoływana jest metoda params. Niestety przeciążenie parametrów jest stosunkowo drogie. W szczególności:
- Przydziela tablicę do przechowywania argumentów zmiennych.
- Rzutuje każdy parametr do obiektu, co powoduje alokacje typów wartości.
- Przypisuje te obiekty do tablicy.
- Wywołuje funkcję .
- Określa typ każdego elementu tablicy, aby określić, jak je serializować.
Jest to prawdopodobnie od 10 do 20 razy droższe niż wyspecjalizowane typy. Nie ma to znaczenia w przypadku małych przypadków, ale w przypadku zdarzeń o dużej ilości może być ważne. Istnieją dwa istotne przypadki, w których należy upewnić się, że przeciążenie parametrów nie jest stosowane:
- Upewnij się, że wyliczone typy są rzutowane na "int", aby były zgodne z jednym z szybkich przeciążeń.
- Utwórz nowe szybkie przeciążenia WriteEvent dla ładunków o dużej liczbie.
Oto przykład dodawania przeciążenia WriteEvent, które przyjmuje cztery argumenty całkowite
[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);
}