Cet article présente la conception et l'implémentation d'un module de journalisation robuste en C++, conçu pour être thread-safe et flexible grâce au pattern Strategy. Cet outil servira de base à la création d'un pool de threads, en facilitant le débogage grâce à des fonctionnalités de journalisation avancées.
Objectifs de Conception
Le système de journalisation doit répondre aux exigences suivantes :
- Sécurité des Threads : Les opérations de journalisation doivent être intrinsèquement thread-safe. L'utilisation directe de
std::coutouprintfest problématique car ils accèdent à des ressources partagées (le terminal) sans mécanisme de synchronisation adéquat, pouvant mener à des écritures concurrentes corruptrices. - Stratégies de Sortie Multiples : Le système doit supporter différentes stratégies de sortie pour les journaux, notamment la sortie vers la console ou vers un fichier.
- Interface C++ : La journalisation doit offrir une API intuitive de style C++, permettant la construction aisée des messages. La classe
Loggerinterne générera les éléments constants du journal (comme le timestamp et le niveau), tandis que l'utilisateur fournira les informations spécifiques via une interface d'insertion de flux.
L'utilisation prévue est la suivante :
Logger logger;
logger(LogLevel::INFO, __FILE__, __LINE__) << "Message d'erreur spécifique 1" << "Message d'erreur spécifique 2";
Structure du Pattern Strategy
Le pattern Strategy sera utilisé pour découpler la logique de génération du message de journal de la manière dont il est effectivement écrit. Une classe abstraite de base définira l'interface de stratégie, et des classes dérivées implémenteront des stratégies spécifiques.
1. Classe de Base : SyncStrategy
Cette classe virtuelle pure définit l'interface commune pour toutes les stratégies de synchronisation. La méthode Sync est responsable de l'écriture du message.
// Interface abstraite pour les stratégies de synchronisation des journaux
class SyncStrategy
{
public:
SyncStrategy() = default;
virtual ~SyncStrategy() = default; // Destructeur virtuel
virtual void Sync(const std::string &message) = 0; // Méthode pure virtuelle
};
2. Stratégie de Sortie Console : ConsoleSyncStrategy
Cette classe dérivée implémente la stratégie pour écrire les journaux sur la sortie standard (console). Pour garantir la thread-safety, un mutex est utilisé pour protéger l'accès à std::cout.
// Stratégie pour écrire les journaux sur la console
class ConsoleSyncStrategy : public SyncStrategy
{
public:
void Sync(const std::string &message) override
{
// Utilisation d'un LockGuard pour une gestion RAII du mutex
std::lock_guard<:mutex> lock(_mutex);
std::cout << message << std::endl; // Utilisation de std::endl pour un saut de ligne et un flush
}
private:
std::mutex _mutex; // Mutex pour la protection thread-safe
};</:mutex>
3. Stratégie de Sortie Fichier : FileSyncStrategy
Cette stratégie gère l'écriture des journaux dans un fichier. Elle inclut la création automatique du répertoire de log si nécessaire et utilise le mode d'ajout (append) pour écrire dans le fichier. La thread-safety est assurée par un mutex.
// Stratégie pour écrire les journaux dans un fichier
class FileSyncStrategy : public SyncStrategy
{
public:
// Constructeur prenant le chemin du répertoire et le nom du fichier
FileSyncStrategy(const std::string& dirPath = "./logs", const std::string& fileName = "application.log")
: _dirPath(dirPath), _fileName(fileName)
{
std::lock_guard<:mutex> lock(_mutex);
// Vérification et création du répertoire si inexistant
if (!std::filesystem::exists(_dirPath))
{
try
{
std::filesystem::create_directories(_dirPath);
}
catch (const std::filesystem::filesystem_error& e)
{
// Gestion d'erreur : affichage sur stderr car le fichier de log n'est pas encore fiable
std::cerr << "Erreur lors de la création du répertoire de logs: " << e.what() << std::endl;
// On pourrait choisir de lancer une exception ici
}
}
}
void Sync(const std::string &message) override
{
std::lock_guard<:mutex> lock(_mutex);
// Construction du chemin complet du fichier
std::string fullPath = _dirPath + "/" + _fileName;
// Ouverture du fichier en mode ajout (append)
std::ofstream logFile(fullPath, std::ios::app);
if (logFile.is_open())
{
logFile << message << std::endl; // Ajout d'un saut de ligne
}
else
{
// Gestion d'erreur : échec d'ouverture du fichier
std::cerr << "Erreur: Impossible d'ouvrir le fichier de log: " << fullPath << std::endl;
}
}
private:
std::string _dirPath;
std::string _fileName;
std::mutex _mutex; // Mutex pour la protection thread-safe
};</:mutex></:mutex>
Gestion du Contenu du Journal
Le contenu d'un journal est composé d'éléments fixes et variables.
Éléments Fixes du Journal :
- Niveau de Journalisation (Log Level) : Définit la sévérité du message (DEBUG, INFO, WARNING, ERROR, FATAL).
- Timestamp : L'heure et la date de l'événement.
- Identifiant du Processus (PID) : L'ID du processus courant.
- Nom du Fichier Source : Le nom du fichier où le log est généré (via
__FILE__). - Numéro de Ligne : La ligne du fichier source où le log est généré (via
__LINE__).
Éléments Variables du Journal :
Ce sont les messages spécifiques fournis par l'utilisateur.
Classe Logger et Classe Interne LogMessage
La classe Logger agit comme l'interface utilisateur principale. Elle détient un pointeur vers une SyncStrategy et permet de choisir la stratégie de sortie.
#include <memory> // Pour std::unique_ptr
#include <sstream> // Pour std::stringstream
#include <iomanip> // Pour std::put_time (si nécessaire, mais std::snprintf est utilisé ici)
#include <filesystem> // Pour la gestion des fichiers et répertoires
#include <fstream> // Pour std::ofstream
#include <ctime> // Pour time, localtime_r, struct tm
#include <unistd.h> // Pour getpid()
#include <mutex> // Pour std::mutex et std::lock_guard
// Définition du namespace pour organiser le code
namespace LogModule
{
// ... (définitions de SyncStrategy, ConsoleSyncStrategy, FileSyncStrategy ici) ...
// Enumération pour les niveaux de journalisation
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// Fonction utilitaire pour convertir LogLevel en chaîne de caractères
std::string LevelToString(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
// Fonction utilitaire pour obtenir le timestamp formaté
std::string GetTimestamp()
{
std::time_t now = std::time(nullptr);
struct std::tm timeinfo;
// Utilisation de localtime_r pour la thread-safety
#ifdef _WIN32
localtime_s(&timeinfo, &now); // Version Windows
#else
localtime_r(&now, &timeinfo); // Version POSIX
#endif
char buffer[80];
// Format: YYYY-MM-DD HH:MM:SS
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
return buffer;
}
// Classe principale Logger
class Logger
{
public:
Logger() : _syncStrategy(std::make_unique<ConsoleSyncStrategy>()) {} // Stratégie par défaut : console
// Méthodes pour activer différentes stratégies
void EnableConsoleOutput()
{
_syncStrategy = std::make_unique<ConsoleSyncStrategy>();
}
void EnableFileOutput(const std::string& dirPath = "./logs", const std::string& fileName = "application.log")
{
_syncStrategy = std::make_unique<FileSyncStrategy>(dirPath, fileName);
}
// Classe interne LogMessage pour construire le message de journalisation
class LogMessage
{
public:
// Constructeur : initialise les éléments fixes et prépare le message
LogMessage(LogLevel level, const char* file, int line, Logger& logger)
: _level(LevelToString(level)),
_file(file),
_line(line),
_loggerRef(logger)
{
std::stringstream ss;
ss << "[" << GetTimestamp() << "] "
<< "[" << _level << "] "
<< "[" << getpid() << "] "
<< "[" << _file << ":" << _line << "] - ";
_messageContent = ss.str();
}
// Surcharge de l'opérateur << pour ajouter des chaînes au message
LogMessage& operator<<(const std::string& str)
{
_messageContent += str;
_messageContent += " "; // Ajoute un espace entre les éléments
return *this;
}
// Surcharge de l'opérateur << pour ajouter des types streamables
template<typename T>
LogMessage& operator<<(const T& value)
{
_messageContent += std::to_string(value); // Conversion basique
_messageContent += " ";
return *this;
}
// Destructeur : déclenche la synchronisation du message lorsque l'objet LogMessage temporaire est détruit
~LogMessage()
{
// Assure que la stratégie est valide avant de l'appeler
if (_loggerRef._syncStrategy)
{
_loggerRef._syncStrategy->Sync(_messageContent);
}
}
private:
std::string _level;
const char* _file;
int _line;
std::string _messageContent;
Logger& _loggerRef; // Référence à l'objet Logger parent
};
// Surcharge de l'opérateur d'appel de fonction "()" pour créer un objet LogMessage
// Cette méthode retourne un objet LogMessage qui sera ensuite utilisé avec l'opérateur <<
LogMessage operator()(LogLevel level, const char* file, int line)
{
// Crée et retourne un objet LogMessage temporaire
return LogMessage(level, file, line, *this);
}
private:
std::unique_ptr<SyncStrategy> _syncStrategy; // Pointeur vers la stratégie de synchronisation active
};
// Instance globale du Logger pour faciliter l'utilisation via des macros
Logger g_logger;
// Macros pour simplifier l'utilisation
#define LOG_INFO(...) g_logger(LogLevel::INFO, __FILE__, __LINE__) << __VA_ARGS__
#define LOG_DEBUG(...) g_logger(LogLevel::DEBUG, __FILE__, __LINE__) << __VA_ARGS__
#define LOG_WARNING(...) g_logger(LogLevel::WARNING, __FILE__, __LINE__) << __VA_ARGS__
#define LOG_ERROR(...) g_logger(LogLevel::ERROR, __FILE__, __LINE__) << __VA_ARGS__
#define LOG_FATAL(...) g_logger(LogLevel::FATAL, __FILE__, __LINE__) << __VA_ARGS__
// Macros pour activer les stratégies
#define ENABLE_CONSOLE_LOG() g_logger.EnableConsoleOutput()
#define ENABLE_FILE_LOG(dir, file) g_logger.EnableFileOutput(dir, file)
} // Fin du namespace LogModule
Optimisation de l'Expérience Utilisateur via les Macros
Pour simplifier davnatage l'utilisation, des macros sont définies. Elles encapsulent l'appel à la méthode operator() de Logger avec les macros prédéfinies __FILE__ et __LINE__.
// Utilisation simplifiée avec les macros
LOG_INFO("Le système démarre.");
LOG_ERROR("Une erreur critique s'est produite.") << "Détail de l'erreur: " << errorCode;
// Activation des stratégies via macros
ENABLE_CONSOLE_LOG(); // Utiliser la console par défaut
// OU
ENABLE_FILE_LOG("./app_logs", "service.log"); // Utiliser un fichier spécifique
Code Complet du Module de Journalisation
Voici le code source complet du module, incluant les stratégies et la classe Logger.
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include <iomanip>
#include <filesystem>
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <mutex>
#include <stdexcept> // Pour std::runtime_error
namespace LogModule
{
// Forward declaration de Logger pour LogMessage
class Logger;
// Interface abstraite pour les stratégies de synchronisation des journaux
class SyncStrategy
{
public:
SyncStrategy() = default;
virtual ~SyncStrategy() = default;
virtual void Sync(const std::string &message) = 0;
};
// Stratégie pour écrire les journaux sur la console
class ConsoleSyncStrategy : public SyncStrategy
{
public:
void Sync(const std::string &message) override
{
std::lock_guard<std::mutex> lock(_mutex);
std::cout << message << std::endl;
}
private:
std::mutex _mutex;
};
// Stratégie pour écrire les journaux dans un fichier
class FileSyncStrategy : public SyncStrategy
{
public:
FileSyncStrategy(const std::string& dirPath = "./logs", const std::string& fileName = "application.log")
: _dirPath(dirPath), _fileName(fileName)
{
std::lock_guard<std::mutex> lock(_mutex);
if (!std::filesystem::exists(_dirPath))
{
try
{
std::filesystem::create_directories(_dirPath);
}
catch (const std::filesystem::filesystem_error& e)
{
// Ici, on pourrait logger vers stderr si la création échoue
std::cerr << "Erreur de création du répertoire '" << _dirPath << "': " << e.what() << std::endl;
// Lancer une exception pour signaler l'échec critique
throw std::runtime_error("Échec de la création du répertoire de logs.");
}
}
}
void Sync(const std::string &message) override
{
std::lock_guard<std::mutex> lock(_mutex);
std::string fullPath = _dirPath + "/" + _fileName;
std::ofstream logFile(fullPath, std::ios::app);
if (logFile.is_open())
{
logFile << message << std::endl;
}
else
{
// Log vers stderr si le fichier ne peut être ouvert
std::cerr << "Erreur: Impossible d'ouvrir le fichier de log '" << fullPath << "'" << std::endl;
}
}
private:
std::string _dirPath;
std::string _fileName;
std::mutex _mutex;
};
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LevelToString(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
std::string GetTimestamp()
{
std::time_t now = std::time(nullptr);
struct std::tm timeinfo;
#ifdef _WIN32
localtime_s(&timeinfo, &now);
#else
localtime_r(&now, &timeinfo);
#endif
char buffer[80];
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
return buffer;
}
class Logger
{
public:
// Classe interne LogMessage
class LogMessage
{
public:
LogMessage(LogLevel level, const char* file, int line, Logger& logger)
: _level(LevelToString(level)),
_file(file),
_line(line),
_loggerRef(logger)
{
std::stringstream ss;
ss << "[" << GetTimestamp() << "] "
<< "[" << _level << "] "
<< "[" << getpid() << "] "
<< "[" << _file << ":" << _line << "] - ";
_messageContent = ss.str();
}
LogMessage& operator<<(const std::string& str)
{
_messageContent += str;
_messageContent += " ";
return *this;
}
// Surcharge pour différents types
template<typename T>
LogMessage& operator<<(const T& value)
{
// Utiliser ostringstream pour une conversion plus générale
std::ostringstream oss;
oss << value;
_messageContent += oss.str();
_messageContent += " ";
return *this;
}
~LogMessage()
{
if (_loggerRef._syncStrategy)
{
_loggerRef._syncStrategy->Sync(_messageContent);
}
}
private:
std::string _level;
const char* _file;
int _line;
std::string _messageContent;
Logger& _loggerRef;
};
// Constructeur de Logger initialisant la stratégie par défaut
Logger() : _syncStrategy(std::make_unique<ConsoleSyncStrategy>()) {}
// Activation de la sortie console
void EnableConsoleOutput()
{
_syncStrategy = std::make_unique<ConsoleSyncStrategy>();
}
// Activation de la sortie fichier
void EnableFileOutput(const std::string& dirPath = "./logs", const std::string& fileName = "application.log")
{
_syncStrategy = std::make_unique<FileSyncStrategy>(dirPath, fileName);
}
// Opérateur d'appel de fonction pour créer un objet LogMessage
LogMessage operator()(LogLevel level, const char* file, int line)
{
return LogMessage(level, file, line, *this);
}
private:
std::unique_ptr<SyncStrategy> _syncStrategy;
};
// Instance globale du logger
Logger g_logger;
// Macros pour simplifier l'usage
#define LOG_DEBUG(...) LogModule::g_logger(LogModule::LogLevel::DEBUG, __FILE__, __LINE__) << __VA_ARGS__
#define LOG_INFO(...) LogModule::g_logger(LogModule::LogLevel::INFO, __FILE__, __LINE__) << __VA_ARGS__
#define LOG_WARNING(...) LogModule::g_logger(LogModule::LogLevel::WARNING, __FILE__, __LINE__) << __VA_ARGS__
#define LOG_ERROR(...) LogModule::g_logger(LogModule::LogLevel::ERROR, __FILE__, __LINE__) << __VA_ARGS__
#define LOG_FATAL(...) LogModule::g_logger(LogModule::LogLevel::FATAL, __FILE__, __LINE__) << __VA_ARGS__
#define ENABLE_CONSOLE_LOG() LogModule::g_logger.EnableConsoleOutput()
#define ENABLE_FILE_LOG(dir, file) LogModule::g_logger.EnableFileOutput(dir, file)
} // Fin du namespace LogModule