Instrumentare il codice per creare eventi EventSource
Questo articolo si applica a: ✔️ .NET Core 3.1 e versioni successive ✔️ .NET Framework 4.5 e versioni successive
La guida introduttiva ha illustrato come creare un oggetto EventSource minimo e raccogliere eventi in un file di traccia. Questa esercitazione illustra in modo più dettagliato la creazione di eventi tramite System.Diagnostics.Tracing.EventSource.
EventSource minimo
[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);
}
La struttura di base di un EventSource derivato è sempre la stessa. In particolare:
- La classe eredita da System.Diagnostics.Tracing.EventSource
- Per ogni tipo di evento che si desidera generare, è necessario definire un metodo. Questo metodo deve essere denominato usando il nome dell'evento creato. Se l'evento contiene dati aggiuntivi, questi devono essere passati usando argomenti. Questi argomenti di evento devono essere serializzati in modo che siano consentiti solo determinati tipi.
- Ogni metodo ha un corpo che chiama WriteEvent passando un ID (un valore numerico che rappresenta l'evento) e gli argomenti del metodo dell’evento. L'ID deve essere univoco all'interno di EventSource. L'ID viene assegnato in modo esplicito usando il System.Diagnostics.Tracing.EventAttribute
- Gli EventSource devono essere istanze singleton. È quindi utile definire una variabile statica, per convenzione denominata
Log
, che rappresenta questo singleton.
Regole per la definizione dei metodi di evento
- Qualsiasi metodo di istanza, non virtuale, con ritorno void definito in una classe EventSource è per impostazione predefinita un metodo di registrazione eventi.
- I metodi virtuali o a ritorno non void sono inclusi solo se sono contrassegnati con System.Diagnostics.Tracing.EventAttribute
- Per contrassegnare un metodo idoneo come non di registrazione, è necessario decorarlo con l’opzione System.Diagnostics.Tracing.NonEventAttribute
- Ai metodi di registrazione eventi sono associati ID evento. Questa operazione può essere eseguita in modo esplicito decorando il metodo con un System.Diagnostics.Tracing.EventAttribute o in modo implicito in base al numero ordinale del metodo della classe . Ad esempio, l'uso della numerazione implicita del primo metodo della classe ha ID 1, il secondo ha ID 2 e così via.
- I metodi di registrazione eventi devono chiamare un overload WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId o WriteEventWithRelatedActivityIdCore.
- L'ID evento, implicito o esplicito, deve corrispondere al primo argomento passato all'API WriteEvent* che chiama.
- Il numero, i tipi e l'ordine degli argomenti passati al metodo EventSource devono essere allineati alla modalità di passaggio alle API WriteEvent*. Per WriteEvent gli argomenti seguono l'ID evento, per WriteEventWithRelatedActivityId gli argomenti seguono relatedActivityId. Per i metodi WriteEvent*Core, gli argomenti devono essere serializzati manualmente nel parametro
data
. - I nomi degli eventi non possono contenere i caratteri
<
o>
. Anche se i metodi definiti dall'utente non possono contenere questi caratteri, i metodiasync
verranno riscritti dal compilatore per includerli. Per assicurarsi che questi metodi generati non diventino eventi, contrassegnare tutti i metodi non evento in un EventSource con NonEventAttribute.
Procedure consigliate
- I tipi che derivano da EventSource in genere non hanno tipi intermedi nella gerarchia o implementano interfacce. Vedi Personalizzazioni avanzate di seguito, per consultare alcune eccezioni in cui ciò può essere utile.
- In genere il nome della classe EventSource è un nome pubblico non valido per EventSource. I nomi pubblici, i nomi che verranno visualizzati nelle configurazioni di registrazione e nei visualizzatori log, devono essere univoci a livello globale. È quindi consigliabile assegnare a EventSource un nome pubblico usando System.Diagnostics.Tracing.EventSourceAttribute. Il nome "Demo" usato in precedenza è breve e probabilmente non sarà univoco, quindi non è una buona opzione da usare in produzione. Una convenzione comune consiste nell'usare un nome gerarchico con
.
o-
come separatore, ad esempio "MyCompany-Samples-Demo" o il nome dell'assembly o dello spazio dei nomi per cui EventSource fornisce eventi. Non è consigliabile includere "EventSource" come parte del nome pubblico. - Assegnare gli ID evento in modo esplicito: in questo modo le modifiche apparentemente innocue al codice della classe di origine, ad esempio la riorganizzazione o l'aggiunta di un metodo al centro, non modificheranno l'ID evento associato a ogni metodo.
- Quando si creano eventi che rappresentano l'inizio e la fine di un'unità di lavoro, per convenzione questi metodi vengono denominati con i suffissi “Start” e “Stop”. Ad esempio, “RequestStart” e “RequestStop”.
- Non specificare un valore esplicito per la proprietà Guid di EventSourceAttribute, a meno che non sia necessario per motivi di compatibilità con le versioni precedenti. Il valore Guid predefinito è derivato dal nome dell'origine, il che consente agli strumenti di accettare il nome più leggibile dall'utente e di ricavare lo stesso Guid.
- Chiamare IsEnabled() prima di eseguire operazioni a elevato utilizzo di risorse correlate alla generazione di un evento, ad esempio il calcolo di un argomento di evento costoso che non sarà necessario se l'evento è disabilitato.
- Cercare di mantenere compatibile l'oggetto EventSource e di eseguirne la versione in modo appropriato. La versione predefinita per un evento è 0. La versione può essere modificata impostando EventAttribute.Version. Modificare la versione di un evento ogni volta che si modificano i dati serializzati con esso. Aggiungere sempre nuovi dati serializzati alla fine della dichiarazione di evento, ovvero alla fine dell'elenco dei parametri del metodo. Se non è possibile, creare un nuovo evento con un nuovo ID per sostituire quello precedente.
- Quando si dichiarano i metodi degli eventi, specificare i dati del payload a dimensione fissa prima di ridimensionare i dati in modo variabile.
- Non usare stringhe contenenti caratteri null. Quando si genera il manifesto di EventSource ETW, tutte le stringhe verranno dichiarate come null terminated, anche se è possibile avere un carattere null in una stringa C#. Se una stringa contiene un carattere null, l'intera stringa verrà scritta nel payload dell'evento, ma qualsiasi parser considererà il primo carattere null come fine della stringa. Se dopo la stringa sono presenti argomenti payload, verrà analizzato il resto della stringa invece del valore previsto.
Personalizzazioni eventi tipiche
Impostazione dei livelli di dettaglio evento
Ogni evento ha un livello di dettaglio e i sottoscrittori di eventi spesso abilitano tutti gli eventi in un EventSource fino a un determinato livello di dettaglio. Gli eventi dichiarano il livello di dettaglio usando la proprietà Level. Ad esempio, in questo EventSource un sottoscrittore che richiede eventi di livello informativo e inferiore non registra l'evento DebugMessage dettagliato.
[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);
}
Se il livello di dettaglio di un evento non viene specificato in EventAttribute, l'impostazione predefinita è Informativo.
Procedure consigliate
Usare livelli inferiori a Informativo per avvisi o errori relativamente rari. In caso di dubbio, attenersi all'impostazione predefinita Informativo e usare Dettagliato per gli eventi che si verificano con una frequenza maggiore a 1000 eventi/sec.
Impostazione delle parole chiave dell'evento
Alcuni sistemi di traccia eventi supportano le parole chiave come meccanismo di filtro aggiuntivo. A differenza del dettaglio, che classifica gli eventi in base al livello di dettaglio, le parole chiave servono per classificare gli eventi in base ad altri criteri, ad esempio aree di funzionalità del codice o utili per la diagnosi di determinati problemi. Le parole chiave sono flag di bit denominati e a ogni evento può essere applicata qualsiasi combinazione di parole chiave. Ad esempio, l’EventSource seguente definisce alcuni eventi correlati all'elaborazione delle richieste e altri eventi correlati all'avvio. Se uno sviluppatore vuole analizzare le prestazioni dell'avvio, potrebbe abilitare solo la registrazione degli eventi contrassegnati con la parola chiave avvio.
[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;
}
}
Le parole chiave devono essere definite usando una classe nidificata denominata Keywords
e ogni singola parola chiave è definita da un membro tipizzato public const EventKeywords
.
Procedure consigliate
Le parole chiave sono più importanti quando si distinguono gli eventi a volume elevato. Ciò consente a un consumer eventi di aumentare il grado di dettaglio a un livello elevato, ma di gestire il sovraccarico delle prestazioni e le dimensioni del log abilitando solo subset ristretti di eventi. Gli eventi attivati più di 1.000/sec sono buoni candidati per una parola chiave univoca.
Tipi di parametro supportati
EventSource richiede che tutti i parametri di evento possano essere serializzati, pertanto accetta solo un insieme limitato di tipi. Si tratta di:
- Primitive: bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr e UIntPtr, Guid decimal, string, DateTime, DateTimeOffset, TimeSpan
- Enumerazioni
- Strutture attribuite con System.Diagnostics.Tracing.EventDataAttribute. Verranno serializzate solo le proprietà pubbliche dell'istanza con tipi serializzabili.
- I tipi anonimi in cui tutte le proprietà sono pubbliche sono tipi serializzabili
- Matrici di tipi serializzabili
- Nullable<T> dove T è un tipo serializzabile
- KeyValuePair<T, U> dove sia T che U sono tipi serializzabili
- Tipi che implementano IEnumerable<T> per un tipo T esatto e dove T è un tipo serializzabile
Risoluzione dei problemi
La classe EventSource è stata progettata in modo da non generare mai un'eccezione per impostazione predefinita. Si tratta di una proprietà utile, poiché la registrazione viene spesso considerata facoltativa e in genere non si vuole che un errore di scrittura di un messaggio di log causi l'esito negativo dell'applicazione. Tuttavia, ciò rende difficile trovare eventuali errori in EventSource. Ecco alcune tecniche che consentono di risolvere i problemi:
- Il costruttore EventSource include overload che accettano EventSourceSettings. Provare ad abilitare temporaneamente il flag ThrowOnEventWriteErrors.
- La proprietà EventSource.ConstructionException archivia qualsiasi eccezione generata durante la convalida dei metodi di registrazione eventi. Ciò può rivelare alcuni errori di creazione.
- EventSource registra gli errori usando l'ID evento 0 e questo evento di errore contiene una stringa che descrive l'errore.
- Durante il debug, la stessa stringa di errore verrà registrata anche usando Debug.WriteLine() e visualizzata nella finestra di output di debug.
- EventSource genera internamente e quindi intercetta le eccezioni quando si verificano errori. Per osservare quando si verificano queste eccezioni, abilitare le eccezioni di prima probabilità in un debugger o usare la traccia eventi con gli eventi eccezione del runtime .NET abilitati.
Personalizzazioni avanzate
Impostazione di OpCodes e attività
ETW si avvale dei concetti di Attività e OpCodes, che sono altri meccanismi per contrassegnare e filtrare gli eventi. È possibile associare eventi ad attività e codice operativo specifici usando le proprietà Task e Opcode. Ecco un esempio:
[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;
}
}
È possibile creare in modo implicito oggetti EventTask dichiarando due metodi evento con ID evento successivi che hanno il modello di denominazione <EventName>Start e <EventName>Stop. Questi eventi devono essere dichiarati uno accanto all'altro nella definizione della classe e il metodo <EventName>Start deve essere il primo.
Formati di eventi autodescrittivo (tracelogging) e manifesto
Questo concetto è importante solo quando si sottoscrive EventSource da ETW. ETW offre due modi diversi per registrare eventi, il formato manifesto e formato autodescrittivo (talvolta definito tracelogging). Gli oggetti EventSource basati su manifesto generano e registrano un documento XML che rappresenta gli eventi definiti nella classe al momento dell'inizializzazione. È quindi necessario che EventSource rifletta su se stesso per generare il provider e i metadati dell'evento. Nel formato autodescrittivo i metadati di ogni evento vengono trasmessi inline con i dati dell'evento anziché in anticipo. L'approccio autodescrittivo supporta i metodi Write più flessibili che possono inviare eventi arbitrari senza aver creato un metodo di registrazione eventi predefinito. È anche leggermente più veloce all'avvio perché evita la riflessione eager. Tuttavia, i metadati aggiuntivi generati con ogni evento comportano un piccolo sovraccarico delle prestazioni, che potrebbe non essere auspicabile quando si invia un volume elevato di eventi.
Per usare il formato eventi autodescrittivo, costruire EventSource usando il costruttore EventSource(String), il costruttore EventSource(String, EventSourceSettings) o impostando il flag EtwSelfDescribingEventFormat in EventSourceSettings.
Tipi EventSource che implementano interfacce
Un tipo EventSource può implementare un'interfaccia per integrarsi facilmente in vari sistemi di registrazione avanzati che usano interfacce per definire una destinazione di registrazione comune. Di seguito è riportato un esempio di possibile utilizzo:
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); }
}
È necessario specificare l’EventAttribute nei metodi di interfaccia. In caso contrario, per motivi di compatibilità, il metodo non verrà considerato come metodo di registrazione. L'implementazione esplicita del metodo di interfaccia non è consentita per evitare conflitti di denominazione.
Gerarchie di classi EventSource
Nella maggior parte dei casi, sarà possibile scrivere tipi che derivano direttamente dalla classe EventSource. In alcuni casi, tuttavia, è utile definire funzionalità che verranno condivise da più tipi EventSource derivati, ad esempio overload WriteEvent personalizzati (vedere Ottimizzazione delle prestazioni per gli eventi a volume elevato di seguito).
Le classi di base astratte possono essere usate purché non definiscano parole chiave, attività, opcode, canali o eventi. Ecco un esempio in cui la classe UtilBaseEventSource definisce un overload WriteEvent ottimizzato, necessario per più EventSource derivati nello stesso componente. Uno di questi tipi derivati è illustrato di seguito come 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
}
Ottimizzazione delle prestazioni per eventi a volume elevato
La classe EventSource include diversi overload per WriteEvent, tra cui uno per il numero variabile di argomenti. Quando non corrisponde nessuno degli altri overload, viene chiamato il metodo parametri. Sfortunatamente, l'overload dei parametri è relativamente costoso. In particolare:
- Alloca una matrice per contenere gli argomenti della variabile.
- Esegue il cast di ogni parametro a un oggetto, il che causa allocazioni per i tipi valore.
- Assegna questi oggetti alla matrice.
- Chiama la funzione.
- Individua il tipo di ogni elemento della matrice per determinare come serializzarlo.
Ciò è probabilmente da 10 a 20 volte più costoso rispetto ai tipi specializzati. Questo non importa molto per i casi a volume ridotto, ma per gli eventi a volume elevato può essere importante. Esistono due casi importanti per assicurarsi che l'overload dei parametri non venga usato:
- Assicurarsi che il cast dei tipi enumerati venga eseguito su “int” in modo che questi corrispondano a uno degli overload rapidi.
- Creare nuovi overload WriteEvent rapidi per i payload a volumi elevati.
Ecco un esempio per l'aggiunta di un overload WriteEvent che accetta quattro argomenti integer
[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);
}