diff --git a/CMakeLists.txt b/CMakeLists.txt index 565ebad55999941996524d8dd8031ee139e7eaea..9e09a0eab161ba1079349001694904aa41e98898 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,7 +125,7 @@ if (PYBIND) add_pybind_dependency(${module_name}) ## - target_link_libraries(${pybind_module_name} PRIVATE fmt::fmt) + target_link_libraries(${pybind_module_name} PUBLIC fmt::fmt) endif() target_link_libraries(${module_name} PUBLIC fmt::fmt) diff --git a/include/aidge/utils/Log.hpp b/include/aidge/utils/Log.hpp index f92d5cf66f20e851e81867989a91ea2840b7db62..8c576fc811dcfb74168e752b0b7dc87ad2c1a482 100644 --- a/include/aidge/utils/Log.hpp +++ b/include/aidge/utils/Log.hpp @@ -9,26 +9,28 @@ * ********************************************************************************/ -#ifndef AIDGE_LOG_H_ -#define AIDGE_LOG_H_ +#ifndef AIDGE_CORE_UTILS_LOG_H_ +#define AIDGE_CORE_UTILS_LOG_H_ #include <memory> +#include <string> #include <vector> #include <fmt/format.h> -#include <fmt/ranges.h> +#include <fmt/color.h> +#ifdef PYBIND +#include <pybind11/pybind11.h> +#endif -#include "aidge/data/half.hpp" #include "aidge/utils/Attributes.hpp" -#ifdef PYBIND -#include <pybind11/pybind11.h> // get_shared_data, set_shared_data, PyIsInitialized -#endif namespace Aidge { #ifdef PYBIND namespace py = pybind11; #endif + + /** * Helper to define a context anywhere, hiding the scoped variable name * which has no relevance. @@ -36,100 +38,136 @@ namespace py = pybind11; #define AIDGE_LOG_CONTEXT(...) \ const Log::Context logContext_##__LINE__(__VA_ARGS__) -template <class U> static void discard_args(U parg) { - (void)parg; -} -template <class U, class... Us> static void discard_args(U parg, Us... pargs) { - (void)parg; - discard_args(pargs...); -} - /** - * Aidge logging class, for displaying and file logging of events. + * @brief Logging utility class for development and debugging + * + * The Log class provides a static interface for logging messages at different + * severity levels. It supports both console output (with optional colors) and + * file output. The logging behavior can be configured through environment + * variables or programmatically. + * + * Environment variables: + * - AIDGE_LOGLEVEL_CONSOLE: Set console output level (Debug,Info,Notice,Warn,Error,Fatal) + * - AIDGE_LOGLEVEL_FILE: Set file output level + * - AIDGE_LOG_COLOR: Enable/disable colored output ("off"/"OFF"/"0" to disable) + * - AIDGE_LOG_FILE: Set log file path + * + * @note Console colors are enabled by default */ class Log { - public: - enum Level { Debug = 0, Info, Notice, Warn, Error, Fatal }; +public: + /** + * @brief Log severity levels in ascending order + */ + enum Level { + Debug = 0, ///< Detailed information for debugging + Info, ///< General information about program execution + Notice, ///< Normal but significant conditions + Warn, ///< Warning messages for potentially problematic situations + Error, ///< Error messages for serious problems + Fatal ///< Critical errors that lead to program termination + }; + /** + * @brief RAII class for managing logging contexts + * + * This class allows adding temporary context information to logs. When an instance + * is created, it adds a context message that will be displayed with all logs until + * the instance is destroyed. + * + * Example: + * @code + * { + * Log::Context ctx("Processing file: {}", filename); + * Log::info("Starting process"); // Will show context + * // Context automatically removed when ctx is destroyed + * } + * @endcode + */ class Context { - public: - template <typename... Args> Context(Args &&...args) { + public: + /** + * @brief Create a new logging context + * @param args Format string and arguments for the context message + */ + template <typename... Args> + Context(Args&&... args) { Log::mContext.push_back(fmt::format(std::forward<Args>(args)...)); } + /** + * @brief Destroy the context, removing it from the active contexts + */ ~Context() { Log::mContext.pop_back(); } }; /** - * Detailed messages for debugging purposes, providing information helpful - * for developers to trace and identify issues. - * Detailed insights of what is happening in an operation, not useful for - * the end-user. The operation is performed nominally. - * @note This level is disabled at compile time for Release, therefore - * inducing no runtime overhead for Release. + * @brief Log a debug message + * @param args Format string and arguments for the message + * @note Debug messages are only compiled in debug builds */ - template <typename... Args> static void debug(Args &&...args) { -#ifndef NDEBUG - // only when compiled in Debug - log(Debug, fmt::format(std::forward<Args>(args)...)); +#ifdef NDEBUG + template <typename... Args> + static void debug(Args&&... /*args*/) { + // Debug logging disabled in release builds + } #else - discard_args(&args...); -#endif + template <typename... Args> + static void debug(Args&&... args) { + log(Debug, fmt::format(std::forward<Args>(args)...)); } +#endif /** - * Messages that provide a record of the normal operation, about - * the application's state, progress, or important events. - * Reports normal start, end and key steps in an operation. The operation - * is performed nominally. + * @brief Log an info message + * @param args Format string and arguments for the message */ - template <typename... Args> static void info(Args &&...args) { + template <typename... Args> + static void info(Args&&... args) { log(Info, fmt::format(std::forward<Args>(args)...)); } /** - * Applies to normal but significant conditions that may require - * monitoring, like unusual or normal fallback events. Reports specific - * paths in an operation. The operation can still be performed normally. + * @brief Log a notice message + * @param args Format string and arguments for the message */ - template <typename... Args> static void notice(Args &&...args) { + template <typename... Args> + static void notice(Args&&... args) { log(Notice, fmt::format(std::forward<Args>(args)...)); } /** - * Indicates potential issues or situations that may lead to errors but do - * not necessarily cause immediate problems. - * Some specific steps of the operation could not be performed, but it can - * still provide an exploitable result. + * @brief Log a warning message + * @param args Format string and arguments for the message */ - template <typename... Args> static void warn(Args &&...args) { + template <typename... Args> + static void warn(Args&&... args) { log(Warn, fmt::format(std::forward<Args>(args)...)); } /** - * Signifies a problem or unexpected condition that the application can - * recover from, but attention is needed to prevent further issues. - * The operation could not be performed, but it does not prevent potential - * further operations. + * @brief Log an error message + * @param args Format string and arguments for the message */ - template <typename... Args> static void error(Args &&...args) { + template <typename... Args> + static void error(Args&&... args) { log(Error, fmt::format(std::forward<Args>(args)...)); } /** - * Represents a critical error or condition that leads to the termination - * of the application, indicating a severe and unrecoverable problem. The - * operation could not be performed and any further operation is - * impossible. + * @brief Log a fatal error message + * @param args Format string and arguments for the message */ - template <typename... Args> static void fatal(Args &&...args) { + template <typename... Args> + static void fatal(Args&&... args) { log(Fatal, fmt::format(std::forward<Args>(args)...)); } /** - * Set the minimum log level displayed in the console. + * @brief Set the minimum level for console output + * @param level Minimum level to display */ static void setConsoleLevel(Level level) { mConsoleLevel = level; @@ -137,96 +175,80 @@ class Log { // This means each static instance is separated and does not communicate. // To allow the communication between the different modules, we use PyCapsule. // https://docs.python.org/3/extending/extending.html#providing-a-c-api-for-an-extension-module - #ifdef PYBIND - #define _CRT_SECURE_NO_WARNINGS +#ifdef PYBIND +#define _CRT_SECURE_NO_WARNINGS if (Py_IsInitialized()){ // Note: Setting mConsoleLevel and not level is important // to avoid garbage collection of the pointer. py::set_shared_data("consoleLevel", &mConsoleLevel); } - #endif // PYBIND - +#endif // PYBIND } - /** - * @brief Get the curent Console Level. - * - * @return const Level - */ - static Level getConsoleLevel(){ - #ifdef PYBIND - #define _CRT_SECURE_NO_WARNINGS + + static Level getConsoleLevel() { +#ifdef PYBIND +#define _CRT_SECURE_NO_WARNINGS if (Py_IsInitialized()){ auto shared_data = reinterpret_cast<Level *>(py::get_shared_data("consoleLevel")); if (shared_data) mConsoleLevel = *shared_data; } - #endif // PYBIND +#endif // PYBIND return mConsoleLevel; } - /** - * Set or disable colors on console. - * Initial value should be assumed true. - */ - static void setConsoleColor(bool enabled) { - mConsoleColor = enabled; - } /** - * Set the minimum log level saved in the log file. + * @brief Enable or disable colored console output + * @param enabled True to enable colors, false to disable */ - constexpr static void setFileLevel(Level level) { - mFileLevel = level; - } + static void setConsoleColor(bool enabled) noexcept { mConsoleColor = enabled; } /** - * Set the log file name. - * Close the current log file and open the one with the new file name. - * If empty, stop logging into a file. + * @brief Set the minimum level for file output + * @param level Minimum level to write to file */ - static void setFileName(const std::string &fileName) { - if (fileName != mFileName) { - mFileName = fileName; - mFile.release(); - - if (!fileName.empty()) { - initFile(fileName); - } - } - } + static void setFileLevel(Level level) noexcept { mFileLevel = level; } /** - * @struct fcloseDeleter - * @brief Custom deleter to prevent compiler warnings when using - * std::unique_ptr with FILE*. - * - * Using function pointers with attributes (e.g., __attribute__((nonnull))) - * as deleters can trigger warnings like -Wignored-attributes. By defining a - * custom deleter struct, these attributes are preserved, eliminating the - * warnings. + * @brief Set the log file path + * @param fileName Path to log file (empty to disable file logging) + * @throw std::runtime_error if file cannot be opened */ - struct fcloseDeleter { - void operator()(FILE *f) const noexcept { - std::fclose(f); - } - }; + static void setFileName(const std::string& fileName); private: static void log(Level level, const std::string& msg); static void initFile(const std::string& fileName); - static Level mConsoleLevel; - static bool mConsoleColor; - static Level mFileLevel; - static std::string mFileName; - static std::unique_ptr<FILE, fcloseDeleter> mFile; - static std::vector<std::string> mContext; + struct fcloseDeleter { + void operator()(FILE* f) const noexcept { std::fclose(f); } + }; + + static Level mConsoleLevel; ///< Minimum level for console output + static bool mConsoleColor; ///< Whether to use colored output + static Level mFileLevel; ///< Minimum level for file output + static std::string mFileName; ///< Path to log file + static std::unique_ptr<FILE, fcloseDeleter> mFile; ///< File handle + static std::vector<std::string> mContext; ///< Stack of active contexts }; + } // namespace Aidge +// Formatter specialization for Log::Level +template <> +struct fmt::formatter<Aidge::Log::Level> : formatter<const char*> { + template <typename FormatContext> + auto format(const Aidge::Log::Level& level, FormatContext& ctx) const { + const char* name = EnumStrings<Aidge::Log::Level>::data[static_cast<int>(level)]; + return formatter<const char*>::format(name, ctx); + } +}; + namespace { template <> -const char *const EnumStrings<Aidge::Log::Level>::data[] = - {"Debug", "Info", "Notice", "Warn", "Error", "Fatal"}; +const char* const EnumStrings<Aidge::Log::Level>::data[] = { + "DEBUG", "INFO", "NOTICE", "WARNING", "ERROR", "FATAL" +}; } -#endif // AIDGE_LOG_H_ +#endif // AIDGE_CORE_UTILS_LOG_H_ diff --git a/src/graph/Node.cpp b/src/graph/Node.cpp index 384e946c674641f7d498d8c5745dcc745f34d751..0c5fbfdcb1284e5b4158190f588b1ca7e529aa4d 100644 --- a/src/graph/Node.cpp +++ b/src/graph/Node.cpp @@ -264,7 +264,7 @@ void Aidge::Node::addChildOp(std::shared_ptr<Node> otherNode, const IOIndex_t ou "Output index (#{}) of the node {} (of type {}) is out of bound (it has {} outputs), when trying to add the child node {} (of type {})", outId, name(), type(), nbOutputs(), otherNode->name(), otherNode->type()); if (otherNode->input(otherInId).second != gk_IODefaultIndex) { - Log::notice("Notice: the {}-th Parent of the child node {} (of type {}) already existed", otherInId, otherNode->name(), otherNode->type()); + Log::notice("the {}-th Parent of the child node {} (of type {}) already existed", otherInId, otherNode->name(), otherNode->type()); } // manage tensors and potential previous parent otherNode->setInputId(otherInId, outId); diff --git a/src/recipes/MatMulToFC.cpp b/src/recipes/MatMulToFC.cpp index 8d902c680b8fa0d30a873b6f355734ce19d608f5..c35b2a64a6bf1186ba43b5ffb696302d0f2c1679 100644 --- a/src/recipes/MatMulToFC.cpp +++ b/src/recipes/MatMulToFC.cpp @@ -71,7 +71,7 @@ void Aidge::matMulToFC(std::shared_ptr<Aidge::Node> matmulNode, std::shared_ptr< { // If both inputs are producers, there is an ambiguity, but both options // result in a correct solution. - Log::notice("Notice: both MatMul inputs are Producers, assume data at input#0 and weights at input#1."); + Log::notice("both MatMul inputs are Producers, assume data at input#0 and weights at input#1."); weight = matmulNode->getParent(1); } AIDGE_ASSERT(weight != nullptr, "Could not deduce weight input for MatMul operator."); diff --git a/src/utils/Log.cpp b/src/utils/Log.cpp index 383fdae173814399d259fdab8858be9093204f70..5fc7a604fb795786511c09c1ce46437917e10de9 100644 --- a/src/utils/Log.cpp +++ b/src/utils/Log.cpp @@ -10,101 +10,144 @@ ********************************************************************************/ #include "aidge/utils/Log.hpp" -#include "aidge/utils/ErrorHandling.hpp" -#include <cstdlib> +#include <fmt/core.h> -#include <fmt/color.h> -#include <fmt/chrono.h> +#include <cstdlib> // std::getenv +#include <memory> +#include <string> +#include <vector> -Aidge::Log::Level Aidge::Log::mConsoleLevel = []() { - const char* logLevel = std::getenv("AIDGE_LOGLEVEL_CONSOLE"); - if (logLevel != nullptr) { - for (std::size_t i = 0; i < size(EnumStrings<Log::Level>::data); ++i) { - if (std::string(logLevel) == EnumStrings<Log::Level>::data[i]) { - return static_cast<Log::Level>(i); - } - } +namespace Aidge { + +/** + * @brief Initialize console level from environment or default to Info + */ +Log::Level Log::mConsoleLevel = []() { + if (const char* level = std::getenv("AIDGE_LOGLEVEL_CONSOLE")) { + return level[0] == 'D' ? Debug : + level[0] == 'I' ? Info : + level[0] == 'N' ? Notice : + level[0] == 'W' ? Warn : + level[0] == 'E' ? Error : + level[0] == 'F' ? Fatal : Info; } return Info; }(); -bool Aidge::Log::mConsoleColor = []() { - const char* logColor = std::getenv("AIDGE_LOG_COLOR"); - if (logColor == nullptr) - return true; - auto logColorStr = std::string(logColor); - if (logColorStr == "off" || logColorStr == "OFF" || - logColorStr == "0") - return false; - return true; + +/** + * @brief Initialize color setting from environment or default to enabled + */ +bool Log::mConsoleColor = []() { + const char* color = std::getenv("AIDGE_LOG_COLOR"); + return !color || (std::string(color) != "off" && + std::string(color) != "OFF" && + std::string(color) != "0"); }(); -Aidge::Log::Level Aidge::Log::mFileLevel = []() { - const char* logLevel = std::getenv("AIDGE_LOGLEVEL_FILE"); - if (logLevel != nullptr) { - for (std::size_t i = 0; i < size(EnumStrings<Log::Level>::data); ++i) { - if (std::string(logLevel) == EnumStrings<Log::Level>::data[i]) { - return static_cast<Log::Level>(i); - } - } + +/** + * @brief Initialize file level from environment or default to Debug + */ +Log::Level Log::mFileLevel = []() { + if (const char* level = std::getenv("AIDGE_LOGLEVEL_FILE")) { + return level[0] == 'D' ? Debug : + level[0] == 'I' ? Info : + level[0] == 'N' ? Notice : + level[0] == 'W' ? Warn : + level[0] == 'E' ? Error : + level[0] == 'F' ? Fatal : Debug; } return Debug; }(); -std::string Aidge::Log::mFileName = []() { - const char* logFile = std::getenv("AIDGE_LOG_FILE"); - if (logFile != nullptr) { - return std::string(logFile); - } - return std::string(); + +/** + * @brief Initialize file path from environment + */ +std::string Log::mFileName = []() { + const char* file = std::getenv("AIDGE_LOG_FILE"); + return file ? std::string(file) : std::string(); }(); -std::unique_ptr<FILE, Aidge::Log::fcloseDeleter> Aidge::Log::mFile {nullptr, Aidge::Log::fcloseDeleter()}; -std::vector<std::string> Aidge::Log::mContext; - -void Aidge::Log::log(Level level, const std::string& msg) { - if (level >= getConsoleLevel()) { - // Apply log level style only for console. - // Styles that were already applied to msg with fmt are kept also in - // the log file. - const auto modifier - = !mConsoleColor ? fmt::text_style() - : (level == Debug) ? fmt::fg(fmt::color::gray) - : (level == Notice) ? fmt::fg(fmt::color::medium_purple) - : (level == Warn) ? fmt::fg(fmt::color::orange) - : (level == Error) ? fmt::fg(fmt::color::red) - : (level == Fatal) ? fmt::bg(fmt::color::red) - : fmt::text_style(); +std::unique_ptr<FILE, Log::fcloseDeleter> Log::mFile{nullptr}; +std::vector<std::string> Log::mContext; + +/** + * @brief Internal logging implementation + * @param level Severity level of the message + * @param msg The message to log + */ +void Log::log(Level level, const std::string& msg) { + /** + * @brief Get the terminal color for a log level + */ + const auto getColor = [](Level lvl) { + switch (lvl) { + case Debug: return fmt::terminal_color::green; + case Info: return fmt::terminal_color::blue; + case Notice: return fmt::terminal_color::bright_blue; + case Warn: return fmt::terminal_color::yellow; + case Error: return fmt::terminal_color::red; + case Fatal: return fmt::terminal_color::bright_magenta; + default: return fmt::terminal_color::white; + } + }; + + /** + * @brief Get the string representation of a log level + */ + const auto levelStr = EnumStrings<Level>::data[static_cast<std::size_t>(level)]; + + if (level >= mConsoleLevel) { + // Print active contexts for (const auto& context : mContext) { fmt::println("Context: {}", context); } - fmt::println("{}", fmt::styled(msg, modifier)); + // Print message with colored level + if (mConsoleColor) { + fmt::print("["); + fmt::print(fg(getColor(level)), "{}", levelStr); + fmt::print("] - {}\n", msg); + } else { + fmt::print("[{}] - {}\n", levelStr, msg); + } } if (level >= mFileLevel && !mFileName.empty()) { if (!mFile) { initFile(mFileName); } - + // Write contexts and message to file for (const auto& context : mContext) { - fmt::println("Context: {}", context); + fmt::println(mFile.get(), "Context: {}", context); } - - fmt::println(mFile.get(), "{}", msg); + fmt::println(mFile.get(), "{}: {}", levelStr, msg); } } -void Aidge::Log::initFile(const std::string& fileName) { - FILE* rawFile = std::fopen(fileName.c_str(), "a"); - - if (!rawFile) { - mFileName.clear(); // prevents AIDGE_THROW_OR_ABORT() to try to log into file - AIDGE_THROW_OR_ABORT(std::runtime_error, - "Could not create log file: {}", fileName); +/** + * @brief Initialize or re-initialize the log file + * @param fileName Path to the log file + * @throw std::runtime_error if file cannot be opened + */ +void Log::initFile(const std::string& fileName) { + if (FILE* file = std::fopen(fileName.c_str(), "a")) { + mFile.reset(file); + } else { + mFileName.clear(); + throw std::runtime_error( + fmt::format("Could not create log file: {}", fileName)); } +} - // Assign the new FILE* with the deleter - mFile = std::unique_ptr<FILE, fcloseDeleter>(rawFile, fcloseDeleter()); - - const std::time_t t = std::time(nullptr); - fmt::println(mFile.get(), "###### {:%Y-%m-%d %H:%M:%S} ######", fmt::localtime(t)); +void Log::setFileName(const std::string& fileName) { + if (fileName != mFileName) { + mFileName = fileName; + mFile.reset(); + if (!fileName.empty()) { + initFile(fileName); + } + } } + +} // namespace Aidge diff --git a/unit_tests/operator/Test_Squeeze_Op.cpp b/unit_tests/operator/Test_Squeeze_Op.cpp index f5fd071984198a5982b36ca2cb16884657126957..f1261fffb756de17891d8c268812b2bb7a196c0a 100644 --- a/unit_tests/operator/Test_Squeeze_Op.cpp +++ b/unit_tests/operator/Test_Squeeze_Op.cpp @@ -446,9 +446,9 @@ TEST_CASE("[core/operator] Squeeze(forward)", "[Squeeze][forward]") { delete[] array_in; } - fmt::print("INFO: GlobalAveragePooling total execution time: {}µs\n", duration.count()); - fmt::print("INFO: Number of operations : {}\n", number_of_operation); - fmt::print("INFO: Operation / µs = {}\n", number_of_operation / duration.count()); + Log::info("GlobalAveragePooling total execution time: {}µs\n", duration.count()); + Log::info("Number of operations : {}\n", number_of_operation); + Log::info("Operation / µs = {}\n", number_of_operation / duration.count()); } } } diff --git a/unit_tests/operator/Test_Unsqueeze_Op.cpp b/unit_tests/operator/Test_Unsqueeze_Op.cpp index 650cbaf3ebb9cacff4a6b16a14706e9d5bf13f6e..5cedcfdfa9c7608aa99426f7c4b496537c43c324 100644 --- a/unit_tests/operator/Test_Unsqueeze_Op.cpp +++ b/unit_tests/operator/Test_Unsqueeze_Op.cpp @@ -373,9 +373,9 @@ TEST_CASE("[core/operator] Unsqueeze(forward)", "[Unsqueeze][forward]") { delete[] array_in; } } - fmt::print("INFO: GlobalAveragePooling total execution time: {}µs\n", duration.count()); - fmt::print("INFO: Number of operations : {}\n", number_of_operation); - fmt::print("INFO: Operation / µs = {}\n", number_of_operation / duration.count()); + Log::info("GlobalAveragePooling total execution time: {}µs\n", duration.count()); + Log::info("Number of operations : {}\n", number_of_operation); + Log::info("Operation / µs = {}\n", number_of_operation / duration.count()); } } diff --git a/unit_tests/utils/Test_Log.cpp b/unit_tests/utils/Test_Log.cpp index 3d8e672b84f5055a12185c3684c34bd888f0545b..1c44e232f9c8ee05bf6f8147d7789131c5a0d803 100644 --- a/unit_tests/utils/Test_Log.cpp +++ b/unit_tests/utils/Test_Log.cpp @@ -17,15 +17,22 @@ using namespace Aidge; -TEST_CASE("[core/log] Log") { +TEST_CASE("[core/log] Log", "[Logger]") { SECTION("TestLog") { - Log::setConsoleLevel(Log::Debug); - Log::debug("debug"); + Log::setConsoleLevel(Log::Level::Debug); + Log::debug("this is a debug message"); Log::debug("{}", fmt::styled("green debug", fmt::fg(fmt::color::green))); - Log::info("info"); - Log::notice("notice"); - Log::warn("warn"); - Log::error("error"); + Log::info("this is an info message"); + Log::notice("this is a notice message"); + Log::warn("this is a warn message"); + Log::error("this is an error message"); + AIDGE_LOG_CONTEXT("Testing the context of logger"); Log::fatal("fatal"); + Log::debug("Now debug messages are supposed to [{}].", fmt::styled("appear", fmt::emphasis::italic)); + Log::info("Current consol level is {}", Log::getConsoleLevel()); + Log::setConsoleLevel(Log::Level::Warn); + Log::notice("This message should not appear."); + + } }