La journalisation (logging) est une pratique essentielle dans le développement d'applications, permettant de suivre les événements, de déboguer les erreurs et de comprendre le comportement d'un système en production. Cet article détaille la mise en place d'un système de journalisation robuste pour une applicasion Node.js en utilisant la bibliothèque populaire log4js, en configurant des journaux séparés pour les requêtes, les réponses et les erreurs.
1. Préparation de l'Environnement de Journalisation
Commencez par créer un répertoire dédié pour stocker tous vos fichiers de journaux à la racine de votre projet. Nommez-le logs. Pour éviter que ces fichiers ne soient committés dans votre système de contrôle de version, ajoutez ce répertoire à votre fichier .gitignore.
node_modules
.env
logs/
2. Configuration de log4js
Créez un fichier de configuration pour log4js. Par exemple, placez-le dans ./src/config/loggerConfig.js. Ce fichier définira les "appenders" (où les journaux sont écrits) et les "catégories" (qui définissent les niveaux de journalisation et les appenders à utiliser).
const path = require('path');
// Répertoire racine pour tous les journaux
const repertoireRacineJournaux = path.resolve(__dirname, '../../logs');
// Définitions des chemins et noms de fichiers pour les différents types de journaux
const cheminsJournaux = {
requetes: {
dossier: '/requetes',
nomFichier: 'acces_requetes',
},
reponses: {
dossier: '/reponses',
nomFichier: 'reponses_serveur',
},
erreurs: {
dossier: '/erreurs',
nomFichier: 'erreurs_application',
},
};
module.exports = {
appenders: {
// Appender pour la console (utile en développement)
console: { type: 'console' },
// Appender pour les requêtes entrantes
journalRequetes: {
type: 'dateFile', // Permet la rotation des fichiers par date
filename: repertoireRacineJournaux + cheminsJournaux.requetes.dossier + '/' + cheminsJournaux.requetes.nomFichier,
pattern: '-yyyy-MM-dd.log', // Suffixe du fichier de journal (un par jour)
alwaysIncludePattern: true, // Inclut toujours le motif dans le nom du fichier
encoding: 'utf-8',
maxLogSize: 5 * 1024 * 1024, // Taille maximale du fichier avant rotation (5MB)
compress: true, // Compresse les anciens fichiers de journal
},
// Appender pour les réponses sortantes
journalReponses: {
type: 'dateFile',
filename: repertoireRacineJournaux + cheminsJournaux.reponses.dossier + '/' + cheminsJournaux.reponses.nomFichier,
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true,
encoding: 'utf-8',
maxLogSize: 5 * 1024 * 1024,
compress: true,
},
// Appender pour les erreurs d'application
journalErreurs: {
type: 'dateFile',
filename: repertoireRacineJournaux + cheminsJournaux.erreurs.dossier + '/' + cheminsJournaux.erreurs.nomFichier,
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true,
encoding: 'utf-8',
maxLogSize: 10 * 1024 * 1024, // Plus grand pour les erreurs (10MB)
numBackups: 3, // Conserve 3 fichiers de sauvegarde compressés
},
},
categories: {
// Catégorie par défaut pour les journaux génériques
default: { appenders: ['console'], level: 'debug' },
// Catégories spécifiques pour chaque type de journal
requetes: { appenders: ['journalRequetes'], level: 'info' },
reponses: { appenders: ['journalReponses'], level: 'info' },
erreurs: { appenders: ['journalErreurs', 'console'], level: 'error' }, // Les erreurs vont aussi à la console
},
// Options globales pour log4js
pm2: true, // Améliore la compatibilité avec PM2 pour les processus clusterisés
disableClustering: true, // Important pour le fonctionnement avec des clusters Node.js
};
3. Création du Middleware de Journalisation
Nous allons encapsuler la logique de journalisation dans un middleware Koa.js. Ce middleware interceptera les requêtes, les réponses et les erreurs, et les enverra aux catégories log4js appropriées. Créez un fichier comme ./src/utils/loggerMiddleware.js.
const log4js = require('log4js');
const loggerConfig = require('../config/loggerConfig'); // Référence au fichier de configuration
// Configure log4js avec les options définies
log4js.configure(loggerConfig);
/**
* Classe utilitaire pour formater les messages de journalisation.
*/
class LogFormatter {
/**
* Formate une entrée de journal pour une requête entrante.
* @param {object} ctx - Contexte Koa de la requête.
* @returns {string} Le message de journal formaté.
*/
static formatRequestEntry(ctx) {
let logMessage = `===== DÉBUT REQUÊTE =====\n`;
logMessage += ` Timestamp: ${new Date().toISOString()}\n`;
logMessage += ` Méthode: ${ctx.method}\n`;
logMessage += ` URL: ${ctx.originalUrl}\n`;
if (ctx.method === 'GET') {
logMessage += ` Paramètres (query): ${JSON.stringify(ctx.query)}\n`;
} else if (ctx.request.body) { // Utiliser ctx.request.body pour le corps des requêtes POST/PUT
logMessage += ` Corps de requête: ${JSON.stringify(ctx.request.body)}\n`;
}
logMessage += ` En-têtes: ${JSON.stringify(ctx.headers)}\n`;
return logMessage;
}
/**
* Formate une entrée de journal pour une réponse sortante.
* @param {object} ctx - Contexte Koa de la réponse.
* @param {number} durationMs - Durée du traitement en millisecondes.
* @returns {string} Le message de journal formaté.
*/
static formatResponseEntry(ctx, durationMs) {
let logMessage = `===== FIN RÉPONSE =====\n`;
logMessage += ` Timestamp: ${new Date().toISOString()}\n`;
logMessage += ` Statut: ${ctx.status}\n`;
// Limiter la taille du corps de réponse si très grand
const responseBody = JSON.stringify(ctx.body);
logMessage += ` Corps de réponse: ${responseBody.substring(0, 500)}${responseBody.length > 500 ? '...' : ''}\n`;
logMessage += ` Durée du traitement: ${durationMs} ms\n`;
return logMessage;
}
/**
* Formate une entrée de journal pour une erreur.
* @param {object} ctx - Contexte Koa de l'erreur.
* @param {Error} error - L'objet erreur capturé.
* @param {number} durationMs - Durée du traitement avant l'erreur.
* @returns {string} Le message de journal formaté.
*/
static formatErrorEntry(ctx, error, durationMs) {
let logMessage = `!!!!! ERREUR SERVEUR !!!!!\n`;
logMessage += ` Timestamp: ${new Date().toISOString()}\n`;
logMessage += LogFormatter.formatRequestEntry(ctx); // Inclut le contexte de la requête
logMessage += ` Message d'erreur: ${error.message}\n`;
logMessage += ` Stack trace: ${error.stack}\n`;
logMessage += ` Durée avant erreur: ${durationMs} ms\n`;
return logMessage;
}
}
/**
* Classe qui interagit directement avec les logger log4js configurés.
*/
class AppLogger extends LogFormatter {
static logIncomingRequest(ctx) {
log4js.getLogger('requetes').info(LogFormatter.formatRequestEntry(ctx));
}
static logOutgoingResponse(ctx, durationMs) {
log4js.getLogger('reponses').info(LogFormatter.formatResponseEntry(ctx, durationMs));
}
static logApplicationError(ctx, error, durationMs) {
log4js.getLogger('erreurs').error(LogFormatter.formatErrorEntry(ctx, error, durationMs));
}
}
/**
* Middleware Koa pour la journalisation des requêtes, réponses et erreurs.
* @returns {function} Le middleware Koa.
*/
module.exports = () => {
return async (ctx, next) => {
const debutTraitement = Date.now();
let duree;
try {
AppLogger.logIncomingRequest(ctx);
await next(); // Passe la main au middleware suivant
duree = Date.now() - debutTraitement;
AppLogger.logOutgoingResponse(ctx, duree);
} catch (err) {
duree = Date.now() - debutTraitement;
AppLogger.logApplicationError(ctx, err, duree);
// Gestion de l'erreur pour le client
ctx.status = err.status || 500;
ctx.body = {
code: ctx.status,
message: err.message || 'Erreur interne du serveur. Veuillez réessayer plus tard.'
};
}
};
};
4. Intégration du Middleware dans votre Application Node.js (Koa.js)
Dans votre fichier d'entrée principal (par exemple, app.js ou index.js), importez et utilisez le middleware de journalisation. Il est recommandé de le placer au début de votre chaîne de middleware pour qu'il puisse intercepter toutes les requêtes et les erreurs.
const Koa = require('koa');
const logMiddleware = require('./utils/loggerMiddleware'); // Chemin vers notre middleware
const app = new Koa();
// Enregistrez le middleware de journalisation en premier
app.use(logMiddleware());
// Exemple de routes pour démontrer la journalisation
app.use(async ctx => {
if (ctx.path === '/simuler-erreur') {
throw new Error('Ceci est une erreur d\'application simulée !');
}
if (ctx.path === '/bonjour') {
ctx.body = { message: 'Bonjour du serveur Koa !' };
ctx.status = 200;
return;
}
// Requête par défaut
ctx.body = 'Service opérationnel.';
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Serveur Koa démarré sur http://localhost:${PORT}`);
console.log('Testez /bonjour ou /simuler-erreur pour voir les journaux.');
});
Avec cette configuration, toutes les requêtes entrantes, les réponses sortantes et les erreurs seront automatiquement journalisées dans des fichiers distincts et rotatifs, vous offrant une visibilité claire sur le comportement de votre application.