Publié par

Il y a 11 ans -

Temps de lecture 12 minutes

Les 10 commandements des logs applicatives

Tout au long du cycle de vie d’une application J2EE, il est nécessaire de posséder des traces de qualité :

  • durant le développement, afin de suivre l’exécution pas à pas et de détecter d’éventuelles anomalies.
  • durant la recette, afin de corréler anomalies fonctionnelles et exécution du programme.
  • durant l’exploitation, afin de surveiller la « bonne santé » de l’application.

Mais obtenir des traces de qualité n’est pas un exercice trivial. C’est pourquoi nous vous proposons nos 10 commandements des logs.

Une application J2EE possède de nombreux types de logs :

  • Des logs purement techniques, ponctuelles, générées automatiquement ou à la demande comme les dumps (ThreadDump, MemoryDump…).
  • Des logs provenant de l’environnement d’exécution : logs Système (journaux d’évènements serveur…), logs du serveur applicatif et logs générées par les librairies tierces utilisées par l’application.
  • Les logs générées par le code développé pour l’application, que nous désignerons comme logs applicatives, sur lesquelles l’équipe projet a la main.

Nous traiterons quasi exclusivement de ces dernières dans cet article.

Les 10 commandements

  1. Les logs sont un outil collaboratif issu d’un processus itératif.
  2. Les logs sont une brique applicative intégrée à l’environnement d’exécution de l’application.
  3. Les logs sont paramétrées en fonction des besoins.
  4. Les logs doivent avoir un impact minimal sur les performances globales de l’application.
  5. Les logs ne doivent pas occasionner de perte d’information.
  6. Une utilisation correcte du logger est la première source d’information.
  7. L’utilisation du framework de logs doit être simplifiée.
  8. Une log efficace doit remplacer le debugger.
  9. Le framework mis en place doit servir exclusivement à produire des logs applicatives.
  10. Les logs permettent de décrire le comportement aux frontières de l’application.

Entrons dans le détail

1 – Les logs sont un outil collaboratif issu d’un processus itératif

Les messages de logs sont destinés à tous les acteurs du projet.
Il est essentiel de garder à l’esprit, lors de de tout développement, qu’une application est vouée à être développée, puis testée, puis exploitée (et éventuellement corrigée). Les logs doivent prendre en compte ces 3 phases de la vie du projet, et permettre à tous les acteurs intervenant lors de ces différentes phases de pouvoir interagir efficacement.
C’est pourquoi chaque méthode devrait au minimum contenir des traces de niveau INFO, WARN et ERROR, chaque niveau ciblant un des acteurs intervenant sur l’application.
De plus, il est difficile de réaliser une ‘bonne’ log du premier coup. La pertinence de celle-ci sera renforcée par des échanges constants entre exploitation et développement (ciblage spécifique d’un problème de performance, anomalie non remontée initialement…).
Chaque correction d’anomalie devrait entrainer l’ajout de traces de niveau DEBUG, qui permettront un diagnostic plus fin en cas de nouvelle occurrence.

2 – Les logs sont une brique applicative intégrée à l’environnement d’exécution de l’application

C’est pourquoi il est essentiel de choisir un framework de logs bien intégré dans le middleware utilisé en production.
Ce framework doit pouvoir être utilisé et paramétré aisément pour obtenir des traces de l’ensemble des composants du système, que ce soit de la part de librairies embarquées tierces comme des systèmes ‘contenants’ (serveur d’application par exemple).
Par exemple, Jakarta Commons Logging est délicat à utiliser avec Websphere.

3 – Les logs sont paramétrées en fonction des besoins

L’écriture d’une log a un coût qu’il ne faut pas négliger. Les performances d’une application en production ne doivent pas être plombées par la génération de lignes de log inutiles. Et il est inenvisageable de demander à chaque développeur d’effacer systématiquement les logs qu’il ajoute (si ces logs ont été utiles à un moment de la vie du projet, il y a de grandes chances qu’elles puissent resservir plus tard).
Il est donc impératif de choisir un framework de logs facilement administrable autant en développement qu’en production :

Pour le développement :

  • Une configuration de logs par défaut quand on fait un checkout du code (typiquement un fichier log4j.properties dans le classpath configuré au niveau WARN qui redirige vers System.out).
  • La possibilité de modifier à chaud la configuration des logs lors de l’éxécution de l’application dans l’IDE (typiquement un configureAndWatch sur classpath:log4j.properties).

Pour l’exploitation :

  • Une configuration par fichiers, séparée du WAR, ce qui permet de livrer / modifier l’application indépendamment de sa configuration (et vice et versa).
  • Des mécanismes de reconfiguration à chaud par modification de fichier et/ou par API (JMX).

On se doit d’exploiter au maximum les différents Appenders.
Le système d’Appender fourni avec les principaux framework de log permet de gérer finement la persistence de la trace : elle peut être éphémère, par exemple dans une sortie Console, ou bien être archivée périodiquement, en utilisant un système de DailyRollingFileAppender.
Une bonne connaissance des Appenders est essentielle dans la gestion à long terme de la log, aussi bien en terme de performance (utilisation des Loggers asynchrones en utilisant JMS) qu’en terme d’espace disque (configurer correctement les FileAppenders afin que les fichiers restent exploitables).

4 – Les logs doivent avoir un impact minimal sur les performances globales de l’application

La majorité des frameworks de log disposent de méthodes conditionnelles de type log.is<Priority>Enabled(). Conditionner l’écriture de la log par ces méthodes permet d’économiser des ressources précieuses lors du runtime (construction d’une aggrégation de chaînes de caractères par exemple). La méthode log.<Priority>(String) effectue bien entendu le même test, mais après la résolution des paramètres.

Evitez :

log.debug("Entrée dans la méthode doIt(), avec param1[" + param1 + "], param2 [" + param2 + "]");

Préférez :

if(log.isDebugEnabled() {
   log.debug("Entrée dans la méthode doIt(), avec param1[" + param1 + "], param2 [" + param2 + "]");
}

5 – Les logs ne doivent pas occasionner de perte d’information

Ce commandement s’applique plus particulièrement aux logs des exceptions.
Il est primordial de qualifier le plus finement possible les exceptions tracées.
C’est pourquoi on privilégiera systématiquement les méthodes Logger#error(java.lang.Object message, java.lang.Throwable t) à Logger#error(java.lang.Object message) , afin de ne pas perdre les informations exposées par la stackTrace.
De plus, on explicitera au maximum le contexte d’exécution dans le message.

Evitez :

...
} catch (UnknownUserException exception) {
   log.error("Une exception s'est produite :" + exception);
   ...
}
...

Préférez :

...
catch (UnknownUserException exception) {
   log.error("L'utilisateur [" + user :"] n'a pas été trouvé dans l'annuaire ["+ myLdap + "]", exception);
...
}
...

NB : le traitement des exceptions fera l’objet d’un prochain article.

6 – Une utilisation correcte du logger est la première source d’information

Chaque niveau de log a ses spécificités. Bien définir l’utilisation de chacun de ces niveaux permet de s’affranchir du risque de logger trop, ou trop peu, aux différentes étapes du cycle de vie de l’application. Utiliser des niveaux de log cohérents et pertinents donne donc un premier niveau d’information essentiel.
Voici les niveaux existants dans les différents frameworks, et leur utilisation communément admise (Log4J/Apache Commons Logging – java.util.logging) :

  • TRACE / FINE : niveau d’information ultrafin.
  • DEBUG / CONFIG : information détaillée pour le suivi d’exécution du programme (identification et résolution d’éventuelles anomalies).
  • INFO : information essentielle sur le programme, suivi de l’exécution d’un point de vue global.
  • WARN / WARNING : situation d’exécution non idéale (utilisation d’API dépréciées, ressource non critique absente…).
  • ERROR / <pas d’équivalent> : situation d’erreur ou inattendue, qui n’entraine pas forcément une situation de blocage (accès à un service externe non critique et dont l’accès est re-testé périodiquement).
  • FATAL / SEVERE : situation d’erreur critique, qui entraîne un blocage voire un arrêt du système (problème de connexion à la DB par exemple).

7 – L’utilisation du framework de logs doit être simplifiée

L’ajout de log doit être le moins contraignant possible pour les développeurs. De plus, dans le cadre d’une équipe de développement, il n’est pas toujours facile pour un développeur de savoir quelles sont les informations pertinentes à tracer, s’il n’est pas l’auteur de la classe suivie. Il faut donc faciliter la création de logs de qualité…

  • … en rendant ses classes explicites.
    En surchargeant la méthode Object#toString(), afin qu’elle renvoie les informations principales d’une classe, on peut facilement tracer les informations en entrée ou sortie de méthode, et expliciter le contexte applicatif de l’exécution. Attention, les méthodes toString() ne doivent jamais lancer d’exception.

Evitez :

public String toString(){
   StringBuilder sb = new StringBuilder();
   sb.append(getClass().getName())
   // Ici, on peut avoir une NullPointerException
   sb.append(" myfield=[").append(this.myfield.toString()).append("]");
   return sb.toString();
}

Préférez :

public String toString(){
   return new ToStringBuilder(this).append("myfield", this.myfield).toString();
}
  • … en définissant, documentant et partageant la hiérarchie de logger
    L’utilisation la plus répandue de la hiérarchie de Logger consitste à déclarer un logger par classe.
private final static Logger LOGGER = Logger.getLogger(MyClass.class);

Cependant, il est souvent interessant de réfléchir à une hiérarchie plus poussée, par domaines technico-fonctionnels par exemple.
Il est parfois même utile de déclarer plusieurs Loggers par classe (un logger pour le suivi de l’exécution générale, un logger particulier pour surveiller une fonction critique)
Afin d’avoir des logs cohérentes, il est nécessaire de définir et de documenter la hiérarchie des Loggers au plus tôt de la vie du projet, et de la communiquer à l’ensemble des équipes, afin d’unifier les méthodes de logging.

8 – Une log efficace doit remplacer le debugger

Il existe un grand nombre de circonstances qui rendent impossible l’utilisation d’un debugger, dont la plus évidente, la nécessité de debugger un système en production. Mais on pourrait aussi citer les traitements multi-threadés, etc
Dans ces cas particuliers, l’utilisation de la log doit se substituer au débuggeur, et répondre aux mêmes besoins : « exécution » pas à pas des algorithmes, inspection des valeurs d’entrée et de sortie des points de passage clefs, parcours des différentes couches de l’application, surveillance des entrées / sorties du système.
La mise à disposition d’un tel outil est vitale pour les développeurs, car trop souvent, les équipes de développements sont dans l’obligation d’attendre plusieurs occurrences d’un même bug en production avant d’être aptes à déterminer ses conditions précises d’occurrence.

9 – Le framework mis en place doit servir exclusivement à produire des logs applicatives

Les logs sont et doivent être un outil technique. Utiliser le framework de log pour générer des traces ‘fonctionnelles’ (audit, génération de traces métier, constitution de fichiers métier) présente un risque majeur de perte de données. En effet, le risque d’effacement des logs est réel : la configuration des différents Appenders provoquent souvent un effacement périodique des logs, le comportement global des logs peut être totalement modifié dans le cadre d’une intervention de type ‘troubleshooting’ (limitation des traces à un sous-ensemble de classes très restreint)…
En revanche, il est possible d’utiliser les fonctionnalités offertes par le framework de logs (RollingFileAppender, etc) mais il faut que cette utilisation soit dissociée du fonctionnement et de la configuration des logs applicatives.

10 – Les logs permettent de décrire le comportement aux frontières de l’application

L’un des enjeux majeurs des Systèmes d’Information actuels est l’intégration des différentes composantes de ce SI.
Il est très souvent ardu de suivre intégralement la chorégraphie entre les différents composants, aboutissant à la réponse ‘finale’ du système.
D’où la nécessité de tracer le plus finement chaque appel entrant ou sortant du système, en utilisant :

  • l’heure de cet appel
  • le système appelant / appelé (par HTTP, JMS, SOAP…)
  • une description concise de l’interaction sollicitée
  • éventuellement, au niveau le plus fin (TRACE), le message émis / reçu

Certaines librairies Open Source, comme commons-http-client (HTTP) et CXF (SOAP) offrent ces fonctionnalités de manière native.

En conclusion

L’application de ces 10 commandements nous pousse de manière assez naturelle à l’utilisation de Log4J, qui répond à l’ensemble des besoins listés ci dessus.
Le choix de Commons Logging ou de java.util.logging, si il est loin d’être proscrit, demande une plus grande vigilance (voir les articles ‘Critiques’ cités en références).

Quoi qu’il en soit, il est primordial d’avoir en tête ces bonnes pratiques dès le démarrage d’un projet. En effet, au même titre que les tests unitaires ou que la documentation interne du code, les logs ne peuvent être repoussées à ‘plus tard’ : plus le temps passe, plus la complexité et le volume de code à ‘tracer’ augmentent.
Ceux qui ont expérimenté « l’intervention pompier » sur une application peu ou mal loggée savent qu’il est quasiment impossible d’otenir des logs globales et/ou pertinentes a posteriori (d’autant qu’il est difficile de ‘budgeter’ une telle opération, dont la valeur métier est nulle).

Références

Les frameworks de logs les plus connus

Critiques

Publié par

Publié par Pablo Lopez

Pablo est directeur technique chez Xebia et formateur Hadoop certifié par Cloudera au sein de Xebia Training .

Commentaire

7 réponses pour " Les 10 commandements des logs applicatives "

  1. Publié par , Il y a 11 ans

    Très intéressant, comme d’habitude ;)

    Juste une note à propos du 4ième point : une solution plus propre et élégante à ce problème serait de passer par SLF4J ou Log5j par exemple :

    Avec SLF4J:

    log.debug(« Invalid state for User {} at departement {} », user, departement);

    SLF4J s’occuperait en interne de vérifier le niveau de logging courant, et si on est bon pour émettre, alors les paramètres seront évalués (appel à toString()) et le log généré.

    Cordialement,
    Jawher.

  2. Publié par , Il y a 11 ans

    Bonjour Jawher,

    Nous espérons retrouver dans log4j la fonctionnalité de slf4j dont vous parlez avec un messageformat et des paramètres en varargs. Cela allégerait sensiblement le code.

    Hélas, log4j, standard de facto, évolue lentement et il ne nous parait pas évident que cette fonctionnalité avantageuse de slf4j justifie l’abandon de log4j comme API de debug.
    Nous avons vu sur des projets que SLF4J, indépendamment de sa valeur, est source de confusion pour des équipes habituées à Log4j.

    Merci pour vos encouragements,

    Cyrille (Xebia)

  3. Publié par , Il y a 11 ans

    Bonjour,

    Bravo pour cet article très intéressant et pertinent.

    Dans l’article, vous parlez de « mécanismes de reconfiguration à chaud par modification de fichier et/ou par API (JMX) ».

    Ce mécanisme m’intéresse beaucoup, pouvez-vous me donner un outil/API qui permette de faire cela ?

    Merci d’avance,
    Fabien

  4. Publié par , Il y a 11 ans

    Bonjour Fabien,

    Log4j offre un mécanisme de reconfiguration à chaud par écrasement du fichier de configuration grâce aux méthodes configureAndWatch(path, delayInMillis) présentes sur le PropertyConfigurator et le DOMConfigurator qui gèrent respectivement les fichiers de configuration log4j.properties et log4j.xml.
    Si vous utilisez Spring Framework, vous bénéficierez de cette fonctionnalité avec la propriété log4jRefreshInterval du Servlet Context Listener Log4jConfigListener.

    Extrait de fichier web.xml :

    <context-param>
       <param-name>log4jConfigLocation</param-name>
       <param-value>classpath:log4j.properties</param-value>
    </context-param>
    <context-param>
       <param-name>log4jRefreshInterval</param-name>
       <param-value>10000</param-value>
    </context-param>
    <listener>
       <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
    </listener>

    Veuillez noter que le Log4jConfigListener doit être déclaré avant le ContextLoaderListener qui démarre le context Spring. Par ailleurs, comme le Log4jConfigListener configure Log4j avant le chargement de la configuration de Spring, le mécanisme de property placeholder (ie. ${my-property}) ne permet pas d’utiliser des valeurs définies dans un fichier .properties mais seulement des propriétés système.

    Pour la reconfiguration par JMX, il n’y a hélas peu de solution. Log4j propose un LoggerDynamicMBean dont le javadoc précise qu’il n’est pas « production-ready ». Cependant, nous avons constaté que les exploitants attachent beaucoup moins d’importance à une reconfiguration des logs par JMX qu’au mécanisme équivalent par modification du fichier log4J.properties.

    Sur le même sujet, nous avons aussi constaté que les exploitants préfèrent largement le format properties au format xml pour configurer log4j ; ils peuvent facilement manipuler le fichier (grep, sed, etc) et le trouvent beaucoup plus lisible.

    Merci pour vos encouragemments,

    Cyrille (Xebia)

  5. Publié par , Il y a 11 ans

    Merci beaucoup pour cette réponse encore une fois très pertinente.

    Fabien

  6. Publié par , Il y a 11 ans

    Bonjour,

    Ces quelques points résument bien les problématiques essentiels du logging, bravo !

    J’ai récemment utilisé une technique (reposant sur le framework Guice) consistant à « injecter » les loggers dans les classes où cela est nécessaire. Puis, j’ai mis en place une interception configurable dynamiquement permettant de tracer les appels de méthodes.

    Ca évite l’écriture de code redondant et épars.

    Anthony MÜLLER

  7. Publié par , Il y a 11 ans

    Bonjour Antony,

    Utiliser des techniques comme l’AOP (e.g. Spring CustomizableTraceInterceptor ) ou l’introspection (e.g. Jakarta ReflectionToStringBuilder) pour générer des logs présente souvent comme limite la génération de messages difficiles à interpréter qui rappellent les dumps système.

    Je préfère, tout en étant conscient que c’est un effort, des messages de logs et des toString() spécifiques à chaque contexte qui ne seront alors plus si redondants ; ces messages sont le fruit d’un travail itératif en fonction des données utiles au troubleshooting.

    Par exemple, l’ordre et la liste des attributs à faire figurer dans un toString() méritent mieux qu’une reflection brutale sur un beans (l’id est le premier champ à afficher, les password et listes volumineuses sont exclues du message de log ou remplacés par leur taille, etc). Une approche intermédiaire pourrait être d’annoter les méthodes clef d’une application (e.g. @BusinessService) pour qu’un aspect génère des logs des paramètres d’entrée et de sortie.

    En revanche, l’AOP et l’introspection sont très utiles pour les mécanismes de « First Failure Data Capture » qui prennent une photo de chaque couche de l’application lorsqu’une exception survient. Ce mécanisme est disponible en standard dans IBM Websphere mais peut aussi être mis en oeuvre avec Spring Framework (cf. Alef Arendsen, Capturing failures and system state (part I)) ou avec Sun JavaVM BTrace (cf BTrace OnTrhow.java).

    Cyrille (Xebia)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.