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 :


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 :


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 :

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 :

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 :

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 :

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.

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