From e80505bb1f8c957bb9e50018a4bb8927b962d062 Mon Sep 17 00:00:00 2001 From: alexds2002 <64355800+alexds2002@users.noreply.github.com> Date: Sun, 27 Oct 2024 15:41:55 +0200 Subject: [PATCH] Add Logger System for any C++ project Using Singleton Design Pattern --- C++/logger/CMakeLists.txt | 12 ++ C++/logger/README.md | 9 + C++/logger/debug_logger_component.h | 260 ++++++++++++++++++++++++++++ C++/logger/log_categories.h | 115 ++++++++++++ C++/logger/main.cpp | 19 ++ C++/logger/project_definitions.h | 117 +++++++++++++ C++/logger/singleton.h | 139 +++++++++++++++ 7 files changed, 671 insertions(+) create mode 100644 C++/logger/CMakeLists.txt create mode 100644 C++/logger/README.md create mode 100644 C++/logger/debug_logger_component.h create mode 100644 C++/logger/log_categories.h create mode 100644 C++/logger/main.cpp create mode 100644 C++/logger/project_definitions.h create mode 100644 C++/logger/singleton.h diff --git a/C++/logger/CMakeLists.txt b/C++/logger/CMakeLists.txt new file mode 100644 index 000000000..769472f66 --- /dev/null +++ b/C++/logger/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.16) + +project(Logger) + +set (CMAKE_CXX_STANDARD 23) + +add_executable(${PROJECT_NAME} + ./log_categories.h + ./debug_logger_component.h + ./project_definitions.h + ./singleton.h + ./main.cpp) diff --git a/C++/logger/README.md b/C++/logger/README.md new file mode 100644 index 000000000..716b69222 --- /dev/null +++ b/C++/logger/README.md @@ -0,0 +1,9 @@ +How to build and run from the terminal: + +1. mkdir build +2. cmake -G "Unix Makefiles" .. && make +3. ./Logger + +Requirements: +cmake 3.16 or any version after +gcc or another compiler that supports C++23(you can downgrade the version in the CMakeLists.txt file in the root down to C++17) diff --git a/C++/logger/debug_logger_component.h b/C++/logger/debug_logger_component.h new file mode 100644 index 000000000..b13d470bb --- /dev/null +++ b/C++/logger/debug_logger_component.h @@ -0,0 +1,260 @@ +#pragma once + +/* + * + * debug_logger_component.h is a collection of global variadic templated functions + * with different overload options(color, time/date, and more to come). + * The functionalities get compiled ONLY when DEBUG_MODE is defined in CMake, + * otherwise the funcitons bodies are compiled empty to avoid Debug Logs in RELEASE_MODE(optimization). + * TODO(Alex): debug_logger_component is planned to be a 'core' header that every class in the engine will have. + * + * !!! WARNINGS !!! + * Shipped products(in RELEASE_MODE) should not rely on these functions as they are only compiled in DEBUG_MODE + * However you do not need to delete them when shipping. + * (warning)inline global function with external linkage have undefined behavior. + * + */ + +/* needed outside DEBUG_MODE to compile in all modes (EPrintColor)*/ +#include "project_definitions.h" + +#ifdef DEBUG_MODE + +#include +#include +#include + +#include "log_categories.h" + +#endif /* DEBUG_MODE */ + +/** + * @brief Logs debug information to the console in debug mode. + * + * This function prints the provided arguments to the console, but only if + * the application is compiled with `DEBUG_MODE` enabled. It uses variadic + * templates to accept a flexible number of arguments and formats them as + * a single line of output. + * + * @tparam Args Variadic template parameter representing the types of the arguments to be logged. + * @param args The arguments to be printed. These can be of any type that is compatible with `std::ostream` (e.g., `std::cout`). + * + * @note + * - This function only works when the `DEBUG_MODE` macro is defined during compilation. + * If `DEBUG_MODE` is not defined, the function has no effect. + * - Before printing, the function checks whether the default logging category (`ELogCategory::Default`) + * is enabled. If it is disabled, no output is printed. + * - The logging format starts with a prefix (`>>>`), followed by the arguments, each printed in sequence, + * and ends with a newline. + * + * @details + * - The function uses the `LogManager` singleton to check whether the default log category is disabled. + * If the category is disabled, the function returns early without printing anything. + * - The use of a fold expression `([&] { std::cout << args; } (), ...)` ensures that all arguments + * are printed in sequence, with no separator between them. + * - This function is marked `noexcept` to ensure that it does not throw exceptions. + * + * Example usage: + * @code + * Debug_Log("This is a debug message with a number: ", 42); + * // Output: >>> This is a debug message with a number: 42 + * @endcode + */ +template +inline void Debug_Log(Args&&... args) noexcept +{ +#ifdef DEBUG_MODE + // Get the LogManager instance and check if the default logging category is disabled + LogManager* logManager = LogManager::GetInstance(); + if(logManager->IsCategoryDisabled(ELogCategory::Default)) + { + return; + } + std::cout << ">>> "; + ([&] + { + std::cout << args; + } (), ...); + std::cout << std::endl; +#endif /* DEBUG_MODE */ +} + +/** + * @brief Print on console dynamic number of args with a print category + * + * The body of the function is only compiled in DEBUG_MODE(RELEASE_MODE optimization) + * + * @param category: print category + * @param ...args: dinamic number of arguments to print regardless of their type + * + * Example usage: + * Debug_Log("Loading next level", 69, 420.69); + * + * @return void + */ +template +inline void Debug_Log(ELogCategory category, Args&&... args) noexcept +{ +#ifdef DEBUG_MODE + /* Do not print disabled categories */ + LogManager* logManager = LogManager::GetInstance(); + if(logManager->IsCategoryDisabled(category)) + { + return; + } + std::cout << ">>> "; + ([&] + { + std::cout << args; + } (), ...); + std::cout << std::endl; +#endif /* DEBUG_MODE */ +} + +/** + * @brief Print on console dynamic number of args with color + * + * The body of the function is only compiled in DEBUG_MODE(RELEASE_MODE optimization) + * + * @param color: print color + * @param ...args: dinamic number of arguments to print regardles of their type + * + * Example usage: + * Debug_Log(EPrintColor::Red, "Loading next level", 69, 420.69); + * + * @return void + */ +template +inline void Debug_Log(const EPrintColor color, Args&&... args) noexcept +{ +#ifdef DEBUG_MODE + /* Do not print if default category is disabled */ + LogManager* logManager = LogManager::GetInstance(); + if(logManager->IsCategoryDisabled(ELogCategory::Default)) + { + return; + } + std::string color_code = Color_To_Ansi(color); + std::cout << ">>> " << color_code; + ([&] + { + std::cout << args; + } (), ...); + std::cout << UNIX_COLOR_END_TAG << std::endl; +#endif /* DEBUG_MODE */ +} + +/** + * @brief Print on console dynamic number of args with color and a category + * + * The body of the function is only compiled in DEBUG_MODE(RELEASE_MODE optimization) + * + * @param category: category to print in + * @param color: print color + * @param ...args: dinamic number of arguments to print regardles of their type + * + * Example usage: + * Debug_Log(EPrintColor::Red, "Loading next level", 69, 420.69); + * + * @return void + */ +template +inline void Debug_Log(const ELogCategory category, const EPrintColor color, Args&&... args) noexcept +{ +#ifdef DEBUG_MODE + /* Do not print disabled categories */ + LogManager* logManager = LogManager::GetInstance(); + if(logManager->IsCategoryDisabled(category)) + { + return; + } + std::string color_code = Color_To_Ansi(color); + std::cout << ">>> " << color_code; + ([&] + { + std::cout << args; + } (), ...); + std::cout << UNIX_COLOR_END_TAG << std::endl; +#endif /* DEBUG_MODE */ +} + +/** + * @brief Print on console dynamic number of args with color and time option + * + * The body of the function is only compiled in DEBUG_MODE(RELEASE_MODE optimization) + * + * @param color: print color + * @param bShowTime: show date and time of function call + * @param ...args: dinamic number of arguments to print regardles of their type + * + * Example usage: + * Debug_Log(EPrintColor::Red, true, "Loading next level", 69, 420.69); + * + * @return void + */ +template +inline void Debug_Log(const EPrintColor color, const bool bShowTime, Args&&... args) noexcept +{ +#ifdef DEBUG_MODE + LogManager* logManager = LogManager::GetInstance(); + if(logManager->IsCategoryDisabled(ELogCategory::Default)) + { + return; + } + if(bShowTime) + { + auto call_time = std::chrono::high_resolution_clock::now(); + auto time_struct = std::chrono::system_clock::to_time_t(call_time); + std::cout << std::ctime(&time_struct); + } + std::string color_code = Color_To_Ansi(color); + std::cout << ">>> " << color_code; + ([&] + { + std::cout << args; + } (), ...); + std::cout << UNIX_COLOR_END_TAG << std::endl; +#endif /* DEBUG_MODE */ +} + +/** + * @brief Print on console dynamic number of args with color, time and category option + * + * The body of the function is only compiled in DEBUG_MODE(RELEASE_MODE optimization) + * + * @param category: print category + * @param color: print color + * @param bShowTime: show date and time of function call + * @param ...args: dinamic number of arguments to print regardles of their type + * + * Example usage: + * Debug_Log(ELogCategory::Engine, EPrintColor::Red, true, "Loading next level", 69, 420.69); + * + * @return void + */ +template +inline void Debug_Log(const ELogCategory category, const EPrintColor color, const bool bShowTime, Args&&... args) noexcept +{ +#ifdef DEBUG_MODE + /* Do not print disabled categories */ + LogManager* logManager = LogManager::GetInstance(); + if(logManager->IsCategoryDisabled(category)) + { + return; + } + if(bShowTime) + { + auto call_time = std::chrono::high_resolution_clock::now(); + auto time_struct = std::chrono::system_clock::to_time_t(call_time); + std::cout << std::ctime(&time_struct); + } + std::string color_code = Color_To_Ansi(color); + std::cout << ">>> " << color_code; + ([&] + { + std::cout << args; + } (), ...); + std::cout << UNIX_COLOR_END_TAG << std::endl; +#endif /* DEBUG_MODE */ +} + diff --git a/C++/logger/log_categories.h b/C++/logger/log_categories.h new file mode 100644 index 000000000..d26a9c0b0 --- /dev/null +++ b/C++/logger/log_categories.h @@ -0,0 +1,115 @@ +#pragma once + +/* + * TODO(Alex): Add API for runtine disabling/enabling of the categories. Maybe with IMGUI + * TODO(Alex): Add name support for each category(optionally show the category name) + */ + +#include +#include "singleton.h" +#include "project_definitions.h" + +/** + * @brief Manages logging categories and their states. + * + * The `LogManager` class is responsible for managing the enabled or disabled + * state of various logging categories. It allows enabling or disabling specific + * logging categories and checking their current state. This class is a singleton + * and inherits from the `Singleton` class template. + * + * @note + * - This class uses the `ELogCategory` enum to represent different logging categories + * and `ELogCategoryState` to represent their enabled/disabled state. + * - All logging categories are initialized as enabled by default. + * + * @tparam LogManager A singleton that ensures only one instance of the `LogManager` class exists. + * + * Example usage: + * @code + * LogManager* logManager = LogManager::GetInstance(); + * logManager->DisableCategory(ELogCategory::Debug); // Disable debug category + * if (logManager->IsCategoryEnabled(ELogCategory::Default)) { + * std::cout << "Default logging is enabled." << std::endl; + * } + * @endcode + */ +class LogManager : public Singleton +{ +public: + + /** + * @brief Constructs the `LogManager` and initializes all categories as enabled. + * + * This constructor initializes all logging categories to `Enabled` by default. + * The number of categories is determined by `ELogCategory::AutoCount`, which + * represents the total number of available logging categories. + */ + LogManager() noexcept + { + for (int i = 0; i < static_cast(ELogCategory::AutoCount); ++i) + { + logCategoryStates[static_cast(i)] = ELogCategoryState::Enabled; + } + } + + /** + * @brief Enables a specific logging category. + * + * This function enables the specified logging category, allowing log messages + * from this category to be processed. + * + * @param category The logging category to enable. + */ + void EnableCategory(ELogCategory category) + { + logCategoryStates[category] = ELogCategoryState::Enabled; + } + + /** + * @brief Disables a specific logging category. + * + * This function disables the specified logging category, preventing log messages + * from this category from being processed. + * + * @param category The logging category to disable. + */ + void DisableCategory(ELogCategory category) + { + logCategoryStates[category] = ELogCategoryState::Disabled; + } + + /** + * @brief Checks if a specific logging category is enabled. + * + * This function checks if the specified logging category is currently enabled. + * + * @param category The logging category to check. + * @return `true` if the category is enabled, `false` otherwise. + */ + bool IsCategoryEnabled(ELogCategory category) const + { + return logCategoryStates.at(category) == ELogCategoryState::Enabled; + } + + /** + * @brief Checks if a specific logging category is disabled. + * + * This function checks if the specified logging category is currently disabled. + * + * @param category The logging category to check. + * @return `true` if the category is disabled, `false` otherwise. + */ + bool IsCategoryDisabled(ELogCategory category) const + { + return logCategoryStates.at(category) == ELogCategoryState::Disabled; + } + +private: + /** + * @brief Stores the state (enabled/disabled) of each logging category. + * + * This map holds the current state of all logging categories, where the key is + * an `ELogCategory` enum and the value is an `ELogCategoryState` enum. + */ + std::map logCategoryStates; +}; diff --git a/C++/logger/main.cpp b/C++/logger/main.cpp new file mode 100644 index 000000000..a5385b632 --- /dev/null +++ b/C++/logger/main.cpp @@ -0,0 +1,19 @@ +// The debug logger works only +// when DEBUG_MODE is defined +// for optimization purposes +#define DEBUG_MODE + +#include "debug_logger_component.h" +#include "log_categories.h" + +int main() +{ + LogManager::GetInstance()->EnableCategory(ELogCategory::Default); + + Debug_Log(ELogCategory::Default, EPrintColor::Red, true, "Loading next level", 69, 420.69); + + Debug_Log("App closing :)"); + + return EXIT_SUCCESS; +} + diff --git a/C++/logger/project_definitions.h b/C++/logger/project_definitions.h new file mode 100644 index 000000000..863955356 --- /dev/null +++ b/C++/logger/project_definitions.h @@ -0,0 +1,117 @@ +#pragma once + +#include + +#define UNIX_COLOR_END_TAG "\033[m" + +/* + * Supported Log Colors + * Limited to 256 colors + */ +enum class EPrintColor : unsigned char +{ + Red, + Green, + Blue, + White, + Black, + Magenta, + Cyan, + Yellow, + Gray, + LightRed, + LightGreen, + LightBlue, + LightWhite, + LightMagenta, + LightCyan, + LightYellow +}; + +/* + * Debug categories to filter Logs + */ +enum class ELogCategory : int +{ + Default, + Error, + Core, + Editor, + Component, + Threads, + AutoCount /* Should be last! Number of categories */ +}; + +enum class ELogCategoryState : int +{ + Enabled, + Disabled, + AutoCount +}; + +/** + * @brief Convert color to its coresponding ANSI code + * + * Color converter for Unix systems, compiled when DEBUG_MODE is defined(RELEASE_MODE optimization) + * + * @param color: enum color to be converted + * + * @return std::string: corresponding ANSI code + */ +inline std::string Color_To_Ansi(const EPrintColor color) noexcept +{ + switch (color) + { + case EPrintColor::Red: + return "\033[1;31m"; + break; + case EPrintColor::Green: + return "\033[1;32m"; + break; + case EPrintColor::Blue: + return "\033[1;34m"; + break; + case EPrintColor::White: + return "\033[1;37m"; + break; + case EPrintColor::Black: + return "\033[1;30m"; + break; + case EPrintColor::Magenta: + return "\033[1;35m"; + break; + case EPrintColor::Cyan: + return "\033[1;36m"; + break; + case EPrintColor::Yellow: + return "\033[1;33m"; + break; + case EPrintColor::Gray: + return "\033[1;90m"; + break; + case EPrintColor::LightRed: + return "\033[1;91m"; + break; + case EPrintColor::LightGreen: + return "\033[1;92m"; + break; + case EPrintColor::LightBlue: + return "\033[1;94m"; + break; + case EPrintColor::LightWhite: + return "\033[1;97m"; + break; + case EPrintColor::LightMagenta: + return "\033[1;95m"; + break; + case EPrintColor::LightCyan: + return "\033[1;96m"; + break; + case EPrintColor::LightYellow: + return "\033[1;94m"; + break; + default: + return "\033[1;37m"; // return White by default + break; + } +} diff --git a/C++/logger/singleton.h b/C++/logger/singleton.h new file mode 100644 index 000000000..62ff31e06 --- /dev/null +++ b/C++/logger/singleton.h @@ -0,0 +1,139 @@ +#pragma once + +#include + +/** + * @file Singleton.h + * @brief A thread-safe template class for implementing the Singleton design pattern. + * + * The `Singleton` class provides a simple way to create a singleton instance + * of a class using the Curiously Recurring Template Pattern (CRTP). It ensures that + * only one instance of the class exists and provides global access to that instance. + * + * Usage Example: + * @code + * class MySingleton : public Singleton + * { + * // Singleton-specific methods + * }; + * + * void SomeFunction() + * { + * MySingleton* instance = MySingleton::GetInstance(); + * } + * @endcode + * + * @tparam T The type of the singleton class that inherits from `Singleton`. + */ +template +class Singleton +{ +public: + /** + * @brief Retrieves the singleton instance. + * + * If the instance does not exist, it is created. This method returns a pointer + * to the singleton instance. + * The Double-Checked Locking Pattern for optimized thread safety, if the program uses the GetInstance + * function N times with this pattern the lock will be aquired only the first time instead of N times. + * + * @return T* A pointer to the singleton instance. + */ + static T* GetInstance() + { + if(!m_instance) + { + const std::lock_guard lock(m_lock); + if(!m_instance) + { + m_instance = new T; + } + } + return m_instance; + } + + /** + * @brief Retrieves the singleton instance by reference. + * + * If the instance does not exist, it is created. This method returns a reference + * to the singleton instance. + * The Double-Checked Locking Pattern for optimized thread safety. + * + * @return T& A reference to the singleton instance. + */ + static T& GetRef() + { + if(!m_instance) + { + const std::lock_guard lock(m_lock); + if(!m_instance) + { + m_instance = new T; + } + } + return *m_instance; + } + + /** + * @brief Destroys the singleton instance. + * + * This method deletes the singleton instance and sets the instance pointer to null. + * All references to the singleton instance will be invalid after this method is called. + * The Double-Checked Locking Pattern for optimized thread safety. + */ + static void DestroyInstance() + { + if(m_instance) + { + std::lock_guard lock(m_lock); + delete m_instance; + m_instance = nullptr; + } + } + + /** + * @brief Prevents moving or copying the singleton instance. + * + * This ensures that the singleton instance cannot be copied or moved, preserving + * the unique instance guarantee. + */ + Singleton(Singleton&& source) = delete; + Singleton(const Singleton& source) = delete; + Singleton& operator=(Singleton&& source) = delete; + const Singleton& operator=(const Singleton& source) = delete; + +protected: + /** + * @brief Protected constructor for the Singleton class. + * + * This prevents external classes from creating instances of the singleton directly. + */ + Singleton() = default; + + /** + * @brief Protected destructor for the Singleton class. + * + * This cleans up the singleton instance when it is destroyed. + */ + ~Singleton() + { + delete m_instance; + m_instance = nullptr; + } + +private: + /** + * @brief Static pointer to the singleton instance. + * + * This static member variable holds the instance of the singleton class. + * inlined so it can be class initialized + */ + inline static T* m_instance{nullptr}; + /** + * @brief Static mutex for thread safety. + * + * Avoids memory leaks as two threads can create a race condition with the m_instance. + */ + inline static std::mutex m_lock{}; +}; +