Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Utilisation du package java.util.logging

Pourquoi utiliser une API de logging ? Utilisation de l'API Log4J2



Accès rapide :
Introduction
Produire des logs avec JUL
Acquisition d'un logger
Les différents niveaux de logs
Produire un log à un niveau donné
Configurer vos logs
Configurer vos logs par programmation
Définir un fichier de configuration
Attention aux problématiques de performances

Introduction

Nous allons donc commencer à nous initier à l'utilisation de la librairie JUL (pour java.util.logging). Vous l'aurez compris, grâce au nom du package qui la contient, cette API est proposée de base par le Java SE et ce depuis sa version 1.4. Il n'y a donc aucun JAR complémentaire à télécharger.

Produire des logs avec JUL

Pour produire des logs vous devez réaliser, à minima, deux étapes :

Acquisition d'un logger

Pour acquérir un logger, il faut utiliser la méthode statique Logger.getLogger. Bien entendu, la classe Logger doit être celle localisée dans le package java.util.logging : parfois d'autres packages peuvent aussi contenir une classe de même nom. La méthode d'acquisition accepte en paramètre une chaîne de caractères. Il s'agit du nom du logger : il est très important, car il servira ultérieurement à configurer ce logger. Vous pouvez mettre le nom qui vous convient. Dans l'exemple suivant, j'imagine être dans un composant logiciel « Compo1 » et j'ai donc nommé mon logger de la même manière : cela me permettra de facilement configurer les loggers de toutes les classes de ce composant logiciel.

un composant logiciel est une unité fonctionnelle qui peut regrouper une ou plusieurs classes Java. En Java, un composant logiciel est souvent associé à un package de votre application.
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
package fr.koor.samples.jul;

import java.util.logging.Logger;

public class TestJUL {

    // Récupération de notre logger.
    private static final Logger LOGGER = Logger.getLogger( "Compo1" );

    // Le point d'entrée du programme.
    public static void main( String [] args ) {

        // TODO: utiliser votre logger
        
    }
}
Fichier TestJUL.java : exemple d'instanciation d'un logger JUL

Vous auriez aussi pu nommer le logger avec le nom du package contenant la classe. Dans ce cas, remplacer la chaîne "Compo1" par l'expression TestJUL.class.getPackage().getName(), comme le montre l'exemple suivant.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
package fr.koor.samples.jul;

import java.util.logging.Logger;

public class TestJUL {

    // Récupération de notre logger.
    private static final Logger LOGGER = Logger.getLogger( TestJUL.class.getPackage().getName() );

    // Le point d'entrée du programme.
    public static void main( String [] args ) {

        // TODO: utiliser votre logger
        
    }
}
Fichier TestJUL.java : exemple d'instanciation d'un logger JUL grâce au nom du package courant
oui, c'est bien le moteur de réflexion Java qui est utilisé pour retrouver le nom du package contenant la classe.

Certains préfèrent encore mettre le nom de la classe comme nom de logger, cela permettra d'avoir un contrôle plus fin lors de la configuration. Mais en contre parie, la configuration sera plus complexe. Voici un exemple d'instanciation en utilisant le nom de la classe courante comme nom de logger.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
package fr.koor.samples.jul;

import java.util.logging.Logger;

public class TestJUL {

    // Récupération de notre logger.
    private static final Logger LOGGER = Logger.getLogger( TestJUL.class.getName() );

    // Le point d'entrée du programme.
    public static void main( String [] args ) {

        // TODO: utiliser votre logger
        
    }
}
Fichier TestJUL.java : exemple d'instanciation d'un logger JUL grâce au nom de la classe
Vous l'aurez remarqué, le logger est, à chaque fois, défini en tant que constante privée (private static final). Afin d'améliorer les performances de votre programme, il est important de respecter cette manière de faire. En tant que membre statique, l'instanciation du logger est sa configuration n'interviendront qu'une unique fois. De plus, le fait qu'il soit marqué comme étant statique garantira que la JVM pourra placer cette instance dans un metaspace (ou le perm dans les vieilles JVMs). L'activité du garbage collector en sera donc réduite. Je vous renvoie vers les chapitres dédiés au garbage collector pour de plus amples informations.

Il est à noter que vous utilisez une méthode statique de récupération du logger (la méthode Logger.getLogger()). Pourquoi n'utilise-t-on pas un constructeur ? La raison est simple, plusieurs classes peuvent partager le même logger. Dans ce cas, on doit toujours demander le même nom de logger et la première classe qui le demande va lancer son instanciation et sa configuration. Les autres appels retourneront cette instance. Voici un exemple de code mettant en évidence ce point.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
package fr.koor.samples.jul;

import java.util.logging.Logger;

public class TestJUL {

    private static final String PACKAGE_NAME = TestJUL.class.getPackage().getName();

    // Récupérations multiples de notre logger.
    private static final Logger LOGGER1 = Logger.getLogger( PACKAGE_NAME );
    private static final Logger LOGGER2 = Logger.getLogger( PACKAGE_NAME );
    
    // Le point d'entrée du programme.
    public static void main( String [] args ) {

        // On fait une comparaison de pointeurs pour bien vérifier que nous 
        // avons bien deux variables qui pointent sur la même instance.
        System.out.println( LOGGER1 == LOGGER2 );       // Affiche true !
        
    }
}
Pour un nom de logger, vous n'aurez qu'une unique instance.

Les différents niveaux de logs

Il nous faut maintenant bien comprendre les différents niveaux de logs avec l'API java.util.logging. Pour ce faire, nous allons commencer par regarder un extrait du contenu de la classe java.util.logging.Level.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
 100 
 101 
 102 
package java.util.logging;

// Les imports

public class Level implements java.io.Serializable {

    // ...
    
    /**
     * OFF is a special level that can be used to turn off logging.
     * This level is initialized to <CODE>Integer.MAX_VALUE</CODE>.
     */
    public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);

    /**
     * SEVERE is a message level indicating a serious failure.
     * <p>
     * In general SEVERE messages should describe events that are
     * of considerable importance and which will prevent normal
     * program execution.   They should be reasonably intelligible
     * to end users and to system administrators.
     * This level is initialized to <CODE>1000</CODE>.
     */
    public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);

    /**
     * WARNING is a message level indicating a potential problem.
     * <p>
     * In general WARNING messages should describe events that will
     * be of interest to end users or system managers, or which
     * indicate potential problems.
     * This level is initialized to <CODE>900</CODE>.
     */
    public static final Level WARNING = new Level("WARNING", 900, defaultBundle);

    /**
     * INFO is a message level for informational messages.
     * <p>
     * Typically INFO messages will be written to the console
     * or its equivalent.  So the INFO level should only be
     * used for reasonably significant messages that will
     * make sense to end users and system administrators.
     * This level is initialized to <CODE>800</CODE>.
     */
    public static final Level INFO = new Level("INFO", 800, defaultBundle);

    /**
     * CONFIG is a message level for static configuration messages.
     * <p>
     * CONFIG messages are intended to provide a variety of static
     * configuration information, to assist in debugging problems
     * that may be associated with particular configurations.
     * For example, CONFIG message might include the CPU type,
     * the graphics depth, the GUI look-and-feel, etc.
     * This level is initialized to <CODE>700</CODE>.
     */
    public static final Level CONFIG = new Level("CONFIG", 700, defaultBundle);

    /**
     * FINE is a message level providing tracing information.
     * <p>
     * All of FINE, FINER, and FINEST are intended for relatively
     * detailed tracing.  The exact meaning of the three levels will
     * vary between subsystems, but in general, FINEST should be used
     * for the most voluminous detailed output, FINER for somewhat
     * less detailed output, and FINE for the  lowest volume (and
     * most important) messages.
     * <p>
     * In general the FINE level should be used for information
     * that will be broadly interesting to developers who do not have
     * a specialized interest in the specific subsystem.
     * <p>
     * FINE messages might include things like minor (recoverable)
     * failures.  Issues indicating potential performance problems
     * are also worth logging as FINE.
     * This level is initialized to <CODE>500</CODE>.
     */
    public static final Level FINE = new Level("FINE", 500, defaultBundle);

    /**
     * FINER indicates a fairly detailed tracing message.
     * By default logging calls for entering, returning, or throwing
     * an exception are traced at this level.
     * This level is initialized to <CODE>400</CODE>.
     */
    public static final Level FINER = new Level("FINER", 400, defaultBundle);

    /**
     * FINEST indicates a highly detailed tracing message.
     * This level is initialized to <CODE>300</CODE>.
     */
    public static final Level FINEST = new Level("FINEST", 300, defaultBundle);

    /**
     * ALL indicates that all messages should be logged.
     * This level is initialized to <CODE>Integer.MIN_VALUE</CODE>.
     */
    public static final Level ALL = new Level("ALL", Integer.MIN_VALUE, defaultBundle);
    
    // ...

}
Un extrait de la classe java.util.logging.Level
peut-être certains d'entre vous se demandent pourquoi il s'agit d'une classe et nom pas d'un type énuméré ? La question est tout à fait légitime et j'aurais préféré un type énuméré ! Il est vrai, que la classe Level porte des méthodes, mais un type énuméré Java peut, lui aussi, avoir des méthodes. Mais pour rappel, l'API JUL est sortie avec Java SE 1.4 et les types énumérés sont officiellement apparus avec Java SE 5.0 (1.5.0, si vous préférez). Les développeurs de l'API n'ont donc pas souhaité, après coup, faire un changement dans l'API de JUL.

Comme vous le constatez, un niveau de log est notamment associé à une valeur numérique. Autoriser l'affichage des logs à un niveau donné active aussi tous les niveaux de logs ont une valeur numérique supérieure. Ainsi, si vous configurez les logs pour afficher à partir du niveau Level.INFO, vous couperez les niveaux inférieurs (Level.CONFIG, Level.FINE, Level.FINER et Level.FINEST), par contre vous activerez aussi tous les niveaux supérieurs (WARNING et SEVERE).

Si, pour un logger donnée vous le configurez au niveau Level.OFF (de valeur Integer.MAX_VALUE), aucun log sera produit, car tous les niveaux de log ont une valeur numérique inférieure. A l'opposé, si vous configurez votre logger au niveau Level.ALL (de valeur Integer.MIN_VALUE), tous les logs seront produits, car ces niveaux auront tous une valeur numérique supérieure.

Produire un log à un niveau donné

La première solution pour produire un log est d'utiliser la méthode log de votre logger. Voici un exemple d'utilisation de cette méthode.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
package fr.koor.samples.jul;

import java.util.logging.Level;
import java.util.logging.Logger;

public class TestJUL {

    // Récupération de notre logger.
    private static final Logger LOGGER = Logger.getLogger( TestJUL.class.getPackage().getName() );

    // Le point d'entrée du programme.
    public static void main( String [] args ) {

        // On produit un log de niveau informatif.
        LOGGER.log( Level.INFO, "Hello World with java.util.logging" );
        
    }
}
Exemple d'utilisation de la méthode Logger.log

Si vous lancez la classe, vous obtiendrez un résultat proche (la date sera différente) de cet affichage.

juil. 30, 2020 2:10:37 PM fr.koor.samples.jul.TestJUL0 main
INFOS: Hello World with java.util.logging

Une autre solution consiste à utiliser des méthodes de convenance qui renvoient les appels sur Logger.log. Ces méthodes sont nommées avec le niveau de log associé (Logger.info, Logger.severe...).

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
package fr.koor.samples.jul;

import java.util.logging.Logger;

public class TestJUL {

    // Récupération de notre logger.
    private static final Logger LOGGER = Logger.getLogger( TestJUL.class.getPackage().getName() );

    // Le point d'entrée du programme.
    public static void main( String [] args ) {

        LOGGER.info( "Hello World with java.util.logging" );
        
        try {
            int value = (int)( Math.random() * 2 );
            int result = 3 / value;
            LOGGER.info( "result == " + result );
        } catch( Exception exception ) {
            LOGGER.severe( "Mon message d'erreur : " + exception );
        }
        
    }
}
Exemple d'utilisation des méthodes Logger.info et Logger.severe

Voici le résultat produit dans le cas où l'exception est levée.

juil. 30, 2020 2:18:23 PM fr.koor.samples.jul.TestJUL0 main
INFOS: Hello World with java.util.logging
juil. 30, 2020 2:18:23 PM fr.koor.samples.jul.TestJUL0 main
GRAVE: Mon message d'erreur : java.lang.ArithmeticException: / by zero

Personnellement, j'ai une préférence pour la première solution : elle permet de mieux contrôler les performances de votre application. Je vais y revenir en fin de chapitre. La première solution permet aussi de générer des chaînes de caractères en y injectant des données stockées dans un tableau. Voici un exemple d'utilisation de cette possibilité.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
package fr.koor.samples.jul;

import java.util.logging.Level;
import java.util.logging.Logger;

public class TestJUL {

    // Récupération de notre logger.
    private static final Logger LOGGER = Logger.getLogger( TestJUL.class.getPackage().getName() );

    // Le point d'entrée du programme.
    public static void main( String [] args ) {

        LOGGER.info( "Hello World with java.util.logging" );
        
        try {
            int value = (int)( Math.random() * 2 );
            int result = 3 / value;
            Object [] data = { value, result };
            LOGGER.log( Level.INFO, "value == {0} - result == {1}", data );
        } catch( Exception exception ) {
            LOGGER.severe( "Mon message d'erreur : " + exception );
        }
        
    }
}
Exemple d'injection de données dans un log
les séquences {0} et {1} permettent de sélectionner, dans le tableau, la donnée à injecter à partir de sa position (basée à partir de 0).

Et voici le résultat produit en l'absence d'exception.

juil. 30, 2020 3:01:19 PM fr.koor.samples.jul.TestJUL0 main
INFOS: Hello World with java.util.logging
juil. 30, 2020 3:01:19 PM fr.koor.samples.jul.TestJUL0 main
INFOS: value == 1 - result == 3
il n'est pas possible d'utiliser cette possibilité avec Logger.info, Logger.severe...

Configurer vos logs

Maintenant que vous savez produire des logs, il est temps de regarder comment contrôler quels sont les logs qui doivent être conservés et ceux qui ne le doivent pas. Pour ce faire, il faut configurer le moteur de log. Deux possibilités sont offertes : soit configurer le moteur par programmation, soit le configurer via un fichier de configuration.

Configurer vos logs par programmation

Le mieux, pour définir votre configuration est d'utiliser le bloc statique de votre classe principale (celle qui porte la méthode main). Le bloc statique est exécuté par un « ClassLoader » au chargement de la classe. Ainsi, la configuration sera définie avant de rentrer dans la méthode main.

Dans l'exemple suivant, on configure le logger pour qu'il envoie les logs dans un fichier logs.xml, en plus des affichages sur la console. Effectivement, nous allons invoquer la méthode addHandler qui ajoute un nouveau « handler » au logger. On ne remplace pas le handler sur la console initialement crée par JUL.

par défaut, quand on log dans un fichier, les traces sont produites au format XML. Cela très pratique, si vous souhaitez ensuite travailler sur ce fichier de log par code Java : certaines situations amènent à ce besoin.
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
package fr.koor.samples.jul;

import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

public class TestJUL {

    // Récupération de notre logger.
    private static final Logger LOGGER = Logger.getLogger( TestJUL.class.getPackage().getName() );

    // Le bloc statique pour configurer notre logger.
    static {
        try {
            FileHandler fileHandler = new FileHandler( "logs.xml" );
            LOGGER.addHandler( fileHandler );
        } catch( Exception exception ) {
            LOGGER.log( Level.SEVERE, "Cannot read configuration file", exception );
        }
    }
    
    // Le point d'entrée du programme.
    public static void main( String [] args ) {

        LOGGER.info( "Hello World with java.util.logging" );
        
        try {
            int value = (int)( Math.random() * 2 );
            int result = 3 / value;
            Object [] data = { value, result };
            LOGGER.log( Level.INFO, "value == {0} - result == {1}", data );
        } catch( Exception exception ) {
            LOGGER.severe( "Mon message d'erreur : " + exception );
        }
        
    }
}
Configuration du logger par code Java

Vous pouvez maintenant consulter le fichier logs.xml pour en voir son contenu.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2020-07-30T16:37:11</date>
  <millis>1596119831164</millis>
  <sequence>0</sequence>
  <logger>Compo1</logger>
  <level>INFO</level>
  <class>fr.koor.samples.jul.TestJUL</class>
  <method>main</method>
  <thread>1</thread>
  <message>Hello World with java.util.logging</message>
</record>
<record>
  <date>2020-07-30T16:37:11</date>
  <millis>1596119831191</millis>
  <sequence>1</sequence>
  <logger>Compo1</logger>
  <level>SEVERE</level>
  <class>fr.koor.samples.jul.TestJUL</class>
  <method>main</method>
  <thread>1</thread>
  <message>Mon message d'erreur : java.lang.ArithmeticException: / by zero</message>
</record>
</log>
Exemple d'un fichier de log JUL au format XML

Maintenant, si vous préferez opter pour le format utilisé sur la console dans vos fichiers de logs, il vous suffit de changer le « formateur ». Pour ce faire nous allons invoquer la méthode setFormatter sur l'instance de FileHandler. Voici la section de code à modifier si vous souhaitez changer le format des logs.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
// Le bloc statique pour configurer notre logger.
static {
    try {
        FileHandler fileHandler = new FileHandler( "logs.xml" );
        fileHandler.setFormatter( new SimpleFormatter() );
        LOGGER.addHandler( fileHandler );
    } catch( Exception exception ) {
        LOGGER.log( Level.SEVERE, "Cannot read configuration file", exception );
    }
}
Configuration d'un formateur de logs

Si vous ouvrez de nouveau le fichier de logs, vous devriez maintenant y voir un contenu proche de celui-ci.

juil. 30, 2020 5:18:22 PM fr.koor.samples.jul.TestJUL main
INFOS: Hello World with java.util.logging
juil. 30, 2020 5:18:22 PM fr.koor.samples.jul.TestJUL main
GRAVE: Mon message d'erreur : java.lang.ArithmeticException: / by zero
l'API JUL met à votre disposition deux formateurs : java.util.logging.SimpleFormatter et java.util.logging.XMLFormatter. De plus, vous pouvez créer vos propres formateurs en dérivant de la classe abstraite java.util.logging.Formatter.

Nous avons parlé, précédemment dans ce document, des niveaux de logs et des liens qui existaient entre eux. Bien entendu, vous pouvez fixer le niveau de log autorisée pour votre logger : pour ce faire, il faut utiliser la méthode LOGGER.setLevel(). Voici un exemple de configuration ou seules les erreurs (Code.SEVERE) sont tracées.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
// Le bloc statique pour configurer notre logger.
static {
    try {
        FileHandler fileHandler = new FileHandler( "logs.xml" );
        fileHandler.setFormatter( new SimpleFormatter() );
        LOGGER.addHandler( fileHandler );
        LOGGER.setLevel( Level.SEVERE );
    } catch( Exception exception ) {
        LOGGER.log( Level.SEVERE, "Cannot read configuration file", exception );
    }
}
Exemple de prise de contrôle sur les niveaux de logs autorisés pour ce logger

En cas de déclenchement d'une exception, seule le message d'erreur est tracé. Le message informatif est ignoré, comme en atteste le résultat suivant.

juil. 30, 2020 5:33:16 PM fr.koor.samples.jul.TestJUL main
GRAVE: Mon message d'erreur : java.lang.ArithmeticException: / by zero

Vous pouvez changer d'autres caractéristiques sur votre logger et notamment le format d'affichage d'un log. Nous allons-y revenir dans la prochaine section.

bien que cette technique soit tout à faire opérationnelle, je ne vous recommande pas de configurer votre moteur de logs par programmation. Si vous souhaiter changer quoi que ce soit, il faudra modifier le code de votre application.

Définir un fichier de configuration

Pour utiliser un fichier de configuration JUL, il faudra utiliser un gestionnaire de logs représenté par une instance de la classe LogManager. On récupère le gestionnaire de logs via la ligne de code suivante.

 1 
private static final LogManager logManager = LogManager.getLogManager();
Exemple de récupération du gestionnaire de logs (LogManager).

Un fichier de configuration JUL est obligatoirement au format « .properties ». Il s'agit d'un format de fichier très basique ou chaque ligne représente une paire clé/valeur (la clé étant séparée de sa valeur via le caractère =). Il est possible d'ajouter une ligne de commentaire dans un tel fichier en la commençant par le caractère #. Si vous fournissez un fichier de configuration JUL, vous pourrez y configurer tous les loggers de l'application.

Pour charger un fichier de configuration JUL, vous devez invoquer la méthode readConfiguration de votre gestionnaire de logs. Encore une fois, il est conseillé de réaliser cette étape dans le bloc statique de votre classe principale. Voici un exemple de chargement d'un fichier de configuration.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
package fr.koor.samples.jul;

import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

public class TestJUL {

    // Récupérarion d'un notre gestionnaire de logs.
    private static final LogManager logManager = LogManager.getLogManager();

    // Récupération de notre logger.
    private static final Logger LOGGER = Logger.getLogger( TestJUL.class.getPackage().getName() );

    // Le bloc statique pour configurer le gestionnaire de logs
    static{
        try {
            logManager.readConfiguration( new FileInputStream("jul.properties") );
        } catch ( IOException exception ) {
            LOGGER.log( Level.SEVERE, "Cannot read configuration file", exception );
        }
    }

    // Le point d'entrée du programme.
    public static void main( String [] args ) {

        LOGGER.info( "Hello World with java.util.logging" );
        
        try {
            int value = (int)( Math.random() * 2 );
            int result = 3 / value;
            Object [] data = { value, result };
            LOGGER.log( Level.INFO, "value == {0} - result == {1}", data );
        } catch( Exception exception ) {
            LOGGER.severe( "Mon message d'erreur : " + exception );
        }
        
    }
}
Exemple d'initialisation du gestionnaire de logs via un fichier de configuration.

Dans l'exemple ci-dessus, je pars du principe que le fichier de configuration se nomme jul.properties. Voici un exemple possible pour le contenu de ce fichier.

# On log sur la console et dans un fichier.
handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler

# On peut configurer le ConsoleHandler, mais ici j'utilise sa configuration par défaut.
# java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter (c'est pas défaut)

# On configure notre FileHandler (il utilise lui aussi un SimpleFormatter).
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.pattern=jul-%u-%g.log

# On change le format des logs pour notre SimpleFormatter.
java.util.logging.SimpleFormatter.format=[%1$s] %4$-10s | (%3$s) %2$-50s | %5$s\n

# Rappels sur les niveaux : OFF / SEVERE / WARNING / INFO / CONFIG / FINE / FINER / FINEST / ALL
# On limite tous les logs des autres composants (des autres packages) à l'affichage des erreurs.
.level=SEVERE

# On active les logs du package fr.koor.samples.jul sur INFO (et donc WARNING et SEVERE).
fr.koor.samples.jul.level=INFO
Fichier jul.properties utilisé pour la configuration de notre moteur de logs.

Si on lance le programme ci-dessus, avec la configuration proposée, vous devriez obtenir le résultat suivant.

[Thu Jul 30 17:54:20 CEST 2020] INFOS      | (fr.koor.samples.jul) fr.koor.samples.jul.TestJUL main                   | Hello World with java.util.logging
[Thu Jul 30 17:54:20 CEST 2020] GRAVE      | (fr.koor.samples.jul) fr.koor.samples.jul.TestJUL main                   | Mon message d'erreur : java.lang.ArithmeticException: / by zero

Reprenons le fichier de configuration définition par définition.

Si cette configuration ne vous convient plus, arrêtez le programme puis modifier le fichier jul.properties et enfin, redémarrez le programme avec cette nouvelle configuration du moteur de logs. J'espère que vous êtes convaincu par l'intérêt que cette possibilité.

Attention aux problématiques de performances

mal utilisés, vos logs peuvent consommer beaucoup de ressources pour pas forcément grand chose. Le mieux, pour comprendre cette problématique de performance, c'est un petit exemple concret : nous allons donc considérer l'exemple suivant.
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
package fr.koor.samples.jul;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

public class TestLogGuard {

    // Un compteur pour démultiplier la problématique 
    private static final int LOOP_COUNT = 10_000_000;

    // Récupération d'un notre gestionnaire de logs.
    private static final LogManager logManager = LogManager.getLogManager();

    // Récupération de notre logger.
    private static final Logger LOGGER = Logger.getLogger( TestJUL.class.getPackage().getName() );

    // Le bloc statique pour configurer le gestionnaire de logs
    static{
        try {
            logManager.readConfiguration( new FileInputStream("jul.properties") );
        } catch ( IOException exception ) {
            LOGGER.log( Level.SEVERE, "Cannot read configuration file", exception );
        }
    }

    // Deux attributs utiles pour notre simulation de toString.
    private String aName = "AName";
    private double aValue = Math.random() * 100;
    
    // On simule un traitement itératif qui produit régulièrement des logs.
    public void doSomething() {
        for (int i = 0; i < LOOP_COUNT; i++) {
            aValue += 0.01;
            LOGGER.info( i + ": A message " + this );
        }
    }

    /*
     * Une méthode pour transformer une instance en chaîne de caractères.
     * L'idée est de simuler l'affichage d'objets complexes dans les logs,
     * ce qui est très utile quand on trace l'activité d'un programme.
     */
    @Override
    public String toString() {
        return "TestLogGuard[aName=" + aName + ", aValue=" + aValue + "]";
    }
    

    // Le lancement de la démonstration.
    public static void main(String[] args) {
        TestLogGuard test = new TestLogGuard();
        
        // On prend des mesures de temps pour chronométrer le temps pris par doSomething.
        long begin = System.currentTimeMillis();
        test.doSomething();
        long end = System.currentTimeMillis();
        
        System.out.println( "Duration : " + (end - begin)  + "ms" );
    }

}
Un petit exemple de code pour comprendre une problématique de performance.

Si vous activez les logs pour notre classe, un grand nombre de traces seront affichées sur la console. C'est normal et étant donné le nombre de tours de boucle, je vous conseille de terminer le processus sans en attendre la fin.

Souvent, une fois la mise en production effectué, les logs de l'application sont restreints au niveau Level.SEVERE (voir Level.WARNINGS) : l'idée étant de ne pas tout enregistrer, mais quand même de tracer les problèmes s'ils surviennent. Le plus simple pour obtenir ce résultat, c'est de modifier ainsi le fichier de configuration de JUL.

# On log sur la console et dans un fichier.
handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler

# On peut configurer le ConsoleHandler, mais ici j'utilise sa configuration par défaut.
# java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter (c'est pas défaut)

# On configure notre FileHandler (il utilise lui aussi un SimpleFormatter).
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.pattern=jul-%u-%g.log

# On change le format des logs pour notre SimpleFormatter.
java.util.logging.SimpleFormatter.format=[%1$s] %4$-10s | (%3$s) %2$-50s | %5$s\n

# Rappels sur les niveaux : OFF / SEVERE / WARNING / INFO / CONFIG / FINE / FINER / FINEST / ALL
# On limite tous les logs des autres composants (des autres packages) à l'affichage des erreurs.
.level=SEVERE
Fichier jul.properties utilisé pour la configuration de notre moteur de logs.

Vous pouvez maintenant relancer le programme et, normalement, aucun log ne sera produit. Seul l'appel à System.out.println() devrait vous donner le temps nécessaire pour ne pas produire 10 millions de logs. Sur ma machine cela prend environ 5 secondes, mais le temps peut varier en fonction de la puissance de calcul de votre PC.

Je ne sais pas trop ce que vous en pensez, mais croyez-moi sur parole : c'est beaucoup trop ! Dans ce programme, on pense 5 secondes à produire des chaînes de caractères pour rien. Cela se passe au niveau de la ligne 37 : avant de rentrer dans l'appel à LOGGER.log, on évalue l'expression i + ": A message " + this. Or, si le log ne doit pas être produit, on a quand même fait les concaténations (plus celles cachées dans le toString).

Pour vous en convaincre, veuillez modifier le code de la méthode doSomething ainsi :

 1 
 2 
 3 
 4 
 5 
 6 
 7 
public void doSomething() {
    for (int i = 0; i < LOOP_COUNT; i++) {
        aValue += 0.01;
        if ( LOGGER.isLoggable( Level.INFO ) )
            LOGGER.info( i + ": A message " + this );
    }
}
Utilisation d'un « log guard »

La différence, dans ce nouveau code, c'est l'utilisation de la condition if ( LOGGER.isLoggable( Level.INFO ) ) : on parle de « log guard » (un gardien de log). Cette condition permet de vérifier si le log doit être produit ou non. Certes, la méthode log fait normalement ce test, mais dans notre cas cela nous évitera de réaliser les concaténations avant l'appel à la méthode.

Relancez le programme avec le « log guard » et constatez la différence. Sur ma machine, cela ne prend plus que 40 millisecondes. CQFD : nous avions donc bien passé environ 5 secondes à produire de chaines de caractères totalement inutiles !

si le message à tracer ne contient aucune concaténation, le « log guard » n'est plus nécessaire. Si vous laissez le « log guard » pour protéger un log sur une chaîne de caractères constante et que le niveau de log est autorisé, la vérification aura lieu deux fois : c'est pas top non plus.

Une autre manière de régler le problème de performance, consiste à utiliser les logs formatés : cette technique ressemble un peu à l'utilisation de la méthode System.out.printf(), en définissant des points d'injections dans une chaîne de caractères servant de format. Afin de tester cette possibilité, je vous propose de réécrire le contenu de la méthode doSomething tel que proposé dans l'exemple ci-dessous. Relancez le programme. Sur ma machine, je ne mets que 88 millisecondes pour terminer les 10 millions de tours de boucles.

 1 
 2 
 3 
 4 
 5 
 6 
public void doSomething() {
    for (int i = 0; i < LOOP_COUNT; i++) {
        aValue += 0.01;
        LOGGER.log( Level.INFO, "{0}: A message {1}", new Object[] { i, this } );
    }
}
Utilisation d'un log formaté
personnellement, c'est la technique que je préconise vivement. Certes le programme prend un petit peu plus de temps, mais cette technique évite la présence des « log guards », souvent un peu lourd à écrire systématiquement : de mon point de vue, la lisibilité du code en est accrue.

En synthèse :

Style de code Temps de réponse pour 10 millions de logs désactivés
LOGGER.info( i + ": A message " + this ); 5000 ms
if ( LOGGER.isLoggable( Level.INFO ) )
    LOGGER.info( i + ": A message " + this );
40 ms
LOGGER.log( Level.INFO, "{0}: A message {1}", new Object[] { i, this } ); 88 ms

Au terme de cette démonstration, j'espère vous avoir convaincu de faire attention aux concaténations dans vos logs.



Pourquoi utiliser une API de logging ? Utilisation de l'API Log4J2