Rapports, traces, journalisations

images/bluebelt.png

Que faire dans un bloc catch ?

Il y a des choses à ne pas faire dans un composant qui souhaite communiquer un rapport d’erreur au monde extérieur.

   try {
      //.......
   } catch (UneException exc) {
      System.err.println("Une erreur est survenue!") ;
      // soit dit en passant: un message qui ne dit rien!
      // ... autres actions
   }
[Attention]inadapté!

Quand un code fait directement usage d’un dispositif de remise de message (comme la console) cela veut dire que ce code sait qu’un tel dispositif est accessible.

C’est rarement le cas quand on écrit un composant logiciel: est-on sûr, par exemple, qu’il y a une console … et un utilisateur pour lire le message?

   try {
      //.......
   } catch (UneException exc) {
      exc.printStackTrace() ;
      // ... autres actions
   }
[Attention]uniquement pour code de demonstration et de test

Une trace de la pile d’exécution au moment de l’incident va être affichée sur la sortie standard d’erreur. Peut-on raisonnablement considérer que quelqu’un va pouvoir lire?

Que va pouvoir faire un utilisateur final de toutes ces informations? Appeler la maintenance et essayer de dicter le message au téléphone?

On doit considérer trois paramètres importants lorsqu’on veut émettre un rapport:

QUI?
A qui s’adresse-t’on? à un utilisateur final? à l’administrateur système? au programmeur en charge de la maintenance?
QUOI?
Un simple message pour l’utilisateur final? un message détaillé à l’administrateur? le contenu de la pile d’exécution pour la maintenance?
COMMENT?
En utilisant l’interface graphique de l’utilisateur? la console de l’administrateur? en envoyant un courriel à la maintenance?

Tout ceci fait l’objet des opérations de logging.

On notera que la notion de "rapport" (logging) englobe les rapports d’erreur, les traces, la journalisation …

Emettre un rapport

Le code qui émet un rapport n’a pas à savoir comment et à qui ce rapport sera remis: il doit se contenter d'émettre. La mise en forme, la remise sera assurée par des codes de gestion des rapports qui sont essentiellement positionnés au déploiement ou à la maintenance sur site.

exceptions et rapports

Les services de logging

Différents système de logging sont disponibles pour Java. Notoirement la librairie apache log4J est très utilisée et il y a même une libraire Commons qui se présente comme une abstraction des différents système. La logithèque SL4J est aussi très aboutie … et d’autres systèmes peuvent être proposés.

Java standard dispose de son propre système dans le package java.util.logging du module java.logging.

A partir de java 9 le logging est considéré comme un service et il est possible de faire un déploiement avec un outil qui s’y conforme (dont java.util.logging qui est l’implantation par défaut si aucune autre implantation ne lui est préférée).

Le service est défini par la classe abstraite System.LoggerFinder et donc si l’on veut brancher un système de logging différent de java.util il faut fournir une implantation de ce service qui retourne un objet conforme au contrat d’interface System.Logger. Il ne peut exister qu’une seule implantation du service qui déclare LoggerFinder (qui est chargé par le ClassLoader racine) ce qui implique qu’il s’agit bien d’un dispositif de déploiement (on ne doit pas associer un Logger particulier à une logithèque).

Attention: la recherche d’un System.Logger se fait par l’appel de System.getLogger(nom) mais la réalisation de cet appel va prendre en compte le module dans lequel l’invocation est faite. Il y a donc deux "espaces" d’utilisation de l’outil de logging:

  • Le nom fourni comme paramètre à getLogger : en général un nom de package (voir les explications liées à java.util.logging ci-après)
  • Le module d’appel. Ce sera le contexte à partir duquel on cherchera la classe qui implante LoggerFinder. L’architecture des modules et des clauses provides est à prendre en compte.

Utiliser un service de logging en faisant abstraction de son implantation suppose d’utiliser les méthodes de l’interface System.Logger. Ceci dit il n’est pas interdit d’intervenir dans les codes de réalisation du logging. A titre d’exemple vous trouverez ci-après une description sommaire des opérations liées à java.util.logging

package com.monbizness.ventes ;
//.... autres  imports
// les constantes du niveau de log
import static java.lang.System.Logger.Level.* ;

public class Panier {
   // chaque "partie" d'une application doit avoir son propre Logger
   static System.Logger logger = System.getLogger("com.monbizness.ventes") ;

   // un code quelque part
      Truc produit ;
      //.....
      logger.log(INFO, "truc avant achat", produit) ; // trace  ou journalisation
      try {
         produit.acheter(1) ;
         ajouter(produit) ;
         //...
      } catch (ExceptionStock exc) {
         logger.log(ERROR, "stock insuffisant" , exc) ;
         // ...  on peut relancer une exception
      }

   ////////////////////////////////////////////////////////////
   // autres codes

      if(TRACE_ON) { // public static final boolean connu au compile_time
         // code non compilé if TRACE_ON == false
         logger.log(TRACE, " entrée méthode panier.setClient()") ;
      }
      if(this.client == null ) {
         logger.log(WARNING, "pas de  client, on continue quand même") ;
      }

   ////////////////////////////////////////////////////////////
}

Il existe de nombreuses versions de methodes log. Comme les rapports peuvent contenir des messages à l’attention d’un opérateur spécifier des Bundles d’internationalisation est plutôt un bonne idée.

Pour le traitement effectif des rapports chaque implantation du service de logging a ses propres méthodes. Nous verrons comment ce qu’il en est avec java.util.logging.

[Note]

Voici une manière de changer les compte-rendus d’exception "orphelines" (RuntimeExceptions qui remontent jusqu’au sommet de la pile d’exécution d’un Thread)

public static void main(String[] args) {
   Thread.setDefaultUncaughtExceptionHandler(
      new Thread.UncaughtExceptionHandler () {
         public void uncaughtException(Thread t, Throwable e) {
            System.getLogger("global").log(
               Level.ERROR, "orpheline sur le fil : " + t , e
            ) ;

         }
      }
   ) ;
   // autres instructions

Utilisation de java.util.logging

Les codes qui gèrent les comportements des Loggers doivent être, autant que possible, des codes de déploiement. On imagine mal un assemblages de modules qui aboutissent à des Loggers différents (l’administration deviendrait un vrai casse-tête!)

Qu’est ce qui détermine le comportement des Loggers?

Nous parlerons ici des objets Logger de java.util.logging nous préciserons les cas où nous utiliserons System.Logger;.

Différentes combinaisons sont possibles :

Configurations de déploiement
On peut, par exemple, modifier un fichier ".properties" semblable à logging.properties et le référencer par la propriété "java.utiL.logging.config.file". Les fonctionnalités sont limités et il vaut mieux écrire une classe de déploiement référencée par la propriété "java.util.logging.config.class" (voir documentation de LogManager). On peut également écrire des handlers spécifiques.
Outils containers
Ces utilitaires permettent d’héberger des composants Java et d’en contrôler l’exécution. Ils fournissent de nombreux services d’intendance dont le logging. Exemple: serveurs d’applications (Containers d’EJB)
Codes applicatifs
Certaines parties de l’application qui sont directement à la "surface" (au contact de l’utilisateur: IHM par exemple) peuvent héberger directement des codes de récupération, de traitement et d’affichage des rapports.

Raccordements des gestionnaires de rapports (handlers)

image: logger et handlers

Les handlerss sont chargés de remettre "physiquement" des LogRecords. Il y a des handlers standard comme FileHandler, ConsoleHandler SocketHandler mais on peut écrire son propre code qui devra hériter de la classe abstraite Handler.

Chaque Handler a besoin d’un Formatter qui transforme un LogRecord en une String. (SimpleFormatter, ou XMLFormatter sont des classes standard de mise en forme)

A chaque niveau dans le cheminement des LogRecords il y a des filtres qui rejettent les messages qui ont un niveau (Level) insuffisant ou bien qui ont un contenu qui est filtré (et donc éventuellement rejeté) par un code qui implante l’interface Filter.

Attention: les Level de java.util.logging ne sont pas les mêmes que ceux de System.Logger.Level voir la table de correspondance dans la documentation de cette dernière classe (en fait un enum).

[Note]

La stratégie de mise en place des System.Loggers devrait être sophistiquée en mettant en place des ressources d’internationalisation:

package com.monbizness.ventes ;

class PackCst {// portée package!
...
   public static final String THEME_BUNDLE = "com/monbizness/messages/Errors" ;
   public static System.Logger CURLOG =
      System.getLogger("com.monbizness.ventes",
        ResourceBundle.getBundle (THEME_BUNDLE);
   // voir également getBundle avec un paramètre module
...

A partir de ce moment les rapports émis à partir des codes de ce package seront mis en forme en utilisant le Bundle correspondant:

   CURLOG.log(ERROR,"errBackup",
      new Object[] {deviceName, fileName});

Avec un déploiement sur java.util.logging: ressource correspondante dans Errors_fr_FR.properties :

errBackup : sauvegarde de {1} sur {0} impossible

--exercices--

Architecture de configuration

Dans une application le nombre de packages impliqués peut être très important. Comme il est préférable de lier les objets Loggers aux packages, comment s’y prendre pour réaliser une configuration raisonnable?

image: configuration de l’arborescence des Loggers

Les Loggers sont créés dynamiquement au runtime: quand on demande un Logger pour un package chaque Logger dans la hiérarchie supérieure est créé (s’il n’existe pas déjà).

D’une certaine manière chaque Logger "hérite" de la configuration du Logger parent:

  • Si le niveau (Level) d’un Logger est null alors il prend la valeur correspondante dans son ancêtre le plus proche au moment de la création.
  • Les objets Filter sont spécifique à chaque Logger.
  • Quand un rapport LogRecord est généré il est soumis à tous les Handlers enregistrés auprès du Logger courant, il est ensuite retransmis au Logger parent. Donc si un logger ne dispose d’aucun Handler ce sont des Handlers dans la hiérarchie au dessus qui prendront en charge le Logrecord. Un appel à "thisLogger.setUseParentHandler(false)" inhibera ce comportement.

On voit donc qu’il suffit de positionner quelques Handlers à des endroits critiques pour avoir une configuration intéressante.

Définition d’un Handler

Le package java.util.logging offre quelques Handlers comme StreamHandler et ses sous-classes FileHandler et SocketHandler mais on peut être amené à écrire ses propres Handlers (pour afficher dans une interface graphique, envoyer un courriel, etc..).

Il faut alors sous-classer Handler et spécialiser les méthodes abstraites publish(LogRecord), flush() et close() (il peut être aussi utile de raccrocher un ErrorManager pour récupérer les erreurs du système de logging lui même).

// code graphique Swing (module java.desktop)
package com.monbizness.ihm ;
....
public class IHMHandler extends Handler {

   static class IHMDialog extends JDialog {
      // HIDE ON CLOSE par défaut
      private JTextArea jtxt = new JTextArea(10,60) ;

      IHMDialog() {
         // internationaliser
         setTitle("Attention! message!") ;
         getContentPane().add(new JScrollPane(jtxt)) ;
      }

      public synchronized void report(String rapport) {
         jtxt.setText(rapport) ;//p.e. append(rapport)
         pack() ;
         show() ;
      }
   }

   static IHMDialog ihm = new IHMDialog() ;

   public void publish (LogRecord record) {
      //normalement on doit demander une fois
      // getHead(this) au formatter
      if (isLoggable(record)) {
         // supposons getFormatter ne rend pas null!
         ihm.report(getFormatter().format(record));
      }
   }

   public void flush() {
   }

   public void close() {
      //normalement on doit demander une fois
      // getTail(this) au formatter
   }
}
[Avertissement]

Le code précédent a un grave défaut: on imagine mal que ce soit un code de déploiement puisqu’il est lié à une interface graphique. Il est probable qu’il faudra alors imaginer plutôt un System.Logger intermédiaire qui intercepte les logs et les repasse ensuite au System.Logger de déploiement.

Détail de réalisation: un Handler doit savoir "décrocher" si le système passe en mode panique en envoyant des messages de log en rafale.

Définition d’un Formatter

Exemple de Formatter très simple qui pourra être utilisé avec le code précédent:

package com.monbizness.utils;

import java.util.logging.* ;

public class FormatSimple extends Formatter {
   public String format(LogRecord record) {
      StringBuilder sb = new StringBuilder();
      sb.append(record.getLevel().getLocalizedName())
         .append(":\n\t") ;
      // formattage localisé + bundle lié au rapport
      if(null != record.getMessage()) {
         sb.append(formatMessage(record)).append('\n');
      }
      // exception
      Throwable th = record.getThrown() ;
      if(th != null) {
         sb.append(th) ;
      }
      return sb.toString() ;
   }
}