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
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
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
Enregistrer un commentaire