Traces (log) dans les applications

On m’a récemment demandé d’expliquer quelles étaient les bonnes pratiques en terme de trace dans les applications. Voilà rapidement mon opinion sur le sujet.

Tracer, pour quoi faire?

Les traces servent à raconter ce qui se passe dans la partie cachée d’un programme informatique. Cette partie cachée étant tout ce qui n’est pas visible par l’utilisateur final. En fonction de leur niveau (ERROR, WARNING, INFO, DEBUG), elles ne s’adressent pas à la même personne, mais elles sont indispensables pour:

  • surveiller simplement l’activité de l’application. Sans parler des capacités des outils de trace moderne comme log4j, un simple tail sur un fichier de log permet de surveiller simplement l’activité d’un programme.
  • savoir ou, quand et surtout dans quel contexte une erreur est apparue
  • permettre au développeur d’analyser un problème sans qu’il soit nécessaire de le reproduire sur son poste

Ce dernier point est particulièrement important, et l’ensemble des bonnes pratiques à mettre en place visent à améliorer cette capacité d’analyse.

Lire la suite

Travailler sur la HEAD d’abord

Les bugs sont souvent levés sur une branche « stable » ou encore « de production », et il semble donc naturel de corriger l’erreur d’abord sur cette branche, puis de reporter les modifications sur la HEAD.

Je pense que c’est une erreur. Dans un monde où on prendrait le temps de corriger et tester aussi bien sur une branche que sur une autre, fût-elle la HEAD, ça ne devrait pas avoir d’importance. Malheureusement, on est plus dans des optiques de « quick, dirty and auto-merge »… Autant limiter la casse, avec un seul objectif en tête: que le futur soit meilleur.

Privilégier l’état du code actuel

La première raison est de privilégier le code tel qu’il est sur la HEAD. C’est le « vrai » code présent, et surtout la seule base logique des développements futurs.

Lorsqu’on appréhende la correction d’un bug ou l’implémentation d’une évolution, il faut donc l’optimiser et la penser avec l’état présent du code, puis ensuite l’adapter au code ancien. Si cet effort n’est pas fait:

  • Le code de la HEAD finit par n’être qu’une adaptation de merge de code ancien.
  • L’éventuel objectif d’amélioration de l’architecture sur la HEAD se retrouve brouillée / parasitée par du code « à l’ancienne ».

Profiter des dernières avancées technologiques

C’est sur la dernière version du logiciel que les dernières technologies, architectures et versions d’API sont intégrées. En Java, ces intégrations peuvent être par exemple une nouvelle version du JDK, le passage à J2EE 6 (ou à OSGi!), ou encore la mise à jour de toutes les librairies Commons Apache.

Si les développements ne sont pas d’abord faits sur la HEAD, ils ne sont pas adaptés dès leur conception à ces changements. On peut ainsi voir du code « neuf » ne pas profiter des avancées du JDK 1.5 juste parce que les développements ont été faits sur une branche plus ancienne qui ne le supportait pas.

Maximiser les chances de résolution rapide des régressions

Il paraît évident que plus un bug est détecté tôt, moins il coûte cher à tout le monde (éditeur, client et utilisateur). On s’acharne donc à mettre en place une palanquée de mesures pour « traquer le bug » au plus tôt: développement en binôme, tests unitaires, outils d’analyse de code, intégration continue, tests automatiques, tests manuels, recette…

Plus on s’éloigne dans cette chaîne, plus le coût de correction est élevé. Si en plus on ajoute le facteur temps, c’est à dire la durée écoulée entre le moment où le code a été écrit et le moment où le bug a été trouvé, les coûts explosent. En effet, le développeur qui aura à corriger le bug ne sera plus dans le contexte, sera peut-être même une personne différente de celle qui a écrit le code, etc. Les risques de régression suite à cette correction deviennent non seulement importants, mais surtout difficiles à évaluer.

Les premiers éléments de cette chaîne (binôme, tests unitaires, intégration continue) sont censés prévenir tout risque de conception ou de régression et sont effectués quelque soit la branche. Les derniers éléments (tests automatiques et manuels, recette) sont sensés tester l’ensemble des processus métiers de manière exhaustive, et passer de manière systématique sur chaque version « officielle ».

Dans la pratique, non seulement il est rare de voir des tests unitaires nombreux, entretenus et pertinents, mais en plus ils ne permettent pas de mettre en évidence les bugs les plus complexes liés à l’exécution d’un workflow métier complet. Les tests automatiques et manuels quant à eux se focaliseront plus volontier sur des workflows métier particuliers sur une livraison de branche de prod, ceeux-là même qui sont impactés par la correction /évolution effectivement attendue. De plus, ils seront exécuté rapidement après le développement. Sur HEAD au contraire, les tests manuels et automatiques, même complets, seront non seulement moins focalisés sur des processus précis, mais en plus seront exécutés bien après le développement: au moment de la première livraison de la future version majeure.

Dans ce contexte, imaginons maintenant qu’une correction, ou pire une évolution, s’intègre assez mal entre la HEAD et une branche de production d’une application, ce qui arrive relativement souvent ;-):

  • Dans le cas où la correction est d’abord conçue et effectuée sur la branche, la correction sera faite plus vite mais entraînera des régressions sur la HEAD, lesquelles ne seront détectées que…trop tard.
  • Dans le cas où on s’occupe d’abord de la HEAD, les éventuelles régressions introduites sur la branche seront immédiatement trouvées, puisque la fonctionnalité incriminée sera intensivement et rapidement testée.

En conclusion…

Bien sûr, c’est surtout vrai pour les éditeurs dont le but n’est pas de faire du code jetable, qui partent rarement de la page blanche et qui souvent, si il est petit face à ses clients, sera forcé de faire des évolutions sur branche issue de branche de production, à l’inverse de toute logique, entraînant des coûts en terme de temps, d’argent et de motivation.

Ce coût doit être calculé pour couvrir la surcharge de travail que peut entraîner, à court terme, l’application de cette recommandation: travailler sur la HEAD d’abord. Les économies seront réalisés dans la maintenabilité, l’évolutivité et la compétitivité des développements futurs.