La journalisation - Principes et implementations Java
Log4J
Log4j est une API de journalisation très répandue dans le monde Java. Il s'agit d'un système hautement configurable, que ce soit au niveau de ce qui doit être enregistré ou de la destination des enregistrements (serveur de logging, fichiers tournants, etc.).
Utilisation
Log4j est un projet open source distribué sous la licence Apache Software. Cette API permet aux développeurs d'utiliser et de paramétrer les traitements de logs. Il est possible de fournir les paramètres de l'outil dans un fichier de configuration. Log4j est compatible avec le JDK 1.1. et supérieur. L'avantage de configurer les traitements de log4j dans un fichier externe (properties ou xml) est de pouvoir modifier la configuration des traitements de logging sans avoir à modifier le code source.
Log4j gère plusieurs niveaux de gravités et les messages peuvent être envoyés dans plusieurs flux. Ces flux sont appelés des appenders :
- un fichier sur disque
- le journal des evénements de Windows
- une connexion TCP/IP
- une base de données
- un canal JMS
- etc.
Installation:
Avant tout, il est nécessaire d'ajouter l'API Log4j au chemin de compilation.
La dernière version de Log4j est téléchargeable à l'url http://jakarta.apache.org/log4j
Elle inclus les binaires (les fichiers .class), les sources complètes et la documentation.
Log4j utilise trois composants principaux :
- Logger : ils permettent de gérer les logs
- Appenders : ils représentent les flux qui vont recevoir les messages de log
- Layouts : ils permettent de formatter le contenu des messages du fichier de log
Dans les version antérieures à la 1.2, l'entité utilisée pour la journalisation était la Category. Aujourd'hui, elle a été remplacée par la classe Logger.
Loggers
Les loggers déterminent si un message doit être envoyé dans le ou les logs. Elles sont représentées par la classe org.apache.log4j.Logger. Chaque logger possède un nom qui est sensible à la casse. Pour créer un logger, il suffit d'instancier un objet Logger. Pour réaliser cette instanciation, la classe Logger fournie une méthode statique getLogger() qui attend en paramètre le nom du logger. Si un logger existe déjà avec le nom fourni, alors la méthode getLogger() renvoie une instance sur ce logger.
Il est pratique de fournir le nom complet de la classe comme nom du logger dans laquelle il est instancié mais ce n'est pas obligation. Il est ainsi possible de créer une hiérarchie spécifique différente de celle de l'application, par exemple basée sur des aspects fonctionnels. L'inconvénient d'associer le nom de la classe au nom du logger est qu'il faut instancier un objet logger dans chaque classe. Le plus pratique est de déclarer cet objet static.
Exemple :
public class Classe1 {
static Logger logger = Logger.getLogger(Classe1.class.getName());
...
}
La méthode log(Priority, Object) permet de demander l'envoie dans le log du message avec la priorité fournie. Il existe en plus une méthode qui permet de demander l'envoi d'un message dans le fichier de log pour chaque priorité : trace,(Object), debug(Object), info(Object), warn(Object), error(Object), fatal(Object).
La demande est traitée en fonction de la hiérarchie du logger et de la priorité du message. Pour éviter d'éventuels traitements pour créer le message, il est possible d'utiliser la méthode isEnabledFor(Priority) pour savoir si le logger prend en compte la priorité ou non.
Exemple :
import org.apache.log4j.*;
public class Example {
static Logger logger = Logger.getLogger(Example.class.getName());
public static void main(String[] args) {
int i=1;
int[] occurence={10,20,30};
BasicConfigurator.configure();
logger.setPriority(Priority.WARN) ;
logger.warn("message de test");
if(logger.isEnabledFor(Priority.INFO)) {
System.out.println("traitement du message de priorité INFO");
logger.info("La valeur de l'occurence "+i+" = " + String.valueOf(occurence[i]));
}
if(logger.isEnabledFor(Priority.WARN)) {
System.out.println("traitement du message de priorité WARN");
loggerwarn("La valeur de l'occurence "+i+" = " + String.valueOf(occurence[i]));
}
}
}
Le résultat qui apparait est donc:
0 [main] WARN Example - message de test
traitement du message de priorit_ WARN
50 [main] WARN Example - La valeur de l'occurence 1 = 20
Le nom du logger permet de la placer dans une hierarchie dont la racine est un logger spécial nommée rootLogger qui est créé par défaut sans nom. La classe Logger possède une classe statique getRootLogger() pour obtenir le logger racine. La hiérarchie des noms est établie grace à la notation par point comme pour les packages.
Exemple :
soit trois loggers
logger1 nommée "umlv"
logger2 nommée "umlv.ig2000"
logger3 nommée "umlv.ig2000.ir"
Alors, le logger3 est fils de logger2, lui même fils de logger1. Cette relation hiérarchique est importante car la configuration établie pour un logger est automatiquement propagée par défaut aux loggers enfants. L'ordre de la création des loggers de la hiérarchie ne doit pas obligatoirement respecter l'ordre de la hiérarchie. Celle ci est constituée au fur et à mesure de la création des loggers.
Priorités
Log4j gére des priorités, ou Level, pour permettre au logger de déterminer si le message sera envoyé dans le fichier de log. Il existe six priorités qui possèdent un ordre hiérarchique croissant :
- TRACE
- DEBUG
- INFO
- WARN
- ERROR
- FATAL
La classe org.apache.log4j.Level encapsule ces priorités. Chaque logger est associé à une priorité qui peut être changée dynamiquement. Le logger détermine si un message doit être envoyé dans le fichier de log en comparant sa priorité avec la priorité du message. Si celle est supérieure ou égale à la priorité du logger, alors le message est envoyé dans le fichier log. La méthode setLevel() de la classe Logger permet de préciser la priorité du logger. Si aucune priorité n'est donnée au logger, il "hérite" de la priorité du premier logger en remontant dans la hiérarchie dont la priorité est renseignée.
Exemple :
soit trois loggers
rootLogger associée à la priorité INFO
logger1 nommé "umlv" sans priorité particulière
logger2 nommé "umlv.ig2000" associée à la priorité ERROR
logger3 nommé "umlv.ig2000.ir" sans priorité particulière
Une demande avec la priorité DEBUG sur logger2 n'est pas traitée car la priorité INFO héritée est supérieure à DEBUG.
Une demande avec la priorité WARN sur logger2 est traitée car la priorité INFO héritée est inférieure à WARN .
Une demande avec la priorité DEBUG sur logger3 n'est pas traitée car la priorité ERROR héritée est supérieure à DEBUG.
Une demande avec la priorité FATAL sur logger3 est traitée car la priorité ERROR héritée est inférieure à FATAL.
En fait dans l'exemple, aucune demande avec la priorité DEBUG ne sera traitée.
Au niveau applicatif, il est possible d'interdire le traitement d'une priorité et de celle inférieure en utilisant le code suivant : Logger.getDefaultHierarchy().setThreshold(Level level). Il faut fournir la priorité à la méthode setThreshold().
Il est possible d'annuler ce traitement dynamiquement en positionnement la propriété système : log4j.disableOverride.
Appenders
L'interface org.apache.log4j.Appender désigne un flux qui représente le fichier de log et se charge de l'envoie de message formatté ce flux. Le formattage proprement dit est réalisé par un objet de type Layout. Ce layout peut être fourni dans le constructeur adapté ou par la méthode setLayout().
Un logger peut posséder plusieurs appenders. Si le logger décide de traiter la demande de message, le message est envoyés à chacun des appenders. Pour ajouter un appender à un logger, il suffit d'utiliser la méthode addAppender() qui attend en paramètre un objet de type Appender.
L'interface Appender est directement implémentée par la classe abstraite AppenderSkeleton. Cette classe est la classe mère de toutes les classes fournies avec log4j pour représenter un type de log :
- org.apache.log4j.jdbc.JDBCAppender : Effectue la journalisation vers une base de données
- org.apache.log4j.net.JMSAppender : Utilise JMS pour journaliser les événements
- org.apache.log4j.nt.NTEventLogAppender : Journalise via le journal des événements de Windows (NT/2000/XP)
- org.apache.log4j.lf5.LF5Appender : Journalise les événements vers une console basée sur Swing, celle-ci permet de trier ou de filtrer les événements
- org.apache.log4j.varia.NullAppender : N'effectue aucune journalisation
- org.apache.log4j.net.SMTPAppender : Envoie un email lorsque certains événements surviennent (à ne pas activer avec un niveau de journalisation DEBUG...)
- org.apache.log4j.net.SyslogAppender : Journalise les événements vers un daemon syslog (distant ou non)
- org.apache.log4j.net.SocketAppender : Envoie les événements de journalisation vers un serveur de journalisation
- org.apache.log4j.net.TelnetAppender : Journalise les événements vers un socket auquel on peut se connecter via telnet
- org.apache.log4j.ConsoleAppender : Effectue la journalisation vers la console
- org.apache.log4j.FileAppender : Journalise dans un fichier
- org.apache.log4j.DailyRollingFileAppender : Journalise dans un fichier qui tourne régulièrement (pas forcément tous les jours)
- org.apache.log4j.RollingFileAppender : Journalise dans un fichier, celui-ci est renommé lorsqu'il atteint une certaine taille et la journalisation reprend dans un nouveau fichier
Notez cependant qu'il est possible d'affecter un niveau seuil (threshold) à tous les Appenders étendant la classe org.apache.log4j.AppenderSkeleton (ce qui est le cas de tous les Appenders fournis avec log4j). Dans ce cas, un message n'est journalisé par un Appender donné que si son niveau est supérieur ou égal à celui du Logger et qu'il est supérieur ou égal au seuil de l'Appender considéré. Tout comme les priorités, les appenders d'un logger père sont héritées par les loggers fils. Pour éviter cette héritage par défaut, il faut mettre le champ additivity à false en utilisant la méthode setAdditivity() de la classe Logger.
Layouts
Ces composants représentés par la classe org.apache.log4j.Layout permettent de définir le format du fichier de log. Un layout est associé à un appender lors de son instanciation.
Il existe plusieurs layouts définis par log4j :
- org.apache.log4j.SimpleLayout : Comme son nom l'indique, il s'agit du Layoutle plus simple, les événements journalisés ont le format Niveau - Message[Retour à la ligne]
- org.apache.log4j.PatternLayout : Layout le plus flexible, le format du message est spécifié par un motif (pattern) composé de texte et de séquences d'échappement indiquant les informations à afficher. Reportez vous à la JavaDoc pour une description complète des séquences d'échappement existantes. Par défaut, les événements sont journalisés au format Message[Retour à la ligne] ;
- org.apache.log4j.XMLLayout : Comme son nom l'indique, formate les données de l'événement de journalisation en XML (à utiliser en conjugaison avec un Appender de la famille des FileAppenders) ;
- org.apache.log4j.HTMLLayout : Les événements sont journalisés au format HTML. Chaque nouvelle session de journalisation (réinitialisation de Log4j) donne lieu à un document HTML complet (avec préambule DOCTYPE,...).
Le PatternLayout permet de préciser le format du fichier de log grâce à des motifs qui sont dynamiquement remplacés par leur valeur à l'exécution. Les motifs commencent par un caractère % suivi d'une lettre :
Motif | Role |
%c | le nom du logger qui a émis le message |
%C | le nom de la classe qui a émis le message |
%d | le timestamp de l'émission du message |
%m | le message |
%n | un retour chariot |
%p | la priorité du message |
%r | le nombre de milliseconde écoulé entre le lancement de l'application et l'émission du message |
%t | le nom du thread |
%x | NDC du thread |
%% | le caractère % |
Il est possible de préciser le formattage de chaque motif grâce à un alignement et/ou une troncature. Dans le tableau ci dessous, la caractère # représente une des lettres du tableau ci dessus, n représente un nombre de caractères.
Motif | Role |
%# | aucun formattage (par défaut) |
%n# | alignement à droite, des blancs sont ajoutés si la taille du motif est inférieure à n caractères |
%-n# | alignement à gauche, des blanc sont ajoutés si la taille du motif est inférieure à n caractères |
%.n | tronque le motif si il est supérieur à n caractères |
%-n.n# | alignement à gauche, taille du motif obligatoirement de n caractères (troncature ou complément avec des blancs) |
Configuration
Le fichier de configuration de permet basiquement de définir le niveau de gravité minimum à traiter, les flux de sorties (Appender) et le format des messages (Layout). Ce fichier peut utiliser deux formats :
- un fichier .properties utilisant des paires clé/valeur
- un fichier XML
La seconde solution offre plus de possibilités et, de par son format, est plus structurée ce qui permet de s'y retrouver plus facilement. (Une troisième méthode est de configurer le système de journalisation par programmation)
Pour déterminer le fichier de configuration ainsi que son format, log4j utilise le mécanisme de recherche suivant. La recherche des fichiers s'effectue via le ClassLoader, les fichiers de configuration doivent donc se trouver sur le classpath.
- Si la propriété système log4j.defaultInitOverride est définie à autre chose que false, le mécanisme de recherche n'est pas exécuté (à utiliser si vous configurez le système par programmation)
- Vous pouvez définir les propriétés système log4j.configuration et log4j.configuratorClass qui indiquent respectivement le fichier à utiliser et la classe servant à lire et à charger les propriétés (elle doit implémenter l'interface org.apache.log4j.spi.Configurator)
- En l'absence de propriété système indiquant le fichier à utiliser, log4j recherche un fichier nommé log4j.xml, si celui-ci n'existe pas, il tente d'obtenir le fichier log4j.properties. Quel que soit le cas (fichier de configuration personnalisé ou mécanisme de recherche), si le fichier est introuvable, le mécanisme est interrompu et log4j n'est pas configuré
- Si aucun Configurator n'est spécifié et que le fichier termine par l'extension .xml, log4j utilise un DomConfigurator (configuration XML), si l'instanciation est impossible (classe inexistante, qui n'implémente pas Configurator, etc.), le mécanisme est interrompu et log4j n'est pas configuré. Si aucune classe n'est précisée et que le fichier n'est pas un fichier XML, un PropertiyConfigurator (configuration classique) est utilisé.
La méthode la plus simple consiste donc à laisser faire log4j en plaçant un fichier log4j.xml dans un répertoire situé sur le classpath.
Quel que soit le format de la configuration, le procédé est toujours le même, seule la forme change. Il faut tout d'abord configurer les différents Appenders. Ensuite vient la configuration des Loggers. Rien n'oblige à configurer tous les Loggers de façon individuelle, il faut utiliser les mécanismes d'héritage de niveau et d'Appender. Le minimum requis pour que Log4j fonctionne correctement est d'attribuer un Appender correctement configuré au Logger racine. Si log4j n'est pas correctement configuré, cela n'empêchera pas l'application de fonctionner, simplement, les messages de journalisation peuvent être perdus.
Pour faciliter la configuration de log4j, l'API fournie plusieurs classes qui implémentent l'interface Configurator. La classe BasicConfgurator est le classe mère des classes PropertyConfigurator (pour la configuration via un fichier de propriétés) et DOMConfigurator (pour la configuration via un fichier XML).
La classe BasicConfigurator permet de configurer le logger Root avec des valeurs par défaut. L'appel à la méthode configure() ajoute au logger root la priorité DEBUG et un ConsoleAppender vers la sortie standard (System.out) associé à un PatternLayout (TTCC_CONVERSION_PATTERN qui est une constante défnie dans la classe PatternLayout).
Exemple :
import org.apache.log4j.*;
public class Example {
static Logger logger = Logger.getLogger(Example.class.getName()) ;
public static void main(String[] args) {
BasicConfigurator.configure();
logger.info("My message");
}
}
Le résultat qui apparait est donc:
0 [main] INFO Example - My message
La classe PropertyConfigurator permet de configurer log4j à partir d'un fichier de propriétés ce qui évite la recompilation de classes pour modifier la configuration. * La méthode configure() qui attend un nom de fichier permet de charger la configuration.
Exemple : le fichier mylogging.properties de configuration de log4j
# Affecte au logger root la priorité DEBUG et un appender nommé CONSOLE_APP
log4j.rootLogger=DEBUG, CONSOLE_APP
# le appender CONSOL_APP est associé à la console
log4j.appender.CONSOLE_APP=org.apache.log4j.ConsoleAppender
# CONSOLE_APP utilise un PatternLayout qui affiche : le nom du thread, la priorité,
# le nom du logger et le message
log4j.appender.CONSOLE_APP.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE_APP.layout.ConversionPattern= [%t] %p %c - %m%n
Exemple :
import org.apache.log4j.*;
public class Example {
static Logger logger = Logger.getLogger(Example.class.getName()) ;
public static void main(String[] args) {
PropertyConfigurator.configure("mylogging.properties");
logger.info("My message");
}
}
Le résultat qui apparait est donc:
[main] INFO Example - My message
La classe DOMConfigurator permet de configurer log4j à partir d'un fichier XML ce qui évite aussi la recompilation de classes pour modifier la configuration. La méthode configure() qui attend un nom de fichier permet de charger la configuration. Cette méthode nécessite un parser XML de type DOM compatible avec l'API JAXP.
Exemple : le fichier mylogging.xml de configuration de log4j
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<appender name="CONSOLE_APP" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="[%t] %p %c - %m%n"/>
</layout>
</appender>
<root>
<priority value ="DEBUG" />
<appender-ref ref="CONSOLE_APP" />
</root>
</log4j:configuration>
Exemple :
import org.apache.log4j.*;
import org.apache.log4j.xml.*;
public class Example {
static Logger logger = Logger.getLogger(Example.class.getName()) ;
public static void main(String[] args) {
try {
DOMConfigurator.configure("mylogging.xml");
} catch (Exception e) {
e.printStackTrace();
}
logger.info("My message");
}
}
Le résultat qui apparait est donc:
[main] INFO Example - My message