Introduction▲
La plupart des projets concernant des applications de moyenne ou de grande taille utilisent une API de journalisation (ou logging en anglais), bien souvent réalisée en interne. Le projet européen SEMPER (http://www.semper.org/) correspond parfaitement à cette assertion. C'est en effet durant son développement qu'a été imaginée la bibliothèque log4j, destinée à la gestion des journaux pour les applications Java. Ce paquetage est aujourd'hui disponible sous licence Apache et jouit d'une excellente réputation. À tel point qu'il se trouve même disponible pour les langages C, C++, C#, Perl, Python, Ruby et Eiffel.
Les outils de journalisation offrent de nombreux avantages au programmeur. La plus évidente concerne bien évidemment la mise au point du code. On pourra ainsi bénéficier d'une solution, certes simple, mais néanmoins suffisante pour mettre au point des applications pour lesquelles un dévermineur ne peut être mis en œuvre. Les logiciels multithreads ou les architectures distribuées imposent l'exploitation d'outils comme log4j. Mais leur utilisation va beaucoup plus loin. Bien utilisés, les journaux offrent un historique pertinent du cycle de vie des applications. Sans aucune intervention de la part de l'utilisateur ou de l'auteur, tous les messages seront enregistrés. Tout cela contraste nettement avec les dévermineurs qui nécessitent des connaissances techniques poussées et ne peuvent offrir qu'un aperçu à un instant précis de l'état du programme. Rappelons également que les journaux peuvent être conservés pour étude ultérieure. Vous pourrez même demander à vos utilisateurs de vous les transmettre avec leurs rapports de bogues pour accélérer et faciliter votre travail.
La plus grande difficulté réside dans l'insertion des instructions de journalisation. Vous devrez les placer avec soin pour que les renseignements soient suffisamment précis sans verser dans le redondant ou l'inutile. Il s'agit de l'un des deux inconvénients inhérents à ce genre de bibliothèques. L'autre concerne tout simplement la dégradation possible des performances. Quelques entrées mal placées, par exemple dans un algorithme sensible (tri, recherche…), pourront entraîner de fâcheux problèmes. Le sujet des performances de log4j sera examiné en détail un peu plus loin.
I. L'API log4j▲
La bibliothèque log4j met trois sortes de composants à disposition du programmeur : les loggers, les appenders et les layouts. Les premiers permettent d'écrire les messages, les deuxièmes servent à sélectionner la destination des messages, et les derniers à mettre en forme les messages.
À ce stade de notre apprentissage, l'avantage d'une API de journalisation sur le traditionnel System.out.println() ne vous apparaît peut-être pas clairement. Il est pourtant simple : log4j vous permet d'activer ou de désactiver certains messages en fonction de vos besoins. Pour ce faire, l'API repose sur la notion de hiérarchie des loggers. Cette dernière fonctionne exactement sur le même principe que les paquetages Java. Le caractère point sert donc de séparateur entre les parents et les enfants dans la hiérarchie. Le principe de la filiation est extrêmement important dans log4j ainsi que vous le verrez bientôt. Par exemple, le logger « org » sera le parent de « org.progx » et l'ancêtre de « org.progx.BackupDavis ». Contrairement aux arbres classiques, comme les arbres XML, il est possible de créer un parent après avoir créé son enfant. L'ordre de création des loggers est donc sans aucune importance. Au sommet de cette hiérarchie se trouve le logger racine qui est un logger par défaut, dont on ne connaît pas le nom et que l'on peut utiliser en invoquant la méthode Logger.getRootLogger(). Mais puisque vous désirerez sûrement manipuler les vôtres, utilisez la méthode Logger.getLogger(). Celle-ci fonctionne de deux manières différentes et exclusives. La chaîne de caractères passée en paramètre vous renverra en effet le logger portant le nom correspondant ou le créera s'il n'existe pas. Dans l'exemple suivant, les instances logger1 et logger2 sont finalement identiques, car elles constituent des références vers le même objet.
Logger logger1 =
Logger.getLogger
(
"org.progx"
);
Logger logger2 =
Logger.getLogger
(
"org.progx.BackupDavis"
);
...
logger1 =
Logger.getLogger
(
"org.progx.BackupDavis"
);
Cette technique vous permet d'initialiser vos loggers en un point de votre application et de les utiliser autre part sans avoir à vous soucier d'en conserver des références ou de les passer en paramètres à vos divers objets et méthodes. Les loggers proposent six méthodes pour écrire vos messages : trace, debug, info, warn, error, fatal et log. Les six premières ne constituent en fait que des alias pour la dernière qui permet d'indiquer, en plus du message, le niveau d'erreur. Ainsi les deux lignes suivantes sont identiques d'un point de vue logique :
logger.info
(
"Démarrage"
);
logger.log
(
Level.INFO, "Démarrage"
);
Le niveau d'erreur est indiqué par une instance de la classe Level ce qui vous permet de créer les vôtres. Vous l'aurez compris, six sont disponibles par défaut : TRACE, DEBUG, INFO, WARN, ERROR et FATAL (par ordre croissant de priorité). Si un niveau peut être assigné à un message, il peut également l'être à un Logger. Dans ce cas, tous les niveaux de priorité inférieure seront ignorés. Dans le cas suivant, le message sera ignoré :
Logger logger =
Logger.getLogger
(
"org.progx"
);
logger.setLevel
(
Level.INFO);
logger.debug
(
"Message ignoré"
);
En paramétrant correctement les niveaux des loggers vous pourrez aisément rendre vos journaux plus ou moins précis. Cette nouvelle fonctionnalité soulève un point particulier, le cas des loggers ne possédant pas de niveau spécifique. Plutôt que de leur en fournir un arbitrairement, log4j recherche le plus proche parent en possédant un et leur transmet. Dans le pire des cas, le niveau attribué sera celui de la racine qui est DEBUG par défaut.
Prenons un exemple simple :
Logger log1 =
Logger.getLogger
(
"org"
);
log1.setLevel
(
Level.INFO);
Logger log2 =
Logger.getLogger
(
"org.progx"
);
Logger log3 =
Logger.getLogger
(
"org.progx.BackupDavis"
);
log3.setLevel
(
Level.WARN);
Dans ce cas, le logger log1 possède un niveau d'erreur de type INFO, car on lui attribue explicitement. Avant exécution de l'instruction setLevel(), son niveau est celui de la racine donc DEBUG.
Le deuxième logger ne reçoit pas de niveau explicite et prend donc celui de son plus proche parent qui est log1. Son niveau sera donc INFO.
Enfin, log3, bien qu'enfant de log2, utilise explicitement le niveau WARN.
Soyez très attentifs au niveau de vos loggers car l'enregistrement des messages en dépendra directement. Imaginez que les trois loggers présentés ci-dessus soient éparpillés dans le code. À première vue, un développeur pourra penser que les messages DEBUG de log2 seront enregistrés. Malheureusement ce ne sera pas le cas puisque la priorité de DEBUG est inférieure à INFO dont log2 hérite depuis log1. Soyez donc très vigilants lorsque vous ne spécifiez pas un niveau explicite à chaque logger.
Pour contourner ce problème, nous vous conseillons de réunir leurs déclarations au même endroit dans le code (pour une même hiérarchie) ou de vous intéresser aux fichiers de configuration.
Si vous tentez d'exécuter ces quelques exemples, vous recevrez un message d'erreur semblable :
log4j:WARN No appenders could be found for logger (org.progx).
log4j:WARN Please initialize the log4j system properly.
Cela signifie que nous n'avons pas configuré nos loggers et qu'ils ne possèdent donc aucune cible pour les messages. C'est ici qu'interviennent les appenders et les layouts.
II. Cibles et format des messages▲
Un appender représente donc la cible d'un message, c'est-à-dire l'endroit où celui-ci sera physiquement affiché ou stocké. log4j vous propose ainsi des appenders pour la console, les fichiers, les sockets, le gestionnaire d'événements Windows NT, le démon Unix syslog ou encore les composants graphiques. Chaque logger dispose de la méthode addAppender() nous permettant de lui affecter une nouvelle cible. Une fois de plus, la hiérarchie de nos loggers joue un rôle très important. En effet, chaque message de journalisation sera transmis aux cibles du logger courant ainsi qu'aux cibles de tous ses parents. En affectant par exemple une cible console au logger racine et une cible fichier au logger org.progx aura les conséquences suivantes : les messages du logger org seront affichés en console et les messages de org.progx (et de tous ses enfants) seront affichés en console et enregistrés dans un fichier.
Vous pouvez néanmoins prévenir ce fonctionnement en exécutant setAdditivity(false) sur le logger concerné. Attention toutefois, car ceci brisera la chaîne de délégation des appenders :
Logger.getRootLogger
(
).addAppender
(
new
ConsoleAppender
(
));
Logger log1 =
Logger.getLogger
(
"org"
);
log1.setAdditivity
(
false
);
log1.addAppender
(
new
FileAppender
(
new
SimpleLayout
(
), "progx.log"
));
Logger log2 =
Logger.getLogger
(
"org.progx"
);
Dans cet exemple, les loggers org et org.progx utilisent une cible de type fichier. Aucun d'entre eux ne pourra bénéficier de la cible console affectée à la racine. Et si les différentes cibles offertes par log4j ne vous suffisent pas, vous pourrez en créer de nouvelles très facilement. Pouvoir personnaliser la destination des messages ne donne absolument aucune indication sur leur format. La lecture de l'exemple précédent aura sûrement confirmé vos soupçons : le formatage des messages incombe aux layouts.
La bibliothèque vous propose 4 layouts par défaut : HTMLLayout, SimpleLayout, PatternLayout et TTCCLayout. Le premier vous permet de générer des journaux au format HTML, le second affiche simplement le message et son niveau, le troisième sert à formater l'affichage d'une manière semblable au printf() du C et le dernier à afficher le contexte d'exécution du message. Le plus intéressant est indubitablement PatternLayout dont la souplesse saura combler toutes vos exigences :
Logger log =
Logger.getLogger
(
"org.progx"
);
PatternLayout layout =
new
PatternLayout
(
"%d %-5p %c - %F:%L - %m%n"
);
ConsoleAppender stdout =
new
ConsoleAppender
(
layout);
log.addAppender
(
stdout);
Le format défini dans cet exemple affiche l'heure et la date, le niveau d'erreur (aligné à gauche), le nom du logger, le nom du fichier, le numéro de la ligne de code correspondante et enfin le message lui-même. Le résultat apparaîtra ainsi dans votre console :
2003-44-29 04:44:32,211 DEBUG org.progx - exemple3.java:18 - Starting
2003-44-29 04:44:32,221 DEBUG org.progx - exemple3.java:20 - Exiting
Nous vous conseillons de consulter la documentation de la classe PatternLayout pour découvrir les nombreuses possibilités de ce layout. Vous y trouverez également quelques conseils très importants au sujet des performances. Par exemple, l'utilisation de la séquence d'échappement %L (pour afficher le numéro de la ligne de code) demandera d'importantes ressources.
III. Configuration dynamique▲
Vous l'aurez compris, les possibilités de log4j sont infinies. Malheureusement, la configuration « en dur » du système de journalisation ne se révèle pas très pratique pour le déploiement de l'application. Généralement, vous ne voudrez pas que l'utilisateur voie les messages de priorité inférieure à INFO ou WARN. Les concepteurs de log4j ont une fois de plus tout prévu grâce au système de configuration. Trois méthodes vous sont proposées par défaut pour configurer automatiquement les loggers : une configuration par défaut, une configuration par fichier au format clé=valeur et une configuration XML.
La première solution consiste à exécuter BasicConfiguration.configure(). Ceci aura pour effet d'attribuer une cible console au logger racine. Son utilité est donc très limitée, mais vous permet de mettre un logger en place très rapidement.
La solution la plus intéressante consiste donc à passer par des fichiers de propriétés, ce que nous permet la classe PropertyConfigurator. Le listing 1 vous présente un exemple de fichier de configuration.
Chaque logger peut être configuré directement depuis un tel fichier. Pour ce faire vous devrez utiliser le format suivant :
log4.logger.NOM=NIVEAU,appender1,appender2 ...
La seule exception concerne le logger racine dont la propriété correspondante est log4j.rootLogger. Une fois que vous aurez défini un logger, vous devrez spécifier les caractéristiques de ses différents appenders. Le principe est simple puisque vous devez tout simplement indiquer la classe de chaque appender puis donner une valeur à leurs attributs. Le format est donc :
log4j.appender.NOM=nom.complet.de.la.classe
Les attributs spécifiques de chaque cible sont définis par introspection. Ainsi, on peut indiquer le fichier journal au FileAppender grâce à la méthode setFile(). Nous devrons donc transposer ceci en log4j.appender.fichier.File. La propriété « File » sera convertie en « setFile() » à l'exécution. L'unique exception à cette règle est le layout. Quel que soit l'appender utilisé, vous devrez utiliser la propriété layout et définir le nom complet de la classe chargée du formatage. La règle de l'introspection s'applique néanmoins à cette dernière. C'est pourquoi la propriété ConversionPattern nous permet d'exécuter la méthode setConversionPattern() du PatternLayout. Le chargement du fichier de configuration sera effectué dans l'application : PropertyConfigurator.configure(« exemple4.properties »). Cette technique est la plus souple et la plus efficace pour le paramétrage de vos loggers. Nous vous invitons à consulter les sources accompagnant l'article pour étudier nos exemples.
Il est possible que l'utilisation de log4j influe sur les performances de votre application, même lorsque la journalisation est entièrement désactivée. Un message debug() ne sera évidemment pas exécuté si le logger possède une priorité supérieure. Néanmoins, le calcul des paramètres fournis à la méthode pourra lui entraîner une chute de performances. La documentation officielle de l'API conseille de vérifier l'état de la journalisation avant toute activité :
if
(
logger.isDebugEnabled
(
))
logger.debug
(
Math.PI *
Math.PI);
L'inconvénient de cette solution réside dans le fait que la méthode debug() va elle aussi exécuter le test isDebugEnabled(). Dans la plupart des cas, cette perte sera minime au regard de celle engendrée par les calculs inutiles ainsi évités. Le seul autre paramètre sur lequel vous pourrez influer concerne le format des messages si vous faites appel au PatternLayout. Comme nous l'avons déjà dit, certaines de ses fonctionnalités entraînent de coûteux calculs que vous devrez à tout prix éviter dans certaines situations. Nous vous conseillons une fois de plus de confier le paramétrage des layouts aux fichiers de configuration pour pouvoir modifier aisément le comportement du formatage des messages. Le reste dépend simplement de la nature de la cible et de la qualité de son implémentation.
log4j.rootLogger=DEBUG,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r %-5p [%t] %c %3x - %m%n
log4j.logger.org.progx=INFO,fichier
log4j.appender.fichier=org.apache.log4j.FileAppender
log4j.appender.fichier.File=exemple4.log
log4j.appender.fichier.layout=org.apache.log4j.PatternLayout
log4j.appender.fichier.layout.ConversionPattern=%d %-5p %c - %F:%-4L - %m%n