Questo articolo illustra come creare un sistema generico che ci permetta di Loggare le eccezioni oppure qualsiasi tipo di messaggio all’interno di qualsiasi applicazione.
Introduzione
Ogni applicazione, indipendentemente dalla sua complessità ha bisogno di una gestione degli errori ed ha bisogno di Debug, i metodi delle classi, soprattutto le più complesse e quelle che utilizzano risorse del computer esterne al nostro programma oppure risorse remote, devono prevedere una gestione degli errori e un sistema che ci permetta il debug anche in produzione.
Per poter fare questo genere di cose, il metodo più antico e collaudato è quello di costruire un sistema di Log che possa fornire informazioni quando il software da errore oppure possa fornire informazioni funzionali per l’ottimizzazione una volta in produzione.
Il nostro sistema di Log
Un sistema di Log deve avere alcune caratteristiche per essere usabile ed essere efficace, vediamo quelle che abbiamo pensato noi.
- Deve essere indipendentemente dalla tecnologia della UI che utilizziamo, pertanto non deve essere agganciato a Windows forms, piuttosto che a WPF, o ad altro.
- Deve essere semplice da usare, in modo che non siano necessarie informazioni dettagliate nei punti ove applicarlo, oppure i programmatori che sono pigri per natura non lo useranno.
- Deve essere usabile ovunque all’interno di una applicazione e gestibile in un unico punto.
- Deve permettere di loggare su qualsiasi mezzo, quindi su file, a video, via posta, ovunque noi vogliamo.
Tradurre i concetti sopra espressi in codice ha dato origine alla seguente serie di oggetti.
- EventLogger è una classe statica, che fornisce i metodi per poter loggare qualsiasi operazione ed un evento (sempre statico) che viene sollevato dalle funzioni di Log e permette all’applicazione di gestire il log in un unico punto.
- SendMessageEventArgs, classe di appoggio all’evento che permette di fornire i dati del messaggio da loggare.
- MessageType, enumerazione dei tipi di messaggio inviati, permette di far sapere al gestore eventi se il messaggio è un informazione, un avviso o un eccezione.
- LogType, enumerazione ad uso dell’event handler che gli permette di decidere quali messaggi devono essere loggati e quali no, dandoci ad esempio la possibilità di attivare un log verboso su una postazione operatore dove si verificano problemi non riproducibili per verificare ad esempio il flusso delle operazioni effettuate, oppure un log breve, contenente solo le eccezioni attivo per default per permetterci di chiedere all’operatore di inviarci l’eventuale storia delle eccezioni verificatesi durante il lavoro.
- ExceptionExtension, una classe contenente le prime funzioni di Extension per le eccezioni, che permettono la costruzione automatica del messaggio di eccezione da parte del sistema di log.
Rigeneriamo utilizzando il framework 4.0 quanto già costruito per il framework 2.0 negli articoli sulle classi di uso comune, ovviamente cercheremo di utilizzare quanto offerto dalle librerie più recenti del framework.
La classe ExeptionExtension
Partiamo con i metodi Extension della classe exception. I metodi Extension sono stati introdotti con il Framework 3.0 e danno la possibilità agli sviluppatori di Estendere le classi già esistenti del framework aggiungendo loro nuove funzionalità senza per questo dover creare delle classi derivate o costruire delle classi ex novo. I metodi Extension devono essere Statici (static in C#, shared in VB). La forma che assume un metodo statico è la seguente:
public static returnType MethodName(this ExtendedClassName extendedClass, ParameterType parameter1, ...., ParameterType parameterN)
Dove:
- returnType può essere void o qualsiasi altra classe vogliamo ritornare al chiamante.
- MethodName, è il nome del metodo che apparirà come parte dei metodi della classe che estendiamo.
- this è usato come placeholder per il compilatore, per indicare che il parametro successivo è la classe su cui applicare il metodo.
- ExtendedClass è il tipo della classe che stiamo estendendo (nel nostro caso Exception).
- ParameterType parameter1 -> ParameterType parameterN sono zero o più parametri che possiamo passare al metodo nella chiamata.
Vediamo il codice della classe extension così da capire meglio le extension con un esempio pratico.
private readonly static string m80Dashes = new string('-', 80);
Creiamo una stringa di separazione per costruire il messaggio di eccezione. Viene creata una sola volta in tutta l’applicazione e riutilizzata per ogni messaggio.
public static string XDwBuildExceptionMessage(this Exception exceptionToParse, string methodName) { StringBuilder sb = new StringBuilder(); sb.AppendLine(m80Dashes); sb.AppendFormat("Exception Time: {0} {1}", DateTime.Now.ToShortDateString(), DateTime.Now.ToLongTimeString()); sb.AppendLine(); sb.AppendLine(m80Dashes); try { sb.AppendFormat("Exception type:{0}", exceptionToParse.GetType()); sb.AppendLine(); MethodBase method = exceptionToParse.TargetSite; if (method != null) { object[] attribs = method.Module.Assembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false); sb.AppendLine(m80Dashes); sb.AppendFormat("Class name: {0}", method.ReflectedType.FullName); sb.AppendLine(); sb.AppendFormat("Method name: {0}", methodName); sb.AppendLine(); sb.AppendFormat("Assembly name: {0}", method.Module.Assembly.FullName); sb.AppendLine(); if (attribs != null && attribs.Length > 0) { AssemblyFileVersionAttribute att = attribs[0] as AssemblyFileVersionAttribute; if (att != null) { sb.AppendFormat("File Version: {0}", att.Version); sb.AppendLine(); } } sb.AppendFormat("Runtime Version: {0}", method.Module.Assembly.ImageRuntimeVersion); sb.AppendLine(); } sb.AppendLine(m80Dashes); sb.AppendFormat("Message: {0}", exceptionToParse.Message); sb.AppendLine(); } catch (Exception ex1) { sb.AppendLine(string.Format("Error reading error in exception")); sb.AppendLine(ex1.Message); } sb.AppendLine(m80Dashes); sb.AppendLine("StackTrace:"); sb.AppendLine(exceptionToParse.StackTrace); sb.AppendLine(m80Dashes); return (sb.ToString()); }
Un metodo per costruire il messaggio di errore, in questo caso utilizziamo l’inglese per le diciture perché questo messaggio è destinato a noi, non all’utente pertanto dobbiamo capire che cosa stiamo guardando anche se il messaggio viene da un computer con un altra lingua, per questo le diciture in inglese invece che all’interno di un file di risorse.
Il messaggio, risultante, contiene il momento in cui si è verificato, e tutti i dati relativi al punto in cui si è verificata che possano essere rilevati dall’exception.
public static string XDwBuildExceptionMessage(this Exception ex) { MethodBase method = ex.TargetSite; string name = "Unknown"; if (method != null) { name = method.Name; } return ex.XFyBuildExceptionMessage(name); }
Un overload del metodo precedente che ricava dai dati dell’eccezione anche il nome metodo, che nell’altro caso veniva arbitrariamente imposto.
public static string XDwBuildExceptionMessage(this Exception exceptionToParse, string methodName)
Vediamo nel dettaglio come abbiamo applicato la forma standard dell’ Extension in questo particolare metodo:
- returnType = string
- MethodName = XDwBuildExceptionMessage -> il prefisso XDw è voluto e sarà usato per tutti i metodi extension, serve per fare in modo che i metodi extension da noi prodotti siano facilmente riconoscibili e siano posizionati vicini, quindi X per Extension Dw per Dotnetwork.
- this Exception exceptionToParse = la classe che stiamo estendendo, questo rende disponibile il metodo a tutte le exeption derivate dalla classe base, quindi è per questo che abbiamo messo il metodo sulla più semplice.
- string methodName = Un parametro arbitrario passato al metodo, possiamo metterne più di uno e possono anche essere opzionali.
Sono certa che creeremo molte altre Extension delle classi base per fornire funzionalità a nostra discrezione da usare nei nostri progetti.
Le enumerazioni LogType e MessageType
public enum MessageType : int { Info, Warning, Error } public enum LogType : int { None = 0, ExceptionsOnly, WarningsAndExceptions, AllMessages, }
Non necessitano spiegazione e sono complementari, la prima viene utilizzata per indicare che tipo di messaggio stiamo loggando, la seconda permette di decidere a livello di gestione del log se loggare o meno un messaggio ricevuto.
La classe SendMessageEventArgs
Una classe di appoggio all’evento che gestirà il sistema di log, permetterà di fornire il messaggio con il suo tipo alla funzione event handler.
public SendMessageEventArgs(string messageToSend, MessageType messageType) { this.mMessageToLog = messageToSend; this.mMessageType = messageType; }
Per brevità ne mettiamo solo il costruttore, ovviamente vi saranno le variabili di classe e le property read only che le espongono all’event handler.
public delegate void SendMessageEventHandler(object sender, SendMessageEventArgs e);
Aggiungiamo inoltre il delegate che permetterà di definire l’evento statico e sollevarlo nella classe EventLogger
La classe EventLogger
Questa classe è il vero e proprio sistema di log, fornisce un evento statico intercettabile quindi a livello applicativo ed i metodi da utilizzare per generare l’evento ovunque nel nostro programma.
private static LogType mLogType; public static event SendMessageEventHandler LogNewEntry; public static LogType LogType { get { return mLogType; } set { mLogType = value; } }
La definizione dell’evento e quella di una proprietà che ci permetterà di decidere a livello di applicazione, qual’è il livello di log dei messaggi che vogliamo attivare.
public static void SendMsg(string messageToSend, MessageType messageType) { SendMessageEventArgs args = new SendMessageEventArgs(messageToSend, messageType); OnLogNewEntry(args); } public static void SendMsg(Exception exceptionToLog) { PreserveStackTrace(exceptionToLog); SendMsg(exceptionToLog.XDwBuildExceptionMessage(), MessageType.Error); }
I due overload del metodo Send Message, che loggano un messaggio arbitrario oppure un Exception, facendo uso della funzione extension discussa in precedenza.
private static void OnLogNewEntry(SendMessageEventArgs e) { if (LogNewEntry != null) { // Invokes the delegates. LogNewEntry(null, e); } } private static void PreserveStackTrace(Exception exception) { MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic); preserveStackTrace.Invoke(exception, null); }
Due metodi di appoggio, il primo scatena l’evento quando è definito un event handler, il secondo è un metodo di Workaround, ci siamo accorti che per qualche motivo a noi non noto, accadeva a volte che lo Stack Trace di un exception andasse perduto nella chiamata al metodo extension, utilizzando questa funzione della reflection, lo Stack Trace non viene modificato dalla chiamata per la costruzione della stringa di eccezione.
Visto che vogliamo debuggare la libreria che abbiamo creato ma vogliamo anche fare in modo che questa diventi una libreria all purpouse da usare in tutte le nostre applicazioni, creeremo un progetto di test su una diversa soluzione. Per fare in modo di avere sempre a disposizione le nostre librerie multipurpouse in ogni soluzione, siccome non mi piace agganciare gli stessi progetti a più soluzioni, perché trovo la cosa problematica soprattutto a livello di gestione progetti su Team Foundation Server, ho inserito nel post build event i comandi batch che mi copiano la libreria e i suoi file accessori in una cartella predefinita che contiene tutte le librerie userò questa copia pubblicata per tutti i reference.
Nel mio caso: B:\Dotnetwork
E’ una bozza piuttosto barbara, visto che se uno dei files non c’è da errore, ma potremo cogliere l’occasione e creare un programmino console allo scopo in un futuro post.
Conclusioni
Il progetto delle librerie generiche dotnetwork aggiornate con il codice del Logger è disponibile al link seguente:
Il progetto che testa l’uso del logger è disponibile al link seguente:
Per qualsiasi domanda, commento, approfondimento, o per segnalare un problema, potete utilizzare il link alla form di contatto in cima alla pagina.