Accéder au contenu principal

Comment créer et gérer efficacement les logs dans votre application Java

Lorsque votre application Java s'execute dans son environment d'exécution, il est primordial que vous logguiez (écriviez) tout ce qui se passe d'important dans un fichier journal: la log. Cette log, vous permettra de tracker, de comprendre et de résoudre divers problèmes survenus lors de l'exécution de votre application. Sans la log, il vous sera difficile de corriger facilement les bugs de votre application Java, car comme son nom l'indique, la log (le journal) consignera tout ce que vous jugerez important. Dans l'univers de Java, il existe plusieurs librairies qui vous permettent de créer, alimenter et archiver la log de votre application.



1. Qu'est ce que le logging (ou la journalisation)

Logguer consiste à consigner (d'écrire) dans un fichier journal (la log) toutes les informations importantes, toutes les erreurs et exceptions qui surviennent lors de l'exécution d'un programme Java. Afin de ne pas saturer le journal, les informations  seront donc catégoriser, selon le contexte, le dégré d'importance, etc..

Deux éléments importants doivent être pris en considération lorsque votre application se met à logguer des informations:

  • Jamais le logging ne doit influer sur les performances de votre applications
  • Jamais le logging ne doit mettre en rade votre environnement d'exécution (disque plein, etc...)
Il faudra donc, toujours mettre en oeuvre différentes techniques pour éviter les deux situations préalablement citées.

2. Pourquoi logguer

Tout système, toute application doit nécessairement tenir un journal des erreurs et des informations importantes afin de permettre l'analyse et la correction des problèmes survenues. Cette journalisation des informations et erreurs permettra au moment opportun de faire un diagnostic clair de la situation. En fait, les logs sont un moyen que votre application utilise pour communiquer et vous informer.

3. Les librairies Java pour logguer

  • Java Logging (java.util.logging)
Cette librairie est de base présente dans la JVM. Pas besoin d'inclure une dépendance externe à votre application.
L'une des plus anciennes librairies existantes dans l'écosystème permettant le logging en Java. Très flexible et riche en fonctionnalités, cette librairie a progressivement été de moins en moins utilisée compte tenu de quelques failles de sécurité et surtout cette librairie n'est plus maintenue.

Dependances Gradle
implementation 'log4j:log4j:1.2.17'

Dependances Maven
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
Il s'agit de la nouvelle version de log4j. Cette librairie est tout aussi flexible et possède plusieurs améliorations comparée à la précédente.

Dependances Gradle
implementation 'org.apache.logging.log4j:log4j-api:2.23.1'
implementation 'org.apache.logging.log4j:log4j-core:2.23.1'

Dependances Maven
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.23.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
</dependency>
</dependencies>

Cette librairie de logging est aussi considérée comme le succésseur de log4j. Elle est aussi considérée comme plus rapide et plus efficace que sa concurrente log4j2. Proposant plusieurs fonctionnalités et hautement extensible, elle est aujourd'hui l'une des librairies les plus utilisées pour le logging en Java.

Dependances Gradle
implementation 'ch.qos.logback:logback-classic:1.5.6'

Dependances Maven
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>

Il existe bien d'autres librairies pour logguer en Java. Mais nous nous limiterons dans ce tutoriel aux librairies les plus populaires.

4. Pourquoi utiliser une couche d'abstraction

Comme vous avez pu l'apercevoir précédemment, il existe plusieurs librairies Java pour gérer les logs de votre application développé en Java. Afin de ne pas énormément modifier votre code lorsque v ous changerez de librairies de logging (pour plusieurs raisons), il est préférable d'utiliser une couche d'abstraction pour vos instructions de logging. Cette couche d'abstraction se chargera quant à elle de communiquer avec la librairie de logging sous-jacente que vous aurez choisi. Ainsi, remplacer la librairie de logging pourra être remplacée aisément sans impacter le code.

Les deux couches d'abstraction les plus utilisées en Java:

Plus ancienne, mais assez efficace. Elle a des problèmes connus quant au chargement des classes en Java

Dependances Gradle

implementation 'commons-logging:commons-logging:1.3.3'

Dependances Maven

<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.3.3</version>
</dependency>

Plus récente, plus simple et plus efficace que la librairie précédente, cette couche d'abstraction est de plus en plus utilisée en tant que couche d'abstraction pour le logging.

Dependances Gradle

implementation 'org.slf4j:slf4j-api:2.0.13'

Dependances Maven

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>

5. Configuration de Logback

  • Le fichier logback.xml
Afin que votre logging fonctionne, vous devez créer un fichier logback.xml à placer dans la classpath de votre application Java. Dans notre exemple, nous créons le fichier logback.xml dans le dossier resources de notre projet Gradle.
Vous pouvez aussi, en ligne de command, indiquez l'emplacement de votre fichier de configuration myConfig.xml, si ce dernier ne réside pas dans le classpath de votre application, comme suit:
-Dlogback.configurationFile=/path/to/myConfig.xml

Le contenu de base du fichier logback.xml

<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>

Dans ce exemple nous indiquons vouloir logguer nos messages sur la console (ce qui correspond à l'output standard: System.out).
Dans l'encoder, nous indiquons le pattern (le format) d'affichage que nous souhaitons avoir: 
  • %d{HH:mm:ss.SSS} : Le format de l'heure 
  • %thread : Le nom du thread
  • %-5level : Le niveau (la catégorie) du message sur 5 caractères maximum avant le nom final
  • %logger{36} : Le nom du loggueur sur 36 caractères maximum avant le nom final
  • %kvp : Les clés valeurs présents dans le message
  • %msg : Le message à logguer
  • %n : Le retour à la ligne pour chaque message

6. Comment Logguer avec SLF4J et Logback


Pour logguer, nous commençons par créer notre loggueur à l'aide de SLF4J

private static final Logger logger = LoggerFactory.getLogger(Main.class);

Puis nous utilisons le loggueur pour écrire des messages dans les logs comme suit:
logger.info("Start the calculation");

Mis bout à bout, nous avons ceci

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
logger.info("Start the calculation");
double result = Math.pow(100, 100);
logger.info("End the calculation with result: {}", result);
}
}

A l'exécution, ce programme produira la log suivante, sur la sortie standard de l'application
12:40:18.041 [main] INFO c.j.loggingmanagement.Main -- Start the calculation
12:40:18.043 [main] INFO c.j.loggingmanagement.Main -- End the calculation with result: 1.0E200

Maintenant, configurons logback afin de logguer dans un fichier de log,  en plus de la console. Pour se faire, nous rajoutons un FileAppender dans la configuration logback.xml comme suit:

<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>logging-management.log</file>

<encoder>
<pattern>%date %level [%thread] %logger{10} [%file:%line] -%kvp- %msg%n</pattern>
</encoder>
</appender>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>

Et à l'exécution du même programme, un fichier de log est crée: logging-management.log et contient
2024-07-21 12:48:45,471 INFO [main] c.j.l.Main [Main.java:9] -- Start the Application
2024-07-21 12:48:45,473 INFO [main] c.j.l.DataComputer [DataComputer.java:10] -- Start the calculation
2024-07-21 12:48:45,473 INFO [main] c.j.l.DataComputer [DataComputer.java:12] -- End the calculation with result: 1.0E200
2024-07-21 12:48:45,474 INFO [main] c.j.l.Main [Main.java:11] -- End the Application

  • Les niveaux(catégories) des messages à logguer
TRACE : Ce niveau de message, est utilisé pour tracer toutes les actions, étapes et instructions d'un programme, afin de pouvoir comprendre à postériori toutes les instructions survenues.
DEBUG : Pour indiquer les différentes étapes de fonctionnement d'un algorithme (logique métier).
INFO : Pour indiquer un message utile et important à la logique métier.
WARN : Lorsqu'une erreur métier est survenue, et doit seulement arrêter le fonctionnement de l'opération en cours.
ERROR : Lorsqu'une erreur métier est survenue, et doit arrêter le fonctionnement de l'application.

En général, vous vous limiterez au niveau INFO, WARN ou ERROR, car les deux premiers niveaux sont trop verbeux, et risquent d'influer sur les performances de votre application.

A titre d'exemple, pour logguer des messages d'erreurs, vous pouvez procéder comme suit (dans le bloc catch)

public class SimpleComputer {
private static final Logger logger = LoggerFactory.getLogger(SimpleComputer.class);

public void compute(){
try {
logger.info("Start the calculation");
double result = Math.pow(100, 100);
logger.info("End the calculation with result: {}", result);

logger.info("Start the division");
double div = Math.divideExact(100, 0);
logger.info("End the division with result: {}", div);
} catch (Exception e) {
logger.warn("Unable to do the calculation because of: " + e.getMessage(), e);
throw new RuntimeException(e);
}
}
}

  • L'archivage et la suppression des fichiers de logs
Avec logback, il est possible d'indiquer dans la configuration, une politique d'archivage et de suppression des fichiers de logs. Il est donc inutile de créer un processus indépendant pour l'archivage des fichiers de logs. Cette politique d'archivate, consite à utiliser le RollingFileAppender en lieu et place du FileAppender comme suit:

<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/logging-management.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/logging-management.log-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<!-- La taille de chaque fichier archivée est de 10MB -->
<maxFileSize>10MB</maxFileSize>
<!-- Ne garder que 30 jours au maximum -->
<maxHistory>30</maxHistory>
<!-- La taille cumulée de tous les fichiers archivés. Si plus grand que 100MB,
les plus anciens fichiers archivés sont supprimés -->
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>

7. Logguer de manière asynchrone

Le logging peut être très couteux, surtout quand les fichiers de logs deviennent grands. Ce coût peut influer sur les performances de votre application en la rendant moins réactive et assez lente.

Pour éviter ce type de problème, il est parfois conseiller de logguer de manière asynchrone. Et Logback nous permet de le faire de manière transparente, via sa configuration en utilisant un AsyncAppender qui fera référence au RollingFileAppender dans notre exemple:

<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/logging-management.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/logging-management.log-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<!-- La taille de chaque fichier archivée est de 10MB -->
<maxFileSize>10MB</maxFileSize>
<!-- Ne garder que 30 jours au maximum -->
<maxHistory>30</maxHistory>
<!-- La taille cumulée de tous les fichiers archivés. Si > 100MB, les plus anciens fichiers archivés sont supprimés -->
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE" />
</appender>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="STDOUT" />
<appender-ref ref="ASYNC" />
</root>
</configuration



Le code source de ce tutoriel est présent sur github à cette addresse: logging-management on github

Ne pas hésiter à commenter ou à poser une question ou à demander de l'aide autours de java et des technologies connexes. Nous nous ferons un plaisir de vous répondre.

Commentaires

Posts les plus consultés de ce blog

Comment valider les données utilisateur dans votre application Java : Guide pratique avec Jakarta Bean Validation

Lorsque vous développez une application qui interagit avec un utilisateur, en lui demandant de saisir des données, il est primordial de vérifier et valider ces données avant tout traitement. Et ceci pour plusieurs raisons: vous aurez la possibilité d'éviter des traitements inutiles si les données reçues ne sont pas conformes aux données attendues et aussi cela permet de protéger votre application en cas d'attaque orchestrée par des hackers. Dans le langage java, il existe une spécification qui vous permet de valider tout type de données transmis dans un bean: Java Bean Validation (ou plus récemment: Jakarta Bean Validation). Dans ce tutoriel, au travers d'une petite application de gestion d'une bibliothèque de quartier, nous allons voir ensemble comment utiliser Jakarta Bean Validation. 1. Pourquoi Jakarta Bean Validation L'objectif de Jakarta Bean Validation est de standardiser la validation des champs au travers de specification (JSR 380). Aussi cette standardisat...

Comprendre l'Inversion de Contrôle : Un Guide Pratique de Guice sur l'Injection des Dépendances dans le Développement de Logiciels

L'un des principes fondamentaux dans le développement de logiciels (ou d'applications) est la mise en oeuvre des principes SOLID. Le D ici indique: Dependency Inversion ou Dependency Injection ou encore Inversion Of Control (IoC). Autrement dit: Inversion de Contrôle ou Injection des Dépendances. L'injection des dépendances à pour principal objectif le découplage des modules entre eux. Cela implique donc de créer et développer les différents composants à base de classes abstraites ou d'interfaces, comme le permet Java. Ce faisant, l'application ou le logiciel devra utiliser une librairie ou un framework qui permette d'injecter les fameuses dépendances en question. Nous verrons dans la suite du tutoriel comment une librairie (ou framework léger) comme Guice (Google Guice) nous aidera dans cet objectif. Note: Spring Framework est l'un des framework les plus connus qui permettent de mettre en oeuvre l'Inversion de Contrôle.  1. Qu'est ce que l'Injec...

Gestion des exceptions en Java: Comprendre, Traiter et Créer

Apprenez à gérer efficacement les exceptions d'exécution en Java, améliorant ainsi la fiabilité et la stabilité de vos applications et programmes. 1. Qu'est ce qu'une exception Comme dans la plus part des languages de programmation, Java vous permet de gérer avec précision des erreurs imprévues (exceptions) qui pourraient survenir lors de l'exécution d'une instruction ou d'un traitement. Une exception est une erreur imprévue qui survient lors de l'exécution d'une instruction de votre code (de votre programme). Imaginons que vous souhaitiez faire la division de deux nombres dans votre programme: x = a / b.  Malheureusement si le diviseur b vaut zéro lors de l'exécution de votre programme, cette situation génèrera une erreur imprévue. On parlera alors d'une exception. Il faut garder à l'esprit que toute instruction dans un programme peut générer une exception (à moins que vos instructions soient des plus basiques et simples). Parfois lorsqu...