Logging
nelle applicazioni J2EE
I vari IDE oggi disponibili dispongono di una serie
di funzionalità di debugging più o meno
avanzate molto comode e potenti. Spesso chi approda
a Java da linguaggi come Visual Basic o Delphi è
abituato ad utilizzare tali strumenti per verificare
il flusso del codice, lo stato delle variabili, inserendo
breakpoint, watch di variabili e altri strumenti simili.
Usare un debugger integrato è una buona soluzione
nel caso in cui si stia sviluppando una applicazione
standalone.
Nel mondo J2EE però il livello di complessità
introdotto da tecnologie e architetture è in
genere tale da rendere questo approccio certamente limitante
e limitato. Le motivazioni per cui ogni programmatore
Java Enterprise decide di utilizzare soluzioni alternative
sono molte. Ecco un breve elenco:
Applicazioni
distribuite, architetture stratificate: le
applicazioni non sono mai costituite da un unico blocco
applicativo ma in genere diversi moduli e diversi
strati applicativi collaborano in modo distribuito
e remoto gli uni dagli altri. Spesso diviene impossibile
utilizzare l'ambiente di sviluppo se si devono tenere
sotto controllo diversi moduli in esecuzioni in modo
remoto o distribuito
Eterogeneità
ed interoperabilità di moduli diversi:
strettamente legato al contesto precedente, i diversi
componenti che formano l'applicazione nel suo contesto
devono essere eseguiti in modo diverso, spesso asincrono
e devono essere monitorati in modo differente (debug
dei moduli nuovi, controllo dei moduli in produzione,
verifica di quelli in upgrade o manutenzione),
Sviluppo,
test, produzione: il normale ciclo di sviluppo
del software prevede differenti modalità di
esecuzione. Si richiede spesso la necessità
di controllare il diverso comportamento di un componente
in funzione del contesto di esecuzione.
Spesso le sessioni di test basate su strumenti come
JUnit, devono poter essere eseguite in automatico
al variare del sistema grazie a semplici script di
sistema.
Verifica
iterativa: proprio perché i vari moduli
possono cambiare nel tempo si rende spesso necessario
tornare ad eseguire controlli basati sull'osservazione
dei messaggi di log con diversa prospettiva. Mentre
si esegue il modulo A che usa il modulo B si potrebbe
riattivare una sessione di debug sul modulo B anche
se questo ormai è stato esaustivamente testato.
Per
questi motivi fondamentali, unitamente ad altre di carattere
pratico, è ormai prassi comune considerare il
log applicativo come un elemento dello sviluppo di una
applicazione, tanto che si sono nel tempo proposti diversi
framework e strumenti applicativi. Il più famoso
che ha rimpiazzato ogni altro alternativo è certamente
log4j, sviluppato dal gruppo Jakarta.
In
questo articolo verrà presentato nelle sue funzionalità
principali affrontando l'uso di questo sistema di log
nei casi che si presentano più frequentemente:
logging all'interno di applicazioni web in Tomcat, logging
in applicazioni web o EJB in JBoss, logging di test
in applicazioni stand alone.
Il
framework log4j
Questo framework permette di inserire messaggi di log
all'interno di una applicazione (sia J2EE che J2SE)
in modo molto potente ed elegante.
Il concetto alla base di tutto il sistema è che
durante il ciclo di sviluppo test e utilizzo di un componente
software nasce l'esigenza di controllare a vario livello
il funzionamento dello stesso.
Se quindi durante lo sviluppo e il test è utile
essere informati di ogni più piccola informazione
circa il flusso applicativo, quando il sistema sarà
stato completamente debuggato e messo in produzione,
è opportuno monitorare solamente gli eventi più
gravi ed imprevisti (le eccezioni o i fatal error).
Per questo motivo log4j offre in modo molto semplice
ed efficace la possibilità di "accendere"
o "spegnere" il log di messaggi di livello
basso (livello dei messaggi ALL e DEBUG) medio (livello
dei messaggi INFO) o grave (ERROR e FATAL).
Combinando questa funzionalità con la possibilità
di redirigere in modo dinamico e composito messaggi
verso una o più destinazione (console, file,
syslog, email ed altro ancora) appare chiaro quanto
sia potente il sistema in questione.
Il fattore probabilmente più importante è
la capacità di configurare e modificare dinamicamente
il comportamento del log (livello, destinazioni) semplicemente
modificando un file di configurazione esterno alla applicazione,
senza la necessità di riscrivere o ricompilare
il codice, tramite la definizione di livelli di messaggio
è possibile in ogni momento.
Gli elementi principali del framework sono tre: loggers
(e loro gerarchie), appenders o destinazioni e layout
di stampa del messaggio.
I
loggers e le gerarchie di loggers
Un logger è uno strumento che permette di inviare
messaggi verso una determinata destinazione (vedi più
avanti). Istanza della classe Logger (che dalla versione
1.2 della API sostituisce la Category) sono strutturati
gerarchicamente in modo analogo ai package in Java dai
quali prendono spunto per la organizzazione dei nomi.
Ad esempio un logger di nome com.mokabyte assume il
ruolo di padre del logger con nome com.mokabyte.mylogger,
e antenato di com.mokabyte.mylogger.speciallogger.
L'organizzazione gerarchica dei logger è la base
su cui è stato progettato il framework permettendone
flessibilità e maneggevolezza.
Al vertice della gerarchia dei logger si trova il RootLooger
che ha la caratteristica di esistere sempre e non può
essere ricavato per nome ma tramite il metodo statico
Logger.getRootLogger();
Ogni
altro logger invece deve essere ricavato tramite il
nome assegnatogli per mezzo del metodo statico
Logger.getLogger(String
name);
Si
tratta di un factory method che restituisce un logger
già pronto all'uso senza doverne invocare il
costruttore: il nome passato come parametro comporta
la creazione di un logger con quel nome.
Leggendo nella documentazione si scopre inoltre che
internamente questo factory utilizza un singleton, per
cui se in vari punti della applicazione si invoca lo
stesso logger (stesso nome) si otterrà sempre
la stessa istanza di logger. Quindi scrivendo
Logger
loggerA = Logger.getLogger("myLogger");
Logger loggerB = Logger.getLogger("myLogger ");
si
ottiene come risultato che le due variabili loggerA
e loggerB punteranno alla stessa istanza di logger.
La presenza di un singleton internamente al framework
ha anche un altro importante riflesso: l'inizializzazione
tramite la lettura dei parametri di configurazione può
essere fatta una sola volta per tutta l'applicazione,
normalmente all'interno della classe principale (per
una applicazione standalone) o nel punto di start per
una applicazione web o EJB (vedi oltre).
Di seguito é riportata la definizione completa
della classe Logger
package
org.apache.log4j;
public
class Logger {
//
metodi di creazione e factory
public static Logger getRootLogger();
public static Logger getLogger(String name);
//
metodi di stampa:
public void debug(Object message);
public void info(Object message);
public void warn(Object message);
public void error(Object message);
public void fatal(Object message);
//
generico metodo di stampa
public void log(Level l, Object message);
}
L'invocazione
di uno dei metodi di stampa dà luogo a quella
che in gergo si dice una request. Si possono quindi
avere 5 livelli di request che corrispondono anche ai
livelli che si possono assegnare ad un determinato logger.
Dal più basso al più alto questi livelli
sono
DEBUG,
INFO, WARN, ERROR e FATAL
Quindi
scrivendo
Logger.info("ecco
un messaggio di avvertimento");
si
genera un request INFO, ovvero si invia il messaggio
contenuto tra virgolette verso la destinazione o le
destinazioni (appender) associati al logger.
Ad ogni logger viene assegnato un livello di importanza
e l'intersezione fra livello del logger con quello della
request da luogo al comportamento del logger stesso:
una request risulta essere attiva (ovvero alla sua invocazione
il messaggio viene effettivamente inviato) se il livello
della request é maggiore o uguale al livello
assegnato al logger.
Quindi la request appena vista verrà eseguita
solo se il logger ha livello DEBUG o INFO.
Anche
se non si possono avere le stesse prestazione del C++
che utilizza il meccanismo di preprocessamento del codice
(che però richiede la ricompilazione), la documentazione
ufficiale di log4j specifica che una request disattivata
non impatta sulle performance della applicazione complessiva:
il messaggio non solo non verrà inviato, ma in
base ad una efficiente ottimizzazione del codice il
metodo non verrà nemmeno eseguito.
All'atto pratico i logger si usano cospargendo il codice
di moltissime debug request, di alcune strategiche info
request e di poche ma essenziali error request (in genere
all'interno di una catch o al verificarsi di determinate
condizioni).
La scelta sul numero, il tipo e la collocazione delle
varie request all'interno del codice, é probabilmente
una delle cose più difficili da stabilire. Si
tratta come sempre di seguire il buon senso, di avvalersi
dell'esperienza e di trovare il giusto compromesso.
Tutte
queste request potranno essere disabilitate in fase
di test o produzione, quando gli errori più grossolani
si presume siano stati individuati ed eliminati.
L'organizzazione gerarchica dei logger consente di impostare
i livelli dei vari logger in modo gerarchico: ad esempio
associando il livello INFO ad un logger MyLogger, tutti
i logger figli di MyLogger adotteranno tale livello,
a meno che nella discesa della gerarchia non vi sia
una ridefinizione.
Un esempio pratico chiarisce questo passaggio meglio
di ogni altra spiegazione teorica; definendo
log4j.category.com.mokabyte=ERROR,
MyConsole
log4j.category.com.mokabyte.community=INFO
log4j.category.com.mokabyte.community.web.MyClass=DEBUG
si
impone il nodo base della gerarchia a livello ERROR
(che riflette tale livello su tutti i figli); quindi,
non essendo definito diversamente il logger com.mokabyte.util
assume livello ERROR. Successivamente si assegna il
livello INFO al logger com.mokabyte.community (quindi
tutti i logger del tipo com.mokabyte.community.xxx.yyy
sono assegnati a livello INFO) e solamente il logger
log4j.category.com.mokabyte.community.web.MyClass verrà
impostato a livello DEBUG.
Il livello del logger radice può essere specificato
tramite la seguente istruzione
log4j.rootLogger=ERROR
Il
fatto che vi sia una stretta corrispondenza fra nomi
di logger e package.classi non è casuale:
essendo la gerarchia di nomi di classi e package formalmente
corretta per specifica (il compilatore genera un errore
se vi sono due classi con lo stesso nome all'interno
dello stesso package), assegnano il Fully Qualified
Name (FQN, nome della classe completo di package) ad
un logger, si ottiene in modo semplice e sicuro un set
di logger dai nomi univoci e gerarchici.
Ad esempio nella classe MyClass si potrebbe scrivere
package
log4j.category.com.mokabyte.community.web;
public
class MyClass {
Logger logger = Logger.getLogger(MyClass.getClass.getName());
...
}
ed
in questo caso il logger qui definito ad uso interno
avrà lo stesso nome della classe, ovvero com.mokabyte.log4j.MyClass.
E' bene notare che da un punto di vista formale niente
vieta di utilizzare nomi di fantasia sempre che si rispetti
una organizzazione gerarchica di tali nomi; ovviamente
tale scelta è sconsigliabile per ragioni di praticità.
I
layout
Sui layout non vi molto da dire, dato che il loro funzionamento
è piuttosto intuitivo: si tratta di oggetti che
servono per definire la formattazione e la forma del
messaggio.
Essendo una request costituita da messaggio testuale,
un layout è generalmente una stringa utilizzata
da un parser per formattare tale messaggio.
Normalmente per configurare il layout del messaggio
si utilizzando pattern molto simili a quelli della funzione
printf() del linguaggio C. Ecco un esempio
%d{ABSOLUTE}
%-5p %c.%M() - %m%n
Ognuno
parametro indica una precisa trasformazione grafica:
%M ad esempio permette di stampare il nome del metodo
all'interno del quale si è creato il messaggio,
mentre %c indica il nome della classe. Invece %m stampa
il messaggio della request.
Quindi la sequenza %c.%M() - %m relativamente al codice
public
class MyClass{
Logger
logger = Logger.getLogger(MyClass.getClass.getName());
...
public void myMethod(){
logger.info("ecco un messaggio
di avvertimento");
}
...
}
inserito
all'interno del metodo myMethod() della classe MyClass,
stampa un messaggio del tipo
[...]
MyClass.myMethod () - ecco un messaggio di avvertimento
Per
maggiori dettagli sulla sintassi di configurazione dei
layout di log4j si consulti la documentazione presente
sul sito web (vedi [log4j]).
Gli
appenders
La relazione che lega un logger con un appender ricalca
il cosiddetto pattern sorgente ascoltatore. Ogni request
eseguita su un particolare logger, verrà inviata
a tutti gli appender che si sono registrati presso quel
logger.
Il framework mette a disposizione alcuni appender predefiniti
come la console, file di testo, code di messaggi JMS,
oppure i demoni syslog di Unix così come il logger
di eventi di NT.
E' possibile definire un proprio appender componendo
gli appender predefiniti, ad esempio reindirizzando
i messaggi verso più appender contemporaneamente.
Anche per gli appender l'organizzazione gerarchica dei
logger è utile: un messaggio prodotto da una
determinata request verrà propagato, oltre all'appender
registrato su quel determinato logger, anche verso tutti
gli appender registrati sui logger più in alto
nella gerarchia. Questo comportamento, detto appender
additivity, può essere evitato settando a false
l'additivity flag. Anche in questo caso in [log4j] si
possono trovare tutti gli approfondimenti sia sul ruolo
degli appender che sulle regole di additività.
La
configurazione
Questa è probabilmente la parte più importante
di tutta la gestione del sistema log4j: questa procedura
vuol dire essenzialmente definire per ogni logger il
suo livello e oltre agli appender e layout ad esso associati.
La configurazione può essere fatta all'interno
del codice o tramite file di configurazione (nel formato
.properties o XML). Per ovvi motivi è da preferirsi
la seconda soluzione che è anche quella che si
analizzerà in questa sede.
Di seguito è riportato un breve esempio di file
di configurazione tramite il quale si cercherà
di dare un'idea della struttura e della sintassi che
tale file deve avere. Ovviamente non si ha la pretesa
di analizzare ogni singolo caso ed elemento, ma piuttosto
di fornire una panoramica un'idea di massima su come
utilizzare tale file per i casi più frequenti.
Verrà prima affrontato il caso del file di configurazione
in formato .properties: esso è organizzato in
sezioni, a seconda della parte che si vuole configurare.
Per prima cosa si possono impostare alcuni parametri
di configurazione generali. Ad esempio
log4j.appender.MyConsole=org.apache.log4j.ConsoleAppender
definisce
un appender di nome MyConsole associato alla console
(classe ConsoleAppender) dove verranno stampati tutti
i messaggi.
Il formato dell'output potrà essere gestito in
vari modi: in questo caso si sceglie di utilizzare un
pattern layout manager
log4j.appender.MyConsole.layout=org.apache.log4j.PatternLayout
specificandone
il formato tramite
log4j.appender.MyConsole.layout.ConversionPattern=%-4r
%-5p [%t] %37c %3x - %m%n
Se
si desiderasse configurare un appender su file, si potrebbe
utilizzare il seguente codice per il quale si è
preso spunto direttamente dalla documentazione ufficiale
#
definisce un appender di nome FA
# FA è RollingFileAppender con dimensione massima
del file di 10 MB
# Utilizza come layout il TTCCLayout, con le date in
formato ISO8061
log4j.appender.FA=org.apache.log4j.RollingFileAppender
log4j.appender.FA.MaxFileSize=10MB
log4j.appender.FA.MaxBackupIndex=1
log4j.appender.FA.layout=org.apache.log4j.TTCCLayout
log4j.appender.FA.layout.ContextPrinting=enabled
log4j.appender.FA.layout.DateFormat=ISO8601
impostare
il valore di soglia (livello di log) per tutta l'applicazione
tramite la riga
log4j.threshold=[level]
dove
al solito i valori di level al solito possono essere
OFF,
FATAL, ERROR, WARN, INFO, DEBUG, ALL.
Si
configurano uno o più appender tramite la seguente
sintassi
log4j.appender.MyAppender=fully.qualified.name.of.appender.class
che
specifica il nome della classe per un appender di nome
MyAppender: il nome dell'appender può contenere
punti separatori.
Successivamente si possono passare alcuni parametri
di configurazione per l'appender.
log4j.appender.appenderName.option1=value1
...
log4j.appender.appenderName.optionN=valueN
A
questo punto si può procedere nella definizione
delle proprietà di configurazione dei logger,
sia specificando il comportamento del logger root, sia
specificando uno per uno ogni logger definito all'interno
della applicazione. Nel primo caso si potrebbe scrivere
ad esempio
log4j.rootLogger=[level],
appenderName, appenderName, ...
dove
level può assumere uno dei valori OFF, FATAL,
ERROR, WARN, INFO, DEBUG, ALL, mentre i vari appenderName
sono i nomi degli appender da utilizzare.
Questa soluzione, anche se non è molto flessibile,
evita di dover configurare ogni singolo logger della
applicazione (che essendo figli di rootLogger ne ereditano
le caratteristiche). Il programmatore dovrà scegliere
il giusto compromesso fra semplicità e potenza.
Infine se si volesse configurare ogni singolo logger
si dovrà per ognuno inserire una riga simile
a quella vista per il rootLogger: ad esempio
log4j.logger.com.mokabyte.community.util.io.PersistenceManager=DEBUG,
MyConsole
log4j.logger.com.mokabyte.community.users.User=ERROR,
MyConsole
log4j.logger.com.mokabyte.community.users.GuestUser=ERROR,
MyConsole
log4j.logger.com.mokabyte.community.users.PowerUser=ERROR,
MyConsole
log4j.logger.com.mokabyte. community.users.UserManager=ERROR,
MyConsole
In questo caso, pur essendo l'applicazione molto semplice,
si è costretti a dover specificare molte informazioni;
se ci si dimentica di definire il comportamento di un
logger verrà generata un errore bloccante.
Per
chi fosse interessato ad una completa analisi dei parametri
di configurazione si consiglia di leggere la pagina
di commento inserita direttamente nel JavaDoc del log4j
[log4jconf].
Nel caso in cui si volesse eseguire una configurazione
veloce e semplice per alcune prove, si può utilizzare
la classe BasicConfigurator che utilizza le impostazione
di default proprie del framework e che non sono modificabili
(appender console, livello DEBUG).
In alternativa al formato .properties del file di configurazione
è possibile utilizzare il formato XML, che risulta
essere leggermente più complesso ma certamente
più potente (grazie alla potenza espressiva dell'XML
ad esempio si possono creare aggregazioni in modo più
chiaro).
Il formato XML è adottato da JBoss per cui è
utile provare a dare uno sguardo al file log4j.xml,
contenuto nella directory conf della configurazione
utilizzata (default|all|minimal) per far partire il
server.
Di seguito è riportato un estratto di alcuni
passaggi interessanti: si noti la parte tramite la quale
si definiscono alcuni appender:
Appender
file
Definisce un appender associato ad un file di tipo
rolling semplice (un file basato su una struttura di
tipo coda di messaggi, con una dimensione massima in
byte) oppure daily rolling (ogni giorno ne viene creato
uno nuovo)
<appender
name="FILE" class="org.jboss.logging.appender.DailyRollingFileAppender">
<errorHandler class="org.jboss.logging.util.OnlyOnceErrorHandler"/>
<param name="File" value="${jboss.server.home.dir}/log/server.log"/>
<param name="Append" value="false"/>
<!--
Rollover at midnight each day -->
<param name="DatePattern" value="'.'yyyy-MM-dd"/>
<layout
class="org.apache.log4j.PatternLayout">
<!-- The default pattern:
Date Priority [Category] Message\n -->
<param name="ConversionPattern"
value="%d %-5p [%c].%M() %m%n"/>
</layout>
</appender>
Appender
console
Definisce un appender associato alla console di sistema.
Si noti l'attributo Target associato alla System.out
di Java; importantissimo l'attributo di soglia Threshold
che permette di tagliare tutti i messaggi sotto un certo
livello limitando in questo modo l'affollarsi di messaggi
indesiderati per quell'appender. Si potrebbe voler inviare
migliaia di messaggi verso appender FILE ma limitare
la console (notoriamente più lenta) a stampare
solo i messaggi a livello ERROR
<appender
name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
<errorHandler class="org.jboss.logging.util.OnlyOnceErrorHandler"/>
<param name="Target" value="System.out"/>
<param name="Threshold" value="DEBUG"/>
<layout
class="org.apache.log4j.PatternLayout">
<!-- The default pattern:
Date Priority [Category] Message\n -->
<param name="ConversionPattern"
value="%d{ABSOLUTE} %-5p %c.%M() - %m%n"/>
</layout>
</appender>
Appender
mail
Permette di inviare mail con i messaggi di log. Dato
l'alto overhead che può causare tale appender
si faccia attenzione ad associarlo solo a messaggi di
livello molto alto (soglia impostata a ERROR)
<appender
name="SMTP" class="org.apache.log4j.net.SMTPAppender">
<errorHandler class="org.jboss.logging.util.OnlyOnceErrorHandler"/>
<param name="Threshold" value="ERROR"/>
<param name="To" value="admin@myhost.domain.com"/>
<param name="From" value="nobody@myhost.domain.com"/>
<param name="Subject" value="JBoss
Sever Errors"/>
<param name="SMTPHost" value="localhost"/>
<param name="BufferSize" value="10"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern"
value="[%d{ABSOLUTE},%c{1}] %m%n"/>
</layout>
</appender>
Appender
async
E' un appender particolarmente utile per creare aggregazioni
di appender con invio di messaggi asincroni rispetto
alla esecuzione del codice applicativo
<appender
name="ASYNC" class="org.apache.log4j.AsyncAppender">
<errorHandler class="org.jboss.logging.util.OnlyOnceErrorHandler"/>
<appender-ref ref="FILE"/>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="SMTP"/>
</appender>
Infine
alcune categorie con associati i livelli
<category
name="org.apache.commons">
<priority value="INFO"/>
</category>
<category
name="org.jboss">
<priority value="INFO"/>
</category>
<category name="com.mokabyte">
<priority value="ERROR"/>
</category>
<category
name="com.mokabyte.mvc.servlets">
<priority value="ERROR"/>
</category>
<category
name="com.mokabyte.mvc.actions">
<priority value="ERROR"/>
</category>
<category
name="com.mokabyte.community">
<priority value="INFO"/>
</category>
Ecco
infine il codice XML per la definizione del root level
dei messaggi
<root>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
Logging
in J2EE
Una applicazione J2EE è caratterizzata da una
serie di requisiti architetturali e tecnologici non
banali; spesso il buon funzionamento (ma anche il cattivo)
dipendono da molti aspetti non facilmente individuabili
o isolabili.
Un tipico applicativo è in genere costituito
da componenti, assemblati in strati, comunicanti tramite
protocolli di vario tipo (CORBA, RMI/IIOP, JNP, AJP,
HTTP, etc.) per cui è spesso difficile capire
dove si sia verificato il problema quando qualcosa non
va come dovrebbe.
Per questo motivo è prassi comune spargere nel
codice nei vari punti cruciali messaggi di log di vario
tipo per capire in ogni istante cosa stia succedendo.
Questo genere di approccio porta in alcuni casi ad una
eccessiva ridondanza dei messaggi, cosa che rende difficile
nello stesso modo l'interpretazione dell'accaduto. Si
pensi ad esempio al caso in cui all'interno di una applicazione
web basata su MVC, una Action esegue una chiamata remota
ad un session bean di façade. In questo caso
si può sintetizzare l'applicazione come strutturata
su due livelli (strato web e strato EJB).
Se si verifica un errore all'interno del session bean,
è presumibile che verrà generato un messaggio
di errore nello strato EJB, mentre la propagazione/wrapping
delle eccezioni verso lo strato web, darà luogo
ad un messaggio anche in questo livello.
Per lo stesso errore si hanno quindi due messaggi corrispondenti
allo stesso evento: il maintainer della applicazione
si ritrova quindi due file di log da controllare e la
ridondanza di informazioni che ne deriva potrebbe rendere
meno semplice l'individuazione del problema.
Per questo motivo la propagazione delle eccezioni, unitamente
alla scelta del livello di log, sono aspetti molto importanti.
Data la vastità dell'argomento si parlerà
di questi temi in un articolo che verrà pubblicato
su MokaByte nei prossimi mesi.
Inizializzazione di log4j: il problema del punto di
inizio
Il problema principale relativo all'utilizzo di log4j
è capire dove e quando debba essere eseguita
la inizializzazione del framework: essendo questa una
operazione che deve essere eseguita una volta sola (successive
inizializzazioni possono portare a problemi se non al
blocco del sistema di log) e prima di ogni altra (per
poter monitorare tutte le altre operazioni del sistema)
è importante identificare il punto di inizio
della applicazione.
Questo rappresenta un po' il problema dell'uovo e della
gallina: fino a quando non ho inizializzato il logger
non posso monitorare l'insorgere di problemi, ma uno
dei problemi che si potrebbero verificare è proprio
l'impossibilità di eseguire la inizializzazione
del logger (ad esempio il file lo4j.properties o log4j.xml
non viene trovato).
Per questo motivo una soluzione che è divenuta
ormai uso comune (si veda i messaggi di log allo start
di JBoss) è la seguente:
-
eseguire sempre allo start della applicazione una
inizializzazione di default senza parametri tramite
il metodo senza parametri configure() della classe
BasicConfigurator; questa operazione dovrebbe poter
essere sempre eseguita tranne in casi di problemi
molto gravi. A questo punto l'applicazione dispone
di un logger di default con il quale effettuare il
controllo delle operazioni successive.
- Al
momento opportuno (poco dopo l'inizializzazione di
default) eseguire il caricamento, controllato dal
logger default, del file di properties e procedere
alla re-inizializzazione parametrica di log4j. Per
forzare il ri-caricamento delle proprietà di
configurazione si può utilizzare il metodo
resetConfiguration() come nel seguente pezzo di codice
try{
Properties LogProperties = new Properties();
// ricava il nome del file di properties
dal sistema
logPropertiesFilename= this.getProperty("logPropertiesFilename");
logger.debug("nome del file di
proprietà log4j="+logPropertiesFilename);
if (logPropertiesFilename == null ||
logPropertiesFilename.equals("")){
logger.info("Il logPropertiesFilename
è nullo si usa quello di default");
logPropertiesFilename =
"WEB-INF/log4j.properties";
}
else{
logger.info("Caricamento
delle proprietà di log dal file
"+logPropertiesFilename);
}
LogProperties.load(servletContext.getResourceAsStream(logPropertiesFilename));
logger.debug("LogProperties caricato
"+LogProperties);
org.apache.log4j.LogManager.resetConfiguration();
PropertyConfigurator.configure(LogProperties);
logger = Logger.getLogger(Log4jInitBean.class.getName());
logger.info("----------------------------------------------------------");
logger.info(" Inizializzazione
Log4J effettuata ");
logger.info("----------------------------------------------------------");
}
catch(Exception e){
String msg ="Exception durante
la procedura di riconfigurazione di Log4j:"+e;
logger.error(msg);
throw new MokaByteInitException(msg);
}
- Nel
caso in cui qualcosa non vada per il verso giusto
durante il caricamento del file di proprietà
il sistema potrà comunque usare il logger di
default precedentemente inizializzato.
Il
punto fondamentale a questo punto è quindi capire
dove e quando l'applicazione esegue lo start, in modo
da inserire tale procedura.
Il punto di inizio varia profondamente a seconda che
si tratti di una applicazione standalone, una applicazione
web o una applicazione EJB.
Per il primo caso la soluzione è piuttosto semplice:
lo start point è nel metodo main() della classe
principale, per cui qui possono essere eseguite senza
particolari problemi le operazioni di configurazione
default e parametrica.
Start
point in una web application e inizializzazione in Tomcat
Nel caso di una applicazione web le cose sono un po'
più complesse, dato che non si può avere
la certezza di quale classe venga eseguita per prima:
la prima servlet della servlet chain? Il primo listener?
Un primo POJO? Quasi mai si ha la certezza, specie se
l'applicazione viene poi modificata nel tempo o se viene
eseguita in web container differenti.
Dopo diverse soluzioni che ho personalmente provato
ed implementato, ho adottato una soluzione standard
per tutte le applicazioni sviluppate dal nostro gruppo
di lavoro: il nostro framework web è basato su
una serie di librerie web all'interno delle quali vi
è un listener di contesto.
Tale listener attiva fra le altre cose un bean per la
inizializzazione di default del log4j.
Quando un utente si collega a una applicazione il listener
di contesto è la prima cosa che viene eseguita
dal container, per cui si può essere sufficientemente
sicuri che sia effettuata la inizializzazione del logger
di default.
Successivamente il sistema prevede una serie di inizializzatori
configurabili tramite file XML (init-beans.xml): il
concetto è simile alle Action di Struts, tranne
che qui i vari bean di inizializzazione sono definiti
ed eseguiti alla creazione del context o della sessione
utente. Per questo è stato definito un bean apposito
per la riconfigurazione di log4j tramite file di proprietà.
Ecco la definizione del bean log4jInitBean inserito
all'interno
<init-bean
name="log4jInitBean"
classname="com.mokabyte.mvc.beans.commons.Log4jInitBean"
scope="context">
<init-params>
<init-param>
<param-name>logPropertiesFilename</param-name>
<param-value>WEB-INF/log4j.properties</param-value>
</init-param>
</init-params>
</init-bean>
Commentando
o scommentando tale porzione di codice si può
in ogni momento attivare il configuratore di log allo
start della applicazione web. Dato che Tomcat non possiede
un sistema di inizializzazione automatica del logger,
in questo caso si terrà attivo tale inizializzatore.
In questo modo ogni applicazione web avrà un
suo file di configurazione autocontenuto all'interno
del file JAR di deploy: questa organizzazione è
molto comoda se si desidera tenere l'organizzazione
il più pulita possibile.
Inizializzazione
in JBoss
JBoss si comporta in modo diverso rispetto al container
web Tomcat. Sia che si tratti di una applicazione web
che di una EJB, l'application server contiene al suo
interno un unico logger configurabile in modo unico
tramite il file log4j.xml.
Per questo motivo se si tentasse di effettuare una inizializzazione
personalizzata tramite il BasicConfigurator si otterrebbe
un errore non bloccante per il server ma bloccante per
la produzione dei messaggi (in pratica più nessun
messaggio di log apparirebbe in console o su file).
All'interno del file log4j.xml si dovranno quindi inserire
tutte le istruzioni di configurazione di tutte le applicazioni
cosa che potrebbe rendere le cose un po' meno pulite
ed isolate.
JBoss però riesce a sopperire a questa mancanza
di pulizia con una potente funzionalità: il log4j.xml
viene infatti monitorato dal container, per cui ogni
modifica effettuata al file viene ricaricata in tempo
reale (o a intervalli regolari a seconda della configurazione),
permettendo di apportare modifiche ai log senza la necessità
di ri-deployare o restartare l'applicazione (come invece
si rende necessario con il sistema offerto da Tomcat).
Da notare infine che l'utilizzo del sistema componibile
di bean di inizializzazione basati sul file init-bens.xml
visto poco sopra, permette di deployare una applicazione
web indifferentemente su Tomcat o JBoss semplicemente
commentando o commentando una piccola porzione di XML
senza dover riscrivere o ricompilare l'applicazione.
Conclusione
Il framework log4j è certamente uno dei prodotti
di casa Jakarta di minor rilievo se paragonato ad oggetti
come Struts, Tomcat, e MyFaces.
Questo prodotto però, per la sua semplicità
d'uso e per la potenza delle funzionalità offerte,
è diventato di fatto il sistema di log più
utilizzato in ambito Java (e non solo grazie a porting
per linguaggi come C e C#) soppiantando quello proposto
da Sun ed introdotto con il JDK 1.4.
Un uso corretto di questo strumento permette in ogni
momento di tenere sotto controllo il comportamento di
una applicazione, anche realizzando retro controlli
di sistemi già in produzione nel caso si verifichino
problemi imprevisti.
Bibliografia
[log4j] - Home page del progetto log4j, http://jakarta.apache.org/log4j
[log4jdocs] - Documentazione ufficiale di log4j
http://jakarta.apache.org/log4j/docs/documentation.html
|