Rispondo ad una richiesta fattami riguardo la possibilità di loggare errori e di poter creare dei Trace all’interno delle applicazioni quando ci sono anomalie che non riusciamo a comprendere perché si verificano solo durante l’uso da parte degli utenti e quindi sulla release installata presso clienti.
Esiste ovviamente la possibilità del Debug da remoto, collegando il debugger di visual studio al processo sulla macchina dell’utente, ma sfortunatamente ci vogliono gli skill sistemistici necessari ad impostare le due macchine per fare questo tipo di attività, skill che buona parte di noi programmatori non possediamo.
Pertanto, il modo più semplice di capire cosa fanno gli utenti e dove si verificano dei problemi è quello di poter inserire dei messaggi di Trace nel codice e visualizzare dati e riferimenti per un successivo debug sulla macchina di sviluppo.
Allo scopo, esistono delle librerie che permettono di creare dei log dalle applicazioni .Net, io ho iniziato a programmare prima che venissero sviluppate e ho quindi creato un sistema di Log molto semplice ma che permette di fare quanto sopra descritto.
Ecco come è composto:
Questa è la solution di test dove ho copiato le mie classi, di cui diamo una breve descrizione:
- EventLogger – La classe principale del logger, che fornisce l’helper che può essere usato ovunque nei nostri programmi per loggare un messaggio, un eccezione, un errore, un avviso. E’ una classe statica che contiene un Evento e un metodo.
- ExceptionExtension – Un metodo di extension della classe Exception per comporre un messaggio a partire dai dati dell’exception ricevuta.
- MessageType – Enumerazione che descrive i possibili messaggi di log
- NsLogType – Enumerazione usata per poter indicare il tipo di log da attivare su una applicazione.
- SendMessageEventArgs – Argomento dell’evento scatenato quando logghiamo qualcosa.
- StringExtension – Funzione di estensione della classe stringa per controllare e generare le cartelle di output.
- App.xaml.cs – la classe che ospita il “Main” dell’applicazione, dove il Logger viene inizializzato per l’uso.
- MainWindow.xaml.cs – La finestra dove c’è il codice per testare il logger.
Come funziona il logger
Il logger utilizza come abbiamo detto due strumenti per lavorare, il primo è un Evento, che viene scatenato ogni volta che un messaggio è ricevuto, il secondo è un Metodo che può essere utilizzato in qualsiasi punto del codice per fornire un messaggio che verrà inviato tramite l’evento.
L’evento è statico, pertanto viene agganciato ad un event handler una sola volta nel Main dell’applicazione, l’Event handler è quello che si occupa fisicamente di generare e aggiornare il file di log.
Il metodo statico può essere richiamato nelle sue varie permutazioni, da qualsiasi punto del programma per scatenare l’evento di Log di un messaggio.
La classe EventLogger.cs
public static class EventLogger {
... }
La classe Eventlogger è una semplice classe C# ha solo la clausola static nella sua dichiarazione, questo indica che non è una classe istanziabile, e fornisce solo servizi.
public static event SendMessageEventHandler LogNewEntry;
private static void OnLogNewEntry(SendMessageEventArgs e) { if (LogNewEntry != null) { // Invokes the delegates. LogNewEntry(null, e); } }
Per quanto minuscolo, questo è il codice più importante, infatti è il codice che compone l’evento scatenato ad ogni chiamata dei metodi con cui le classi della nostra applicazione vogliono loggare un evento, un dato, o un messaggio. Descriveremo in seguito come è fatta la classe SendMessageEventArgs che fornisce i dati da loggare.
private static NsLogType mLogType;
public static NsLogType LogType { get { return mLogType; } set { mLogType = value; } }
Qui sopra abbiamo una property, anche essa statica che usualmente viene aggiornata allo startup dell’applicazione con un valore che indica il livello di Log che l’applicazione deve usare. Usualmente nel normale funzionamento si indica di Loggare solo le Eccezioni, mentre se stiamo facendo un debug, faremo loggare tutti i messaggi. In questo modo, possiamo spargere nel codice l’invio di informazioni sul funzionamento delle varie parti del codice che serviranno a raccogliere i dati relativi a cosa fanno gli utenti e in quali punti del codice passano.
public static void SendMsg(Exception exceptionToLog, string methodName) { SendMsg(exceptionToLog.XxBuildExceptionMessage(methodName), MessageType.Error); }
public static void SendMsg(Exception exceptionToLog) { PreserveStackTrace(exceptionToLog); SendMsg(exceptionToLog.XxBuildExceptionMessage(), MessageType.Error); }
public static void SendMsg(string messageToSend, MessageType messageType) { SendMessageEventArgs args = new SendMessageEventArgs(messageType, messageToSend, string.Empty); OnLogNewEntry(args); }
public static void SendMsg(MessageType messageType, string messageBody, string messageDetails) { SendMessageEventArgs args = new SendMessageEventArgs(messageType, messageBody, messageDetails); OnLogNewEntry(args); }
Qui sopra il codice dei 4 diversi metodi per loggare un messaggio o il contenuto di una Exception. SendMsg è il metodo helper che sarà chiamato nel codice delle nostre applicazioni ovunque vogliamo segnalare qualcosa. Le diverse versioni (applicazioni del Polimorfismo) permettono di costruire i messaggi in modo diverso in base all’esigenza del momento.
private static void PreserveStackTrace(Exception exception) { MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic); preserveStackTrace.Invoke(exception, null); }
Questo è un metodo a supporto del log delle exception, che è stato reso thread safe per fare in modo di poter loggare anche se ci troviamo in un thread diverso dal thread principale dell’applicazione.
La classe SendMessageEventArgs.cs
Questa classe serve a trasmettere i dati del messaggio all’Event Handler che si occupa di scrivere il file di log.
public class SendMessageEventArgs : EventArgs {
public SendMessageEventArgs(MessageType messageType, string messageBody, string messageDetails) { MessageType = messageType; MessageBody = messageBody; MessageDetails = messageDetails; StringBuilder sb = new StringBuilder(); sb.AppendLine(messageBody); sb.AppendLine(messageDetails); MessageToLog = sb.ToString(); }
public string MessageToLog { get; private set; }
public string MessageBody { get; private set; }
public string MessageDetails { get; private set; }
public MessageType MessageType { get; private set; } }
Questa classe è molto semplice, viene costruita generando i messaggi e contiene tutti i dati che possono essere scritti nel file di log.
L’enumerazione MessageType.cs
public enum MessageType : int { Info, Warning, Error }
Questa enumerazione è usata per indicare il tipo del messaggio che stiamo generando.
L’enumerazione NsLogType.cs
public enum NsLogType : int { None = 0, ExceptionsOnly, WarningsAndExceptions, AllMessages, }
Questa enumerazione è usata per decidere cosa deve essere loggato da parte del codice che implementa la scrittura sul file di Log.
La classe StringExtension.cs
public static void XxCheckPath(this String stringValue) { if (!Directory.Exists(stringValue)) { Directory.CreateDirectory(stringValue); } }
Come il nome della classe dice, in questa classe mettiamo dei metodi Extension della classe String del Framework, questi metodi sono disegnati per permettere di aggiungere funzionalità alle classi interne al framework senza dover scrivere codice complesso, derivando le classi base solo per aggiungere funzionalità. In questo caso l’extension permette di usare una stringa che contiene un Path per generare una cartella.
La classe ExceptionExtension.cs
Questa classe a supporto della generazione dei messaggi, contiene una serie di metodi Extension per la classe Exception che compongono il contenuto dell’exception per fornire messaggi che spieghino al meglio qual’é l’errore che si è verificato e dove si sia verificato. Non la copio in dettaglio, perché da solo supporto alla classe principale.
La configurazione del Logger in App.xaml.cs
All’interno di App.xaml.cs, allo startup dell’applicazione andiamo a configurare l’EventLogger, creando il codice che scrive in questo caso in un file di testo, ma potrebbe scrivere su un database, oppure sui Log di sistema di windows.
string mLogFileName = null; private const string LOGFILE = "BasicLogger\\BasicLogger_log_{0}"; public string LogFileName { get { if (mLogFileName == null) { mLogFileName = string.Format( Path.Combine(Environment.GetFolderPath( Environment.SpecialFolder.CommonDocuments), LOGFILE), DateTime.Now.ToString("yyyyMMdd")); } return mLogFileName; } }
Questa prima porzione di codice, crea una property che all’avvio dell’applicazione, genera un nome file per il Log, composto da un prefisso e dalla data del giorno formattata in modo da ordinare per data i file di log quando li ordiniamo per nome. In questo modo abbiamo un file di log applicazione diverso ogni giorno. Se lo notate, utilizzando le classi Environment di .Net configuriamo la cartella di destinazione del file di log, sulla cartella Public Documents, visibile, accessibile e modificabile da tutti gli utenti di un PC.
public void StartupTheLogger() { EventLogger.LogType = NsLogType.AllMessages; EventLogger.LogNewEntry += new SendMessageEventHandler(EventLogger_LogNewEntry); }
Il metodo qui sopra viene chiamato dallo startup dell’applicazione e decide cosa Loggare e aggancia l’event Handler sollevato dai messaggi di Log che si occuperà di scrivere sul log. Nell’esempio che ho fatto, su LogType metto un valore fisso, in realtà nelle mie applicazioni, questo è un parametro di configurazione che viene memorizzato come preferenza utente in modo che si possa cambiare ad uno dei valori in base a cosa vogliamo fare (usualmente Log delle eccezioni come normale, Log Di tutti i messaggi quando vogliamo debuggare.
private void EventLogger_LogNewEntry(object sender, SendMessageEventArgs e) { try { if (EventLogger.LogType != NsLogType.None) { StringBuilder message = new StringBuilder(); bool doLog = false; switch (e.MessageType) { case MessageType.Error: { doLog = true; break; } case MessageType.Warning: { if (EventLogger.LogType == NsLogType.WarningsAndExceptions || EventLogger.LogType == NsLogType.AllMessages) { doLog = true; } break; } case MessageType.Info: { if (EventLogger.LogType == NsLogType.AllMessages) { doLog = true; } break; } } if (doLog) { FileInfo fi = new FileInfo(LogFileName); fi.DirectoryName.XxCheckPath(); message.AppendLine(new string('*', 80)); message.AppendFormat("[{0} {1}]", DateTime.Now.ToShortDateString(), DateTime.Now.ToLongTimeString()); message.AppendLine(); message.Append(e.MessageToLog); message.AppendLine(); message.AppendLine(new string('*', 80)); message.AppendLine(); File.AppendAllText(LogFileName, message.ToString()); } } } catch (Exception) { //Se il logger da un eccezione c'è qualcosa che non va ma non possiamo //gestirla } }
In base a come è configurato il tipo di Log, questo metodo scrive una serie di righe su un file di testo. Ovviamente il formato può essere generato a piacimento cambiando quanto sta nella porzione di codice che viene eseguita se doLog è vero.
protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; StartupTheLogger(); OpenMainWindow(); }
Ed arriviamo allo startup dell’applicazione, dove chiamiamo il metodo di Startup del Logger. Anche se non è argomento di questo articolo vi invito a dare un’occhiata e implementare l’event Handler UnhandledException che è utilissimo per ottenere informazioni alla prima installazione di una applicazione, dove la stessa potrebbe esplodere a causa della mancanza di qualche libreria o altro.
La classe MainWindow.xaml.cs
Nella parte XAML di questa classe, abbiamo solo inserito alcuni Button per testare il logger pertanto non ne spiegheremo il contenuto.
private void LogError_Click(object sender, RoutedEventArgs e) { EventLogger.SendMsg("Messaggio di Errore, in caso di errore gestito ma bloccante", MessageType.Error); ResultText += Environment.NewLine + "Ho loggato un messaggio Errore di test"; } private void LogException_Click(object sender, RoutedEventArgs e) { try { ResultText += Environment.NewLine + "Ho loggato un messaggio Exception di test"; throw new InvalidOperationException("Test exception invalid operation"); } catch (Exception ex) { EventLogger.SendMsg(ex); } } private void LogInfo_Click(object sender, RoutedEventArgs e) { EventLogger.SendMsg("Messaggio info di test", MessageType.Info); ResultText += Environment.NewLine + "Ho loggato un messaggio Info di test"; } private void LogOpenFile_Click(object sender, RoutedEventArgs e) { System.Diagnostics.Process.Start(((App)Application.Current).LogFileName); ResultText += Environment.NewLine + "Ho Aperto il file di log"; } private void LogWarning_Click(object sender, RoutedEventArgs e) { EventLogger.SendMsg("Messaggio di avviso per errore non bloccante", MessageType.Warning); ResultText += Environment.NewLine+ "Ho loggato un messaggio Avviso di test"; }
Nel codice della MainWindow, abbiamo i metodi di test dei principali modi di loggare qualcosa, ovvero il log di una informazione, di una eccezione, di un avviso, di un errore.
Direi che con queste informazioni ed il codice a corredo, potete creare una dll ove memorizzare le classi base per il log e poi usarle in tutte le vostre applicazioni desktop.
Il codice a corredo può essere scaricato al link qui sotto indicato.
Se ci sono domande, dubbi, commenti, usate il tastino con la busta in cima alla pagina per inviare un messaggio e vi risponderò quanto prima.