Il log di Enterprise Library - parte 3 - personalizzazione

Scritto da  Massimo Bonanni il mercoledì 10 agosto 2011
Linguaggio: VB   •  Framework: 4.0   •  Livello: 100

Download sorgenti

Download pdf


In questo articolo della serie sull'Application Block di Logging di Enterprise Library ci occuperemo di come sia possibile customizzare lo stesso per adattarlo alle nostre esigenze.
Il modulo di logging è stato pensato, infatti, per essere facilmente esteso, in particolare vedremo come realizzare le nostre versioni del trace listener, del log formatter e del log filter.

Trace Listener
Il trace listener è il componente del log che si occupa di scrivere effettivamente i dati che lo sviluppatore ha deciso di loggare.
Enterprise Library mette a disposizione una serie abbastanza completa di Listener "di serie":

  • EmailTraceListener: log su email ;
  • FlatFileTraceListener, RollingFlatFileTraceListener: log su file di testo;
  • MsmqTraceListener : log su windows message queue;
  • WmiTraceListener : logging tramite eventi WMI;
  • XmlTraceListener: log su file XML;

ma permette di utilizzare anche:

  • EventLogTraceListener: log su registro eventi (presente in System.Diagnostic).

Se tra i precedenti non troviamo il listener che fa per noi, possiamno sempre crearne uno nostro (custom) derivando la classe da CustomTraceListener.
CustomTraceListener altro non è che una classe derivata direttamente da TraceListener di System.Diagnostic e che ci obbliga ad implementare i seguenti metodi:

  • Write : viene richiamato quando si ha la necessità di scrivere una stringa nel listener;
  • WriteLine : viene richiamato quando si ha la necessità di scrivere una stringa seguita da un "ritorno a capo";

Anche se, per creare un listener friubile, sono sufficienti questi due metodi, è consigliabile scrivere l'override dei due overload del metodo TraceData.
I metodi Write e WriteLine vengono, infatti, richiamati da Enterprise Library con il semplice messaggio inserito nella LogEntry inviata al log, mentre nel TraceData abbiamo l'intera istanza di LogEntry.
Per fare un esempio supponiamo di voler realizzare un TraceListener che invii il log sotto forma di SMS e supponiamo di avere a disposizione un servizio (non ci interessa come sia fatto) che invii un testo ad un numero.

Imports Microsoft.Practices.EnterpriseLibrary.Logging
Imports Microsoft.Practices.EnterpriseLibrary.Common.Configuration
Imports Microsoft.Practices.EnterpriseLibrary.Logging.Configuration
Imports Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners
 
<ConfigurationElementType(GetType(CustomTraceListenerData))> _
Public Class SmsTraceListener
    Inherits CustomTraceListener
 
    Private _SmsService As ISmsService
 
    Public Sub New()
        _SmsService = CreateSmsService()
    End Sub
 
    Public Overloads Overrides Sub Write(message As String)
    End Sub
 
    Public Overloads Overrides Sub WriteLine(message As String)
    End Sub
 
    Public Overrides Sub TraceData(eventCache As System.Diagnostics.TraceEventCache, source As String, eventType As System.Diagnostics.TraceEventType, id As Integer, data As Object)
        Dim logEntry = CType(data, LogEntry)
        Dim smsMessage = GetSmsMessage(logEntry)
        Dim number = Me.Attributes("Number")
        _SmsService.Send(number, smsMessage)
    End Sub
 
    Private Function CreateSmsService() As ISmsService
        Return New SmsService()
    End Function
 
    Private Function GetSmsMessage(logEntry As LogEntry) As String
        Dim smsMessage = String.Format("{0:dd/MM/yyyy HH:mm:ss} - {1}", logEntry.TimeStamp, logEntry.Message)
        If smsMessage.Length > 160 Then
            smsMessage = String.Concat(smsMessage.Substring(0, 157), "...")
        End If
        Return smsMessage
    End Function
End Class

 

Osserviamo che:

  • recuperiamo il log entry inviato al modulo di log andando ad effettuare il cast dell'argomento Data del metodo TraceData;
  • utilizziamo la collezione Attributes per contenere il numero a cui inviare il messaggio. Questa collezione, come vedremo tra un pò, è definibile con valori presi dal file di configurazione e la cosa ci fa particolarmente comodo;
  • la nostra classe è decorata con l'attributo ConfigurationElementType che è necessario per far si che il nostro listener sia in grado di essere gestito dalla console di configurazione di Enterprise Library. In questo esempio abbiamo deciso di utilizzare la sezione di configurazione CustomTraceListenerData che è la sezione di configurazione del listener custom fornita di serie da Enterprise Library.

Una volta creato  il listener è opportuno configurare la nostra applicazione affinchè sia in grado di utilizzarlo.
Per fare questo apriamo la console di configurazione cliccando sul file di configurazione della nostra applicazione, premendo il tasto destro del mouse e scegliendo l'opzione "Edit Configuration File" contraddistinta dall'iconetta di Enterprise Library.
Per inserire la configurazione del nostro trace listener è sufficiente utilizzare il comando (identificato dal simbolo "+") presente nella colonna dei trace listener:

Figura 1

Selezionando l'opzione "Add Custom Trace Listener" verrà aperta la finestra per la selezione della classe da importare come trace listener:

Figura 2

Come possiamo notare dalla precedente immagine, abbiamo la possibilità di scegliere uno dei tipi presenti all'interno del nostro progetto, presente in un assembly della GAC oppure caricare un assembly che lo contiene.

Una volta selezionato la classe, l'interfaccia di configurazione presenta il pannello di configurazione:

Figura 3

In questo troviamo:

  • il nome che vogliamo dare all'istanza del listener (possiamo avere anche più istanze dello stesso listener identificate da diversi nomi);
  • gli eventuali attributi che verranno passati al listener nella collezione Attributes (nel nostro caso "Number");
  • l'eventuale formatter da applicare al listener che possiamo utilizzare, all'interno dello stesso, tramite la proprietà Formatter della classe TraceListener;
  • il filtro della severity (il cui funzionamento è analogo a quello dei listener di serie);
  • le opzioni di output per aggiungere uteriori dati al log.

Una volta inserito il trace listener all'interno della configurazione, il suo utilizzo è del tutto analogo ad un qualsiasi altro listener.

Log Entry Formatter
Nel precedente paragrafo abbiamo visto come realizzare un trace listener custom in cui abbiamo gestito, anche, la formattazione del messaggio da inviare (metodo GetSmsMessage).  Nella realtà dei fatti, non è compito del listener occuparsi di formattare l'output del log, ma l'operazione è demandata ad un formatter.
Il formatter è una classe che implementa l'interfaccia ILogFormatter e che, grazie al metodo Format (implementazione dell'interfaccia) è in grado di prendere una LogEntry e restituire una stringa formattata pronta per essere scritta nel log.
Nel modulo di log dell'Enterprise Library troviamo due formatter quali il TextFormatter che permette di formattare la LogEntry in base ad una stringa di formato definita in configurazione e BinaryLogFormatter che consente di formattare la LogEntry in un formato binario pensato per poter essere trasmesso verso altri dispositivi.
Questi formatter sono assolutamente sufficienti per la maggior parte degli scenari che possono capitarci, ma se abbiamo la necessità di formattare la LogEntry in maniera particolare, possiamo creare il nostro formatter custom.
Nell'esempio del listener che invia il log con un messaggio sms, potremmo avere la necessità di formattare la LogEntry considerando la data di creazione e il messaggio di testo ma in modo che il numero di caratteri non superi un numero impostato in configurazione (ad esempio 160).
Per creare il nostro formatter è sufficiente implementare l'interfaccia ILogFormatter e il metodo Format:

Imports Microsoft.Practices.EnterpriseLibrary.Logging.Configuration
Imports Microsoft.Practices.EnterpriseLibrary.Common.Configuration
Imports Microsoft.Practices.EnterpriseLibrary.Logging.Formatters
Imports System.Collections.Specialized
 
<ConfigurationElementType(GetType(CustomFormatterData))>
Public Class SmsLogFormatter
    Implements ILogFormatter
 
    Public Sub New(params As NameValueCollection)
        If params("SmsLenght") IsNot Nothing Then
            Integer.TryParse(params("SmsLenght"), _SmsLength)
        End If
    End Sub
 
    Private _SmsLength As Integer = 160
 
    Public Function Format(log As Microsoft.Practices.EnterpriseLibrary.Logging.LogEntry) As String Implements Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.ILogFormatter.Format
        Return GetSmsMessage(log)
    End Function
 
    Private Function GetSmsMessage(logEntry As Microsoft.Practices.EnterpriseLibrary.Logging.LogEntry) As String
        Dim smsMessage = String.Format("{0:dd/MM/yyyy HH:mm:ss} - {1}", logEntry.TimeStamp, logEntry.Message)
        If smsMessage.Length > _SmsLength Then
            smsMessage = String.Concat(smsMessage.Substring(0, _SmsLength - 3), "...")
        End If
        Return smsMessage
    End Function
End Class

 

 Osserviamo che:

  • è stato implementato un costruttore che accetta un'istanza di NameValueCollection in input. Questo è necessario (pena una eccezione da parte del modulo di log di Enterprise Library) e permette di ricevere le impostazioni di configurazione;
  • la classe è decorata con l'attributo ConfigurationElementType. Anche in questo caso ha lo scopo di rendere il formatter configurabile all'interno della console di configurazione di Enterprise Library.

Per poter utilizzare il formatter procediamo in maniera analoga a quanto fatto per il listener: è sufficiente utilizzare il comando (identificato dal simbolo "+") presente nella colonna dei formatter:

Figura 4

Anche in questo caso, come per  il listener, dobbiamo selezionare il tipo da utilizzare come formatter:

Figura 5

Figura 6

Nel momento in cui associamo il formatter al listner (come mostrato nella precedente figura), possiamo modificare il listener stesso per disaccoppiare la funzionalità di formattazione del messaggio:

Imports Microsoft.Practices.EnterpriseLibrary.Logging
Imports Microsoft.Practices.EnterpriseLibrary.Common.Configuration
Imports Microsoft.Practices.EnterpriseLibrary.Logging.Configuration
Imports Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners
 
<ConfigurationElementType(GetType(CustomTraceListenerData))> _
Public Class SmsTraceListener
    Inherits CustomTraceListener
 
    Private _SmsService As ISmsService
 
    Public Sub New()
        _SmsService = CreateSmsService()
    End Sub
 
    Public Overloads Overrides Sub Write(message As String)
    End Sub
 
    Public Overloads Overrides Sub WriteLine(message As String)
    End Sub
 
    Public Overrides Sub TraceData(eventCache As System.Diagnostics.TraceEventCache, source As String, eventType As System.Diagnostics.TraceEventType, id As Integer, data As Object)
        Dim logEntry = CType(data, LogEntry)
        If Me.Formatter IsNot Nothing Then
            Dim message = Me.Formatter.Format(logEntry)
            Dim number = Me.Attributes("Number")
            _SmsService.Send(number, message)
        End If
    End Sub
 
    Private Function CreateSmsService() As ISmsService
        Return New SmsService()
    End Function
End Class

 

La proprietà Formatter del listener viene iniettata con il formatter associato (se presente) e possiamo quindi utilizzare liberamente il metodo Format senza preoccuparci a runtime di ciò che c'è dietro.

Log Filter
Un altro componente che possiamo customizzare è il log filter.
La funzione del log filter è quella di eseguire una scrematura sulle LogEntry che vengono inviate al modulo di log prima che queste finiscano in pasto ai listener configurati.
In questo caso, un log filter personalizzato è una classe che implementa l'interfaccia ILogFilter cioè il metodo Filter e la proprietà Name.
La proprietà Name serve all'infrastruttura dell'Enterprise Library per gestire il filtro mentre il metodo Filter ha lo scopo di decidere se una LogEntry (passata per argomento) può essere inviata al log o meno.
Nel nostro caso implementiamo un filtro che ci consenta di far passare le LogEntry solamente in un intervallo di tempo tra due orari passati in configurazione:

Imports Microsoft.Practices.EnterpriseLibrary.Common.Configuration
Imports Microsoft.Practices.EnterpriseLibrary.Logging.Configuration
Imports Microsoft.Practices.EnterpriseLibrary.Logging.Filters
Imports System.Collections.Specialized
 
<ConfigurationElementType(GetType(CustomLogFilterData))> _
Public Class TimeLogFilter
    Implements ILogFilter
 
    Public Sub New(params As NameValueCollection)
        _TimeFrom = GetTimeSpan(params("TimeFrom"))
        _TimeTo = GetTimeSpan(params("TimeTo"))
    End Sub
 
    Private _TimeFrom As TimeSpan
    Private _TimeTo As TimeSpan
 
    Public Function Filter(log As Microsoft.Practices.EnterpriseLibrary.Logging.LogEntry) As Boolean Implements Microsoft.Practices.EnterpriseLibrary.Logging.Filters.ILogFilter.Filter
        If _TimeFrom.CompareTo(log.TimeStamp.ToLocalTime.TimeOfDay) <= 0 And _TimeTo.CompareTo(log.TimeStamp.ToLocalTime.TimeOfDay) >= 0 Then
            Return True
        Else
            Return False
        End If
    End Function
 
    Public ReadOnly Property Name As String Implements Microsoft.Practices.EnterpriseLibrary.Logging.Filters.ILogFilter.Name
        Get
            Return "TimeLogFilter"
        End Get
    End Property
 
    Private Function GetTimeSpan(strTime As String) As TimeSpan
        Dim retval As TimeSpan = New TimeSpan(0, 0, 0)
        If Not String.IsNullOrWhiteSpace(strTime) Then
            Dim times = strTime.Split(":"c)
            If times.Length >= 3 Then
                Try
                    retval = New TimeSpan(CInt(times(0)), CInt(times(1)), CInt(times(2)))
                Catch ex As Exception
 
                End Try
            End If
        End If
        Return retval
    End Function
 
End Class

 

Possiamo osservare che:

  • abbiamo definito un costruttore con un parametro di tipo NameValueCollection che verrà richiamato dall'infrastruttura di Enterprise Library fornendoci I valori inseriti nel file di configurazione;
  • nel nostro caso tali valori sono due orari, nel formato HH:mm:ss, che definiscono l'ora minima e l'ora massima della fascia di scrittura log;
  • abbiamo implementato il metodo Filter che restitusce True se il timestamp della LogEntry rientra tra i due orari suddetti;
  • anche in questo caso, come nei precedenti, la classe è decorata con l'attributo ConfigurationElementType per definire la tipologia di sezione utilizzata nel file di configurazione.

Da notare, inoltre, che il timestamp della LogEntry è quello del meridiano di Greenwich, quindi dobbiamo applicare il metodo ToLocalTime per ottenere l'orario locale.
Una volta compilata la libreria con la classe appena creata, possiamo utilizzarla in maniera del tutto analoga a quanto fatto per i il trace listener o il formatter:

Figura 7 

Figura 8

Negli attributi possiamo inserire i valori che definiscono la fascia oraria in cui le LogEntry vengono inviate al log e che verranno passati al costruttore del nostro filtro.
Ogni volta che usiamo l'istruzione Write della classe LogWriter, verrà richiamato il metodo Filter e, in caso, di risultato positivo la LogEntry continuerà il suo viaggio verso il listener e il formatter.

Conclusioni
Una caratteristica peculiare dell'Enterprise Library è quella di fornire tanti punti di estensibilità e il modulo di logging, pur se già molto completo, non è da meno.
Estendere il comportamento del log è, come abbiamo visto, un'attività per nulla complessa ma che può riservarci molte soddisfazioni.

 

Riferimenti

[1] Enterprise Library Web Site: http://entlib.codeplex.com/
[2] Logging Application Block MSDN: http://msdn.microsoft.com/en-us/library/ff664569(v=PandP.50).aspx


Tags: Enterprise Library 5.0

 
x