From 7bcce1db5fe2b28d0aba3f04ad47235914ba3827 Mon Sep 17 00:00:00 2001 From: bergmann Date: Sat, 22 Jun 2019 03:16:18 +0200 Subject: [PATCH] * Initial commit * Moved and refactored code from old project cpputils --- .gitmodules | 3 + CMakeLists.txt | 53 ++ cmake/cpplogging-config.cmake | 7 + cmake/cpplogging-var.cmake | 20 + cmake/modules | 1 + include/cpplogging/interface.h | 4 + include/cpplogging/interface/logger.h | 197 +++++ include/cpplogging/interface/logger.inl | 118 +++ include/cpplogging/manager.h | 7 + include/cpplogging/manager/consumer.h | 6 + .../cpplogging/manager/consumer/consumer.h | 82 +++ .../cpplogging/manager/consumer/consumer.inl | 26 + .../manager/consumer/consumer_stream.h | 52 ++ include/cpplogging/manager/logger_impl.h | 62 ++ include/cpplogging/manager/manager.h | 158 ++++ include/cpplogging/manager/manager.inl | 44 ++ include/cpplogging/manager/matcher.h | 6 + include/cpplogging/manager/matcher/matcher.h | 45 ++ .../cpplogging/manager/matcher/matcher_all.h | 35 + .../manager/matcher/matcher_default_logger.h | 36 + .../manager/matcher/matcher_regex.h | 50 ++ include/cpplogging/manager/rule.h | 70 ++ include/cpplogging/manager/rule.inl | 50 ++ include/cpplogging/types.h | 43 ++ src/CMakeLists.txt | 112 +++ src/cpplogging/interface/logger.cpp | 7 + src/cpplogging/manager/consumer/consumer.cpp | 17 + .../manager/consumer/consumer_stream.cpp | 25 + .../manager/consumer/formatting.cpp | 681 ++++++++++++++++++ src/cpplogging/manager/logger_impl.cpp | 51 ++ src/cpplogging/manager/manager.cpp | 140 ++++ src/cpplogging/manager/matcher/matcher.cpp | 9 + .../manager/matcher/matcher_all.cpp | 9 + .../matcher/matcher_default_logger.cpp | 11 + .../manager/matcher/matcher_regex.cpp | 14 + test/CMakeLists.txt | 43 ++ test/cpplogging/cpplogging_tests.cpp | 127 ++++ 37 files changed, 2421 insertions(+) create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 cmake/cpplogging-config.cmake create mode 100644 cmake/cpplogging-var.cmake create mode 160000 cmake/modules create mode 100644 include/cpplogging/interface.h create mode 100644 include/cpplogging/interface/logger.h create mode 100644 include/cpplogging/interface/logger.inl create mode 100644 include/cpplogging/manager.h create mode 100644 include/cpplogging/manager/consumer.h create mode 100644 include/cpplogging/manager/consumer/consumer.h create mode 100644 include/cpplogging/manager/consumer/consumer.inl create mode 100644 include/cpplogging/manager/consumer/consumer_stream.h create mode 100644 include/cpplogging/manager/logger_impl.h create mode 100644 include/cpplogging/manager/manager.h create mode 100644 include/cpplogging/manager/manager.inl create mode 100644 include/cpplogging/manager/matcher.h create mode 100644 include/cpplogging/manager/matcher/matcher.h create mode 100644 include/cpplogging/manager/matcher/matcher_all.h create mode 100644 include/cpplogging/manager/matcher/matcher_default_logger.h create mode 100644 include/cpplogging/manager/matcher/matcher_regex.h create mode 100644 include/cpplogging/manager/rule.h create mode 100644 include/cpplogging/manager/rule.inl create mode 100644 include/cpplogging/types.h create mode 100644 src/CMakeLists.txt create mode 100644 src/cpplogging/interface/logger.cpp create mode 100644 src/cpplogging/manager/consumer/consumer.cpp create mode 100644 src/cpplogging/manager/consumer/consumer_stream.cpp create mode 100644 src/cpplogging/manager/consumer/formatting.cpp create mode 100644 src/cpplogging/manager/logger_impl.cpp create mode 100644 src/cpplogging/manager/manager.cpp create mode 100644 src/cpplogging/manager/matcher/matcher.cpp create mode 100644 src/cpplogging/manager/matcher/matcher_all.cpp create mode 100644 src/cpplogging/manager/matcher/matcher_default_logger.cpp create mode 100644 src/cpplogging/manager/matcher/matcher_regex.cpp create mode 100644 test/CMakeLists.txt create mode 100644 test/cpplogging/cpplogging_tests.cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a2cd849 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "/home/bergmann/projects/TotoStarOnline/projects/worker/cpplogging/cmake/modules"] + path = /home/bergmann/projects/TotoStarOnline/projects/worker/cpplogging/cmake/modules + url = b3rgmann@git.bergmann89.de:cpp/CmakeModules.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1b83d0d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,53 @@ +# Initialize CMake ################################################################################ + +CMake_Minimum_Required ( VERSION 3.12.0 FATAL_ERROR ) + +# Set CMAKE_BUILD_TYPE +If ( NOT CMAKE_BUILD_TYPE ) + Set ( CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build!" FORCE ) +EndIf ( NOT CMAKE_BUILD_TYPE ) +Set_Property ( CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo MinSizeRel ) + +# Set CMAKE_MODULE_PATH +If ( EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/" ) + Set ( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/" ) +EndIf ( ) +If ( EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/" ) + Set ( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules" ) +EndIf ( ) + +# Project ######################################################################################### + +Include ( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cpplogging-var.cmake" ) +Project ( cpplogging + DESCRIPTION "A simple library" + VERSION "${CPPLOGGING_VERSION}" ) +Include ( CTest ) +Include ( GNUInstallDirs ) + +# Subdirectories +Add_SubDirectory ( ${CMAKE_CURRENT_SOURCE_DIR}/src ) +Add_SubDirectory ( ${CMAKE_CURRENT_SOURCE_DIR}/test ) + +# Install +Include ( CMakePackageConfigHelpers ) +Write_Basic_Package_Version_File ( "${CMAKE_CURRENT_BINARY_DIR}/cmake/cpplogging-config-version.cmake" + VERSION ${CPPLOGGING_VERSION} + COMPATIBILITY AnyNewerVersion ) +Configure_File ( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cpplogging-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/cmake/cpplogging-config.cmake" + @ONLY ) + +Set ( ConfigPackageLocation "${CPPLOGGING_INSTALL_DIR_SHARE}/cmake" ) +Install ( EXPORT cpplogging + NAMESPACE cpplogging:: + DESTINATION ${ConfigPackageLocation} ) +Install ( FILES + "${CMAKE_CURRENT_BINARY_DIR}/cmake/cpplogging-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/cmake/cpplogging-config-version.cmake" + DESTINATION + ${ConfigPackageLocation} + COMPONENT + Devel ) diff --git a/cmake/cpplogging-config.cmake b/cmake/cpplogging-config.cmake new file mode 100644 index 0000000..183873b --- /dev/null +++ b/cmake/cpplogging-config.cmake @@ -0,0 +1,7 @@ +# cpplogging-config.cmake - package configuration file + +Get_Filename_Component ( CURRENT_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH ) +Include ( ${CURRENT_DIR}/cpplogging.cmake ) + +Include ( CMakeFindDependencyMacro ) +Find_Dependency ( cppcore ) diff --git a/cmake/cpplogging-var.cmake b/cmake/cpplogging-var.cmake new file mode 100644 index 0000000..b784fff --- /dev/null +++ b/cmake/cpplogging-var.cmake @@ -0,0 +1,20 @@ +# Version +Set ( CPPLOGGING_VERSION_MAJOR 1 ) +Set ( CPPLOGGING_VERSION_MINOR 0 ) +Set ( CPPLOGGING_VERSION_PATCH 0 ) +Set ( CPPLOGGING_VERSION_BUILD 0 ) +Set ( CPPLOGGING_VERSION_SHORT "${CPPLOGGING_VERSION_MAJOR}.${CPPLOGGING_VERSION_MINOR}" ) +Set ( CPPLOGGING_VERSION "${CPPLOGGING_VERSION_SHORT}.${CPPLOGGING_VERSION_PATCH}.${CPPLOGGING_VERSION_BUILD}" ) +Set ( CPPLOGGING_NAME "cpplogging-${CPPLOGGING_VERSION_SHORT}" ) +Set ( CPPLOGGING_OUTPUTNAME "helloworld" ) + +# Install directories +Set ( CPPLOGGING_INSTALL_DIR_INCLUDE "include/${CPPLOGGING_NAME}" ) +Set ( CPPLOGGING_INSTALL_DIR_LIB "lib" ) +Set ( CPPLOGGING_INSTALL_DIR_SHARE "share/${CPPLOGGING_NAME}" ) + +# C Standard +Set ( CMAKE_C_STANDARD 11 ) +Set ( CMAKE_CXX_STANDARD 17 ) +Set ( CMAKE_C_STANDARD_REQUIRED ON ) +Set ( CMAKE_CXX_STANDARD_REQUIRED ON ) diff --git a/cmake/modules b/cmake/modules new file mode 160000 index 0000000..1a32531 --- /dev/null +++ b/cmake/modules @@ -0,0 +1 @@ +Subproject commit 1a32531aef2deeebd5637b1873bc4e976628801c diff --git a/include/cpplogging/interface.h b/include/cpplogging/interface.h new file mode 100644 index 0000000..dac2b28 --- /dev/null +++ b/include/cpplogging/interface.h @@ -0,0 +1,4 @@ +#pragma once + +#include "interface/logger.h" +#include "interface/types.h" diff --git a/include/cpplogging/interface/logger.h b/include/cpplogging/interface/logger.h new file mode 100644 index 0000000..9118f81 --- /dev/null +++ b/include/cpplogging/interface/logger.h @@ -0,0 +1,197 @@ +#pragma once + +#include +#include + +#define cpplogging_log(p_logger, p_level) \ + if (p_logger.is_enabled(::cpplogging::log_level::p_level)) \ + p_logger.log() \ + .level(::cpplogging::log_level::p_level) \ + .file(__FILE__) \ + .line(__LINE__) + +namespace cpplogging +{ + + struct logger; + + namespace __impl + { + + /** + * @brief Helper class to create a log entry. + */ + struct log_helper + : public std::ostringstream + { + private: + logger& _logger; + log_entry_ptr_s _entry; + + private: + /** + * @brief Move constructor. + */ + log_helper(log_helper&&) = delete; + + /** + * @brief Copy constructor. + */ + log_helper(const log_helper&) = delete; + + public: + /** + * @brief Constructor. + * + * @param[in] p_logger Logger this log helper belongs to. + */ + inline log_helper(logger& p_logger); + + /** + * @brief Constructor. + * + * @param[in] p_logger Logger this log helper belongs to. + * @param[in] p_entry Log entry to create. + */ + inline log_helper(logger& p_logger, const log_entry_ptr_s& p_entry); + + /** + * @brief Destructor. Will finally write the log entry. + */ + inline ~log_helper(); + + /** + * @brief Set the log level of the entry, + */ + inline log_helper& level(log_level level); + + /** + * @brief Set the sender of the log entry. + */ + inline log_helper& sender(const void * sender); + + /** + * @brief Set the filename of the log entry. + */ + inline log_helper& file(const char * file); + + /** + * @brief Set the line number of the log entry. + */ + inline log_helper& line(int line); + + /** + * @brief Set the message of the log entry. + */ + inline log_helper& message(const std::string& msg); + + /** + * @brief Add the passed string to the log message. + */ + inline log_helper& add_message(const std::string& msg); + + /** + * @brief Set the message of the entry using format string. + */ + template + inline log_helper& format(const std::string& fmt, T_args&&... args); + + /** + * @brief Add a message to the entry using format string. + */ + template + inline log_helper& add_format(const std::string& fmt, T_args&&... args); + + private: + /** + * @brief Format a string. + * + * @tparam T_args Arguments of the format string. + * + * @param[in] fmt Format string. + * @param[in] sz Size of the buffer to use for the formatting. + * @param[in] args Arguments of the format. + */ + template + static inline std::string format(const std::string& fmt, size_t sz, T_args&&... args); + }; + + } + + /** + * @brief Class to create log messages. + */ + struct logger + { + public: + friend __impl::log_helper; + + private: + std::string _name; //!< name of the logger + + private: + /** + * @brief Move constructor. + */ + logger(logger&&) = delete; + + /** + * @brief Copy constructor. + */ + logger(const logger&) = delete; + + public: + /** + * @brief Constructor. + */ + inline logger(const std::string name); + + /** + * @brief Destructor. + */ + virtual ~logger() = default; + + /** + * @brief Get the name of the logger. + */ + inline const std::string& name() const; + + /** + * @brief Check if the passed log level is enabled. + * + * @param[in] level Level to check. + * + * @retval true The passed log level is enabled. + * @retval false The passed log level is disabled. + */ + virtual bool is_enabled(log_level level) const = 0; + + /** + * @brief Write a log entry, + * + * @return Log helper to create log entry. + */ + inline __impl::log_helper log(); + + protected: + /** + * @brief Write a log entry. + * + * @param[in] entry Log entry to write. + */ + virtual void write_entry(const log_entry_ptr_s& entry) const = 0; + + public: + /** + * @brief Get the instance on an logger with the passed name. + * + * @param[in] name Name of the loggger. + * + * @return Instance of the requested logger. + */ + static logger& get(const std::string& name = ""); + }; + +} + +#include "logger.inl" diff --git a/include/cpplogging/interface/logger.inl b/include/cpplogging/interface/logger.inl new file mode 100644 index 0000000..c9dcbca --- /dev/null +++ b/include/cpplogging/interface/logger.inl @@ -0,0 +1,118 @@ +#pragma once + +#include "logger.h" + +namespace cpplogging +{ + + namespace __impl + { + + /* log_helper */ + + log_helper + ::log_helper(logger& p_logger) + : _logger (p_logger) + , _entry (new log_entry()) + { _entry->name = p_logger.name(); } + + log_helper + ::log_helper(logger& p_logger, const log_entry_ptr_s& p_entry) + : _logger (p_logger) + , _entry (p_entry) + { _entry->name = p_logger.name(); } + + log_helper + ::~log_helper() + { + _entry->message += str(); + _logger.write_entry(_entry); + } + + log_helper& log_helper + ::level(log_level level) + { + _entry->level = level; + return *this; + } + + log_helper& log_helper + ::sender(const void * sender) + { + _entry->sender = sender; + return *this; + } + + log_helper& log_helper + ::file(const char * file) + { + _entry->file = file; + return *this; + } + + log_helper& log_helper + ::line(int line) + { + _entry->line = line; + return *this; + } + + log_helper& log_helper + ::message(const std::string& msg) + { + _entry->message = msg; + return *this; + } + + log_helper& log_helper + ::add_message(const std::string& msg) + { + _entry->message += msg; + return *this; + } + + template + log_helper& log_helper + ::format(const std::string& fmt, T_args&&... args) + { + _entry->message = format(fmt, 0x8000, std::forward(args)...); + return *this; + } + + template + log_helper& log_helper + ::add_format(const std::string& fmt, T_args&&... args) + { + _entry->message += format(fmt, 0x8000, std::forward(args)...); + return *this; + } + + template + std::string log_helper + ::format(const std::string& fmt, size_t sz, T_args&&... args) + { + std::unique_ptr buff(static_cast(malloc(sz)), &free); + auto len = snprintf(buff.get(), sz, fmt.c_str(), std::forward(args)...); + if (len < 0) + throw std::runtime_error("unable to format string"); + return std::string(buff.get(), len); + } + + } + + /* logger */ + + logger + ::logger(const std::string name) + : _name(name) + { } + + const std::string& logger + ::name() const + { return _name; } + + __impl::log_helper logger + ::log() + { return __impl::log_helper(*this); } + +} diff --git a/include/cpplogging/manager.h b/include/cpplogging/manager.h new file mode 100644 index 0000000..d750b78 --- /dev/null +++ b/include/cpplogging/manager.h @@ -0,0 +1,7 @@ +#pragma once + +#include "manager/manager.h" +#include "manager/consumer.h" +#include "manager/logger_impl.h" +#include "manager/matcher.h" +#include "manager/rule.h" diff --git a/include/cpplogging/manager/consumer.h b/include/cpplogging/manager/consumer.h new file mode 100644 index 0000000..affc8e5 --- /dev/null +++ b/include/cpplogging/manager/consumer.h @@ -0,0 +1,6 @@ +#pragma once + +#include "consumer/consumer.h" +#include "consumer/consumer_stream.h" + +#include "consumer/consumer.inl" diff --git a/include/cpplogging/manager/consumer/consumer.h b/include/cpplogging/manager/consumer/consumer.h new file mode 100644 index 0000000..b0e454b --- /dev/null +++ b/include/cpplogging/manager/consumer/consumer.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +#include + +namespace cpplogging +{ + + /** + * @brief Class to consume log entries. + */ + struct consumer + { + private: + using format_type = std::vector; + + protected: + std::string _name; //!< name of the consumer + format_type _format; //!< parsed logging format + + /** + * @brief Get the default log format. + */ + static inline const std::string& default_format(); + + /** + * @brief Parse the given format and generate a pre-compiled list of strings. + * + * @param[in] format Format to parse. + * + * @return Precompiled format. + */ + static format_type parse_format(const std::string& format); + + /** + * @brief Write a log entry to the passed stream using the precompiled format. + * + * @param[in] e Entry to write to stream. + * @param[in] f Precompiled format to use for writing. + * @param[in] os Stream to write entry to. + */ + static void write_formatted(const log_entry& e, const format_type& f, std::ostream& os); + + public: + /** + * @brief Constructor. + * + * @param[in] name Name of the consumer. + * @param[in] auto_register Automatically register the consumer. + * @param[in] format Format to use for logging. + */ + consumer( + const std::string& name, + bool auto_register, + const std::string& format = default_format()); + + /** + * @breif Destructor. + */ + virtual ~consumer(); + + /** + * @brief Get the name of the logger. + */ + inline const std::string& name() const; + + /** + * @brief Consume a log entry. + * + * @param[in] entry Log entry to consume. + */ + virtual void write_entry(const log_entry_ptr_s& entry) const = 0; + }; + + using consumer_ptr_u = std::unique_ptr; + +} + +#include "consumer.inl" diff --git a/include/cpplogging/manager/consumer/consumer.inl b/include/cpplogging/manager/consumer/consumer.inl new file mode 100644 index 0000000..2368f01 --- /dev/null +++ b/include/cpplogging/manager/consumer/consumer.inl @@ -0,0 +1,26 @@ +#pragma once + +#include "consumer.h" + +namespace cpplogging +{ + + /* consumer */ + + const std::string& consumer + ::default_format() + { + static const std::string value( + "[${runtime:-016.6f}] " + "${level:5S} " + "${sender:016x}@${thread:016x} " + "${filename:-25}:${line:5d}$n" + "${message}"); + return value; + } + + const std::string& consumer + ::name() const + { return _name; } + +} diff --git a/include/cpplogging/manager/consumer/consumer_stream.h b/include/cpplogging/manager/consumer/consumer_stream.h new file mode 100644 index 0000000..8e42940 --- /dev/null +++ b/include/cpplogging/manager/consumer/consumer_stream.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include "consumer.h" + +namespace cpplogging +{ + + /** + * @brief Consumer that writes the log entries to a stream, + */ + struct consumer_stream + : public consumer + { + private: + using stream_ptr_u = std::unique_ptr; + + private: + mutable std::mutex _mutex; //!< Mutex to protect the stream. + std::ostream& _stream; //!< Stream to write log entries to. + stream_ptr_u _storage; //!< Pointer to store owned stream. + + public: + /** + * @brief Constructor. + * + * @param[in] name Name of the consumer. + * @param[in] stream Stream to write data to. + * @param[in] auto_register Automatically register the consumer. + */ + consumer_stream(const std::string& name, std::ostream& stream, bool auto_register); + + /** + * @brief Constructor. + * + * @param[in] name Name of the consumer. + * @param[in] stream Stream to write data to. + * @param[in] auto_register Automatically register the consumer. + */ + consumer_stream(const std::string& name, stream_ptr_u&& stream, bool auto_register); + + /** + * @brief Consume a log entry. + * + * @param[in] entry Log entry to consume. + */ + void write_entry(const log_entry_ptr_s& entry) const override; + + }; + +} diff --git a/include/cpplogging/manager/logger_impl.h b/include/cpplogging/manager/logger_impl.h new file mode 100644 index 0000000..6613b62 --- /dev/null +++ b/include/cpplogging/manager/logger_impl.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include "rule.h" +#include "../interface/logger.h" + +namespace cpplogging +{ + + struct logger_impl + : public logger + { + private: + mutable std::mutex _mutex; //!< mutex to protect the object + std::set _rules; //!< rules assigned to this logger + log_level _min_level { log_level::unknown }; //!< min log level over all rules + log_level _max_level { log_level::unknown }; //!< max log level over all rules + + public: + using logger::logger; + + /** + * @brief Add a rule to this logger. + * + * @param[in] rule Rule to add. + */ + void add_rule(rule& rule); + + /** + * @brief Remove a rule to this logger. + * + * @param[in] rule Rule to remove. + */ + void remove_rule(rule& rule); + + /** + * @brief Check if the passed log level is enabled. + * + * @param[in] level Level to check. + * + * @retval true The passed log level is enabled. + * @retval false The passed log level is disabled. + */ + bool is_enabled(log_level level) const override; + + protected: + /** + * @brief Write a log entry. + * + * @param[in] entry Log entry to write. + */ + void write_entry(const log_entry_ptr_s& entry) const override; + + /** + * @brief Update the log levels cached in the logger. + */ + void update_log_level(); + }; + +} diff --git a/include/cpplogging/manager/manager.h b/include/cpplogging/manager/manager.h new file mode 100644 index 0000000..f6aa60f --- /dev/null +++ b/include/cpplogging/manager/manager.h @@ -0,0 +1,158 @@ +#pragma once + +#include +#include +#include + +#include "rule.h" +#include "logger_impl.h" +#include "consumer/consumer.h" + +namespace cpplogging +{ + + /** + * @brief Wrapps the static configuration methods for the logger engine. + * + * The logging engine has two modes: logging and config. + * + * In the config mode, the user can add rules, consumer and matcher. + * In the logging mode the engine is not thread-safe and must be accessed + * from only one thread! + * + * In the logging mode the user can only create new loggers or log messages. + * The logging mode is thread-safe. The user can use as many thread as he want, + * to log messages. + */ + struct manager + { + private: + struct consumer_wrapper + { + consumer& _consumer; //!< consumer to use for operations + consumer_ptr_u _storage; //!< storage to store owner object + + /** + * @brief Construtor to create a consumer wrapper by using the consumer reference. + */ + inline consumer_wrapper(consumer& p_consumer); + + /** + * @brief Construtor to create a consumer wrapper. The wrapper takes the ownership of the consumer. + */ + inline consumer_wrapper(consumer_ptr_u&& p_consumer); + + /** + * @brief Operator to compare the wrapper to a other wrapper + */ + inline bool operator<(const consumer_wrapper& other) const; + + /** + * @brief Operator to access the consumer behind the wrapper. + */ + inline consumer* operator->(); + + /** + * @brief Operator to access the consumer behind the wrapper. + */ + inline const consumer* operator->() const; + + /** + * @brief Operator to access the consumer behind the wrapper. + */ + inline consumer& operator*(); + + /** + * @brief Operator to access the consumer behind the wrapper. + */ + inline const consumer& operator*() const; + }; + + using logger_impl_ptr_u = std::unique_ptr; + using logger_map = std::map; + using rule_list = std::list; + using consumer_set = std::set; + + private: + static std::mutex _mutex; //!< mutex to protect the engine + static logger_impl _default; //!< default logger + static logger_map _logger; //!< normal logger instances + static rule_list _rules; //!< rules to combine loggers with consumers + static consumer_set _consumer; //!< consumers registered + + public: + /** + * @brief Get the instance of the logger with the requested name. + * + * @param[in] name Name of the logger to get the instance for. + * + * @return Instance of the logger with the requested name. + */ + static logger& get_logger(const std::string& name = ""); + + /** + * @brief Register a new consumer in the logging framework. + * + * @param[in] consumer Reference of the consumer to register. + */ + static void register_consumer(consumer& consumer); + + /** + * @brief Register a new consumer in the logging framework. + * + * @param[in] consumer Consumer to register (the logging framework will own the consumer). + * + * @return Pointer to the consumer. + */ + static const consumer* register_consumer(consumer_ptr_u&& consumer); + + /** + * @brief Unregister a consumer from the logging framework. + * + * @param[in] consumer Reference of the consumer to unregister + */ + static void unregister_consumer(consumer& consumer); + + /** + * @brief Unregister a consumer from the logging framework. + * + * @param[in] consumer Consumer to unregister from the framework. + */ + static void unregister_consumer(consumer* consumer); + + /** + * @brief Define a new rule in the logging framework. + * + * @param[in] logger_matcher Matcher to use for matching loggers. + * @param[in] consumer_matcher Matcher to use for matching consumers. + * @param[in] min_level Minimum log level. + * @param[in] max_level Maximum log level. + * + * @return Return the handle to the rule (for removing). + */ + static rule_handle define_rule( + matcher_ptr_u&& logger_matcher, + matcher_ptr_u&& consumer_matcher, + log_level min_level = log_level::debug, + log_level max_level = log_level::error); + + /** + * @brief Remove a rule from the logging framework. + * + * @param[in] handle Handle of the rule to remove. + */ + static void undefine_rule(rule_handle handle); + + /** + * @brief Remove all consumers, rules, matchers and loggers. + */ + static void reset(); + + private: + /** + * @brief Initialize the given logger instance. + */ + static logger_impl& init_logger(logger_impl& logger); + }; + +} diff --git a/include/cpplogging/manager/manager.inl b/include/cpplogging/manager/manager.inl new file mode 100644 index 0000000..40878e4 --- /dev/null +++ b/include/cpplogging/manager/manager.inl @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include "manager.h" + +namespace cpplogging +{ + + /* manager::consumer_wrapper */ + + manager::consumer_wrapper + ::consumer_wrapper(consumer& p_consumer) + : _consumer (p_consumer) + , _storage (nullptr) + { } + + manager::consumer_wrapper + ::consumer_wrapper(consumer_ptr_u&& p_consumer) + : _consumer (*p_consumer) + , _storage (std::move(p_consumer)) + { } + + bool manager::consumer_wrapper + ::operator<(const consumer_wrapper& other) const + { return &_consumer < &other._consumer; } + + consumer* manager::consumer_wrapper + ::operator->() + { return &_consumer; } + + const consumer* manager::consumer_wrapper + ::operator->() const + { return &_consumer; } + + consumer& manager::consumer_wrapper + ::operator*() + { return _consumer; } + + const consumer& manager::consumer_wrapper + ::operator*() const + { return _consumer; } + +} diff --git a/include/cpplogging/manager/matcher.h b/include/cpplogging/manager/matcher.h new file mode 100644 index 0000000..321dca9 --- /dev/null +++ b/include/cpplogging/manager/matcher.h @@ -0,0 +1,6 @@ +#pragma once + +#include "matcher/matcher.h" +#include "matcher/matcher_all.h" +#include "matcher/matcher_default_logger.h" +#include "matcher/matcher_regex.h" diff --git a/include/cpplogging/manager/matcher/matcher.h b/include/cpplogging/manager/matcher/matcher.h new file mode 100644 index 0000000..91dc2f1 --- /dev/null +++ b/include/cpplogging/manager/matcher/matcher.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include "../consumer/consumer.h" +#include "../../interface/logger.h" + +namespace cpplogging +{ + + /** + * @brief Class to use for matching a rule or a logger. + */ + struct matcher + { + /** + * @brief Destructor. + */ + virtual ~matcher() = default; + + /** + * @brief Check if the passed logger match this matcher. + * + * @param[in] logger Logger to check. + * + * @retval true If the logger matches. + * @retval false If the logger does not match. + */ + virtual bool match(const logger& logger) const; + + /** + * @brief Check if the passed consumer match this matcher. + * + * @param[in] consumer Consumer to check. + * + * @retval true If the consumer matches. + * @retval false If the consumer does not match. + */ + virtual bool match(const consumer& consumer) const; + + }; + + using matcher_ptr_u = std::unique_ptr; + +} diff --git a/include/cpplogging/manager/matcher/matcher_all.h b/include/cpplogging/manager/matcher/matcher_all.h new file mode 100644 index 0000000..be67c67 --- /dev/null +++ b/include/cpplogging/manager/matcher/matcher_all.h @@ -0,0 +1,35 @@ +#pragma once + +#include "matcher.h" + +namespace cpplogging +{ + + /** + * @brief This matcher matches any logger and any consumer. + */ + struct matcher_all + : public matcher + { + /** + * @brief Check if the passed logger match this matcher. + * + * @param[in] logger Logger to check. + * + * @retval true If the logger matches. + * @retval false If the logger does not match. + */ + bool match(const logger& logger) const override; + + /** + * @brief Check if the passed consumer match this matcher. + * + * @param[in] consumer Consumer to check. + * + * @retval true If the consumer matches. + * @retval false If the consumer does not match. + */ + bool match(const consumer& consumer) const override; + }; + +} diff --git a/include/cpplogging/manager/matcher/matcher_default_logger.h b/include/cpplogging/manager/matcher/matcher_default_logger.h new file mode 100644 index 0000000..eb9e20d --- /dev/null +++ b/include/cpplogging/manager/matcher/matcher_default_logger.h @@ -0,0 +1,36 @@ +#pragma once + +#include "matcher.h" + +namespace cpplogging +{ + + /** + * @brief This matcher does only match the default logger. + */ + struct matcher_default_logger + : public matcher + { + private: + logger& _default; //!< the default logger + + public: + using matcher::match; + + /** + * @brief Constructor. + */ + matcher_default_logger(); + + /** + * @brief Check if the passed logger match this matcher. + * + * @param[in] logger Logger to check. + * + * @retval true If the logger matches. + * @retval false If the logger does not match. + */ + bool match(const logger& logger) const override; + }; + +} diff --git a/include/cpplogging/manager/matcher/matcher_regex.h b/include/cpplogging/manager/matcher/matcher_regex.h new file mode 100644 index 0000000..2e56501 --- /dev/null +++ b/include/cpplogging/manager/matcher/matcher_regex.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include "matcher.h" + +namespace cpplogging +{ + + /** + * @brief Matcher that uses a regular expression to match a logger or a consumer + */ + struct matcher_regex + : public matcher + { + private: + std::regex _regex; //!< regex to use for matching + bool _invert; //!< invert the regular expression + + public: + /** + * @brief Constructor. + * + * @param[in] regex Regex to use for matching. + * @param[in] invert Invert the regular expression. + */ + matcher_regex(const std::string regex, bool invert = false); + + /** + * @brief Check if the passed logger match this matcher. + * + * @param[in] logger Logger to check. + * + * @retval true If the logger matches. + * @retval false If the logger does not match. + */ + bool match(const logger& logger) const override; + + /** + * @brief Check if the passed consumer match this matcher. + * + * @param[in] consumer Consumer to check. + * + * @retval true If the consumer matches. + * @retval false If the consumer does not match. + */ + bool match(const consumer& consumer) const override; + }; + +} diff --git a/include/cpplogging/manager/rule.h b/include/cpplogging/manager/rule.h new file mode 100644 index 0000000..7775f95 --- /dev/null +++ b/include/cpplogging/manager/rule.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +#include "matcher/matcher.h" +#include "consumer/consumer.h" + +namespace cpplogging +{ + + struct rule + { + private: + mutable std::mutex _mutex; + std::set _consumer; + + public: + matcher_ptr_u logger_matcher; //!< matcher to use for logger matching + matcher_ptr_u consumer_matcher; //!< matcher to use for consumer matching + log_level min_level; //!< minimum log level + log_level max_level; //!< maximum log level + + /** + * @brief Constructor. + * + * @param[in] lm Matcher to use for logger matching. + * @param[in] cm Matcher to use for consumer matching. + * @param[in] min Minimum log level. + * @param[in] max Maximum log level. + */ + inline rule(matcher_ptr_u&& lm, matcher_ptr_u&& cm, log_level min, log_level max); + + /** + * @brief CHeck if the passed log level is enabled in this rule. + * + * @param[in] level Level to check. + * + * @retval true If the log level is enabled for this rule. + * @retval false If the log level is not enabled for this rule. + */ + inline bool is_enabled(log_level level) const; + + /** + * @brief Add a consumer to this rule. + * + * @param[in] consumer Consumer to add to the rule. + */ + inline void add_consumer(const consumer& consumer); + + /** + * @brief Remove a consumer from this rule. + * + * @param[in] consumer Consumer to remove from the rule. + */ + inline void remove_consumer(const consumer& consumer); + + /** + * @brief Write the passed log entry to all registered consumers. + * + * @param[in] entry Entry to write to the consumers. + */ + inline void write_entry(const log_entry_ptr_s& entry) const; + }; + + using rule_handle = void *; + +} + +#include "rule.inl" diff --git a/include/cpplogging/manager/rule.inl b/include/cpplogging/manager/rule.inl new file mode 100644 index 0000000..3774cb3 --- /dev/null +++ b/include/cpplogging/manager/rule.inl @@ -0,0 +1,50 @@ +#pragma once + +#include "rule.h" + +namespace cpplogging +{ + + /* rule */ + + rule + ::rule(matcher_ptr_u&& lm, matcher_ptr_u&& cm, log_level min, log_level max) + : logger_matcher (std::move(lm)) + , consumer_matcher (std::move(cm)) + , min_level (min) + , max_level (max) + { } + + bool rule + ::is_enabled(log_level level) const + { + return min_level <= level + && max_level >= level; + } + + void rule + ::add_consumer(const consumer& consumer) + { + std::lock_guard lg(_mutex); + _consumer.insert(&consumer); + } + + void rule + ::remove_consumer(const consumer& consumer) + { + std::lock_guard lg(_mutex); + _consumer.erase(&consumer); + } + + void rule + ::write_entry(const log_entry_ptr_s& entry) const + { + std::lock_guard lg(_mutex); + if (is_enabled(entry->level)) + { + for (auto& c : _consumer) + c->write_entry(entry); + } + } + +} diff --git a/include/cpplogging/types.h b/include/cpplogging/types.h new file mode 100644 index 0000000..5057ded --- /dev/null +++ b/include/cpplogging/types.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +namespace cpplogging +{ + + /** + * @brief Log level. + */ + enum class log_level + { + unknown = 0, + debug, + info, + warn, + error, + }; + + /** + * @brief Log entry that stored all relevant information for a logged message. + */ + struct log_entry + { + using clock_type = std::chrono::steady_clock; + using uptime_type = clock_type::time_point; + + log_level level { log_level::debug }; + uptime_type uptime { clock_type::now() }; + time_t systime { std::time(nullptr) }; + const void* sender { nullptr }; + std::thread::id thread { std::this_thread::get_id() }; + const char* file { nullptr }; + int line { 0 }; + std::string name; + std::string message; + }; + + using log_entry_ptr_s = std::shared_ptr; + +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..27ace49 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,112 @@ +# Initialize ###################################################################################### + +Include ( cotire OPTIONAL RESULT_VARIABLE HAS_COTIRE ) +Include ( pedantic OPTIONAL RESULT_VARIABLE HAS_PEDANTIC ) +Include ( strip_symbols OPTIONAL RESULT_VARIABLE HAS_STRIP_SYMBOLS ) + +Option ( CPPLOGGING_INSTALL_HEADER + "Install headers of cpplogging." + ON ) +Option ( CPPLOGGING_INSTALL_STATIC + "Install static library of cpplogging." + ON ) +Option ( CPPLOGGING_INSTALL_SHARED + "Install shared library of cpplogging." + ON ) +Option ( CPPLOGGING_INSTALL_DEBUG + "Install the stripped debug informations of cpplogging." + OFF ) +Option ( CPPLOGGING_NO_STRIP + "Do not strip debug symbols from binary." + OFF ) + +# Object Library ################################################################################## + +Set ( CMAKE_POSITION_INDEPENDENT_CODE ON ) +Set ( CPPLOGGING_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../include ) +File ( GLOB_RECURSE CPPLOGGING_HEADER_FILES ${CPPLOGGING_INCLUDE_DIR}/*.h ) +File ( GLOB_RECURSE CPPLOGGING_INLINE_FILES ${CPPLOGGING_INCLUDE_DIR}/*.inl ) +File ( GLOB_RECURSE CPPLOGGING_SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp ) +Add_Library ( cpplogging-objects + OBJECT + ${CPPLOGGING_HEADER_FILES} + ${CPPLOGGING_INLINE_FILES} + ${CPPLOGGING_SOURCE_FILES} ) +Target_Include_Directories ( cpplogging-objects + PUBLIC + $ + $ ) + +# Static Library ################################################################################## + +Add_Library ( cpplogging-static STATIC $ ) +Set_Target_Properties ( cpplogging-static + PROPERTIES + OUTPUT_NAME "cpplogging" + VERSION ${CPPLOGGING_VERSION} ) +Target_Include_Directories ( cpplogging-static + PUBLIC + $ + $ ) + +# Shared Library ################################################################################## + +Add_Library ( cpplogging-shared SHARED $ ) +Set_Target_Properties ( cpplogging-shared + PROPERTIES + OUTPUT_NAME "cpplogging" + VERSION ${CPPLOGGING_VERSION} + SOVERSION ${CPPLOGGING_VERSION_SHORT} ) +Target_Include_Directories ( cpplogging-shared + PUBLIC + $ + $ ) + +# Optimization #################################################################################### + +# pedantic +If ( HAS_PEDANTIC ) + Pedantic_Apply_Flags_Target ( cpplogging-objects CXX ) + Pedantic_Apply_Flags_Target ( cpplogging-static CXX ) + Pedantic_Apply_Flags_Target ( cpplogging-shared CXX ) +EndIf ( ) + +# cotire +If ( HAS_COTIRE ) + Cotire ( cpplogging-objects ) + Cotire ( cpplogging-static ) + Cotire ( cpplogging-shared ) +EndIf ( ) + +# Install ######################################################################################### + +# Header +If ( CPPLOGGING_INSTALL_HEADER ) + Install ( FILES ${CPPLOGGING_INCLUDE_DIR}/cpplogging.h + DESTINATION ${CPPLOGGING_INSTALL_DIR_INCLUDE} ) + Install ( DIRECTORY ${CPPLOGGING_INCLUDE_DIR}/cpplogging + DESTINATION ${CPPLOGGING_INSTALL_DIR_INCLUDE} ) +EndIf ( ) + +# Static +If ( CPPLOGGING_INSTALL_STATIC ) + Install ( TARGETS cpplogging-static + EXPORT cpplogging + DESTINATION ${CPPLOGGING_INSTALL_DIR_LIB} ) +EndIf ( ) + +# Shared +If ( CPPLOGGING_INSTALL_SHARED ) + Install ( TARGETS cpplogging-shared + EXPORT cpplogging + DESTINATION ${CPPLOGGING_INSTALL_DIR_LIB} ) +EndIf ( ) + +# Debug +If ( HAS_STRIP_SYMBOLS AND NOT CPPLOGGING_NO_STRIP ) + Strip_Symbols ( cpplogging-shared CPPLOGGING_DBG_FILE ) + If ( CPPLOGGING_INSTALL_DEBUG ) + Install ( FILES ${CPPLOGGING_DBG_FILE} + DESTINATION ${CPPLOGGING_INSTALL_DIR_LIB} ) + EndIf ( ) +EndIf ( ) diff --git a/src/cpplogging/interface/logger.cpp b/src/cpplogging/interface/logger.cpp new file mode 100644 index 0000000..823d859 --- /dev/null +++ b/src/cpplogging/interface/logger.cpp @@ -0,0 +1,7 @@ +#include +#include + +using namespace ::cpplogging; + +logger& logger::get(const std::string& name) + { return manager::get_logger(name); } diff --git a/src/cpplogging/manager/consumer/consumer.cpp b/src/cpplogging/manager/consumer/consumer.cpp new file mode 100644 index 0000000..72073d1 --- /dev/null +++ b/src/cpplogging/manager/consumer/consumer.cpp @@ -0,0 +1,17 @@ +#include +#include + +using namespace ::cpplogging; + +consumer::consumer(const std::string& name, bool auto_register, const std::string& format) + : _name (name) + , _format (parse_format(format)) +{ + if (auto_register) + manager::register_consumer(*this); +} + +consumer::~consumer() +{ + manager::unregister_consumer(this); +} diff --git a/src/cpplogging/manager/consumer/consumer_stream.cpp b/src/cpplogging/manager/consumer/consumer_stream.cpp new file mode 100644 index 0000000..fea8c9f --- /dev/null +++ b/src/cpplogging/manager/consumer/consumer_stream.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include +#include + +using namespace ::cpplogging; + +consumer_stream::consumer_stream(const std::string& name, std::ostream& stream, bool auto_register) + : consumer (name, auto_register) + , _stream (stream) + , _storage (nullptr) + { } + +consumer_stream::consumer_stream(const std::string& name, stream_ptr_u&& stream, bool auto_register) + : consumer (name, auto_register) + , _stream (*stream) + , _storage (std::move(stream)) + { } + +void consumer_stream::write_entry(const log_entry_ptr_s& entry) const +{ + std::lock_guard lk(_mutex); + write_formatted(*entry, _format, _stream); +} diff --git a/src/cpplogging/manager/consumer/formatting.cpp b/src/cpplogging/manager/consumer/formatting.cpp new file mode 100644 index 0000000..2639fa3 --- /dev/null +++ b/src/cpplogging/manager/consumer/formatting.cpp @@ -0,0 +1,681 @@ +#include +#include +#include +#include + +#include +#include + +using namespace ::cpplogging; + +enum class parser_state +{ + is_text, //!< current pos is inside normal text + is_token_begin, //!< current pos is in token begin (except char '$') + is_in_token, //!< current pos is in token (everything after '{') + is_in_format, //!< current pos is in format (everything after ':') + is_in_format_align, //!< current pos is in format align (expect char '-') + is_in_format_zero, //!< current pos is in format align (expect char '0') + is_in_format_dot, //!< current pos is in format dot (expect char '.') + is_in_format_type, //!< current pos is in format type (expect char 's', 'f', x', 'X' or 'd') +}; + +enum class value_token + : uint8_t +{ + unknown = 0, //!< unknown + level, //!< log level + uptime, //!< system uptime + systime, //!< unix timestamp + runtime, //!< runtime of the application + sender, //!< sender of the log message + thread, //!< thread id + filename, //!< filename only + filepath, //!< whole filepath + line, //!< line number + name, //!< name of the logger + message, //!< log message + newline, //!< write new line +}; + +enum class value_format + : uint8_t +{ + unknown = 0, + decimal, + real, + hex_lower, + hex_upper, + string, + string_upper, +}; + +struct format_item +{ + const uint16_t magic { 0 }; //!< magic value to identify the item + value_token token { value_token::unknown }; //!< value to print + value_format format { value_format::unknown }; //!< format to print the value with + uint8_t width { 0 }; //!< with of the value + uint8_t precision { 0 }; //!< precision (for float values) + bool right_align { false }; //!< use right alignment + bool zero_fill { false }; //!< fill with zeros (if not than spaces) + + inline void reset() + { memset(this, 0, sizeof(*this)); } + + inline std::string as_string() const + { return std::string(reinterpret_cast(this), sizeof(*this)); } + +} __attribute__ ((packed)); + +static_assert(sizeof(format_item) == 8, "format_item has invalid size!"); + +struct op_less_invariant_string +{ + inline bool operator()(const std::string& lhs, const std::string& rhs) const + { + auto c1 = lhs.c_str(); + auto c2 = rhs.c_str(); + auto l1 = lhs.size(); + auto l2 = rhs.size(); + while (l1 > 0 && l2 > 0 && std::tolower(*c1) == std::tolower(*c2)) + { + ++c1; + ++c2; + --l1; + --l2; + } + return l1 > 0 && l2 > 0 + ? std::tolower(*c1) < std::tolower(*c2) + : l1 < l2; + } +}; + +struct invariant_string_map + : public std::map +{ + using map::map; + + inline value_token get_token(const std::string& token) const + { + auto it = find(token); + return it == end() + ? value_token::unknown + : it->second; + } +}; + +static invariant_string_map value_token_map({ + { "level", value_token::level }, + { "uptime", value_token::uptime }, + { "systime", value_token::systime }, + { "runtime", value_token::runtime }, + { "sender", value_token::sender }, + { "thread", value_token::thread }, + { "filename", value_token::filename }, + { "filepath", value_token::filepath }, + { "line", value_token::line }, + { "name", value_token::name }, + { "message", value_token::message }, + { "newline", value_token::newline }, +}); + +consumer::format_type consumer::parse_format(const std::string& format) +{ + consumer::format_type ret; + + parser_state state = parser_state::is_text; + const char * s = format.c_str(); + const char * e = s + format.size(); + const char * c = s; + const char * x; + char * tmp; + unsigned long long int u; + format_item item; + + while (c <= e) + { + switch (*c) + { + /* if this is the EOF */ + case '\0': + if (s < c) + { + if (!ret.empty() && !ret.back().empty() && *ret.back().c_str() != '\0') + ret.back() += std::string(s, static_cast(c - s - 1)); + else + ret.emplace_back(s, c - s); + } + break; + /* the format string starts with $ */ + case '$': + switch (state) + { + /* if we are 'in_text', we switch to 'is_token_begin' */ + case parser_state::is_text: + state = parser_state::is_token_begin; + break; + /* in an other case switch back to 'is_text' */ + default: + state = parser_state::is_text; + item.reset(); + break; + } + break; + + /* after '$' we expect '{' */ + case '{': + switch (state) + { + /* if we have seen '$' we are in 'is_token_begin' and switch to 'is_in_token' */ + case parser_state::is_token_begin: + if (s + 1 < c) + { + if (!ret.empty() && !ret.back().empty() && *ret.back().c_str() != '\0') + ret.back() += std::string(s, static_cast(c - s - 1)); + else + ret.emplace_back(s, c - s - 1); + } + s = c - 1; + x = c + 1; + state = parser_state::is_in_token; + break; + /* in an other case switch back to 'is_text' */ + default: + state = parser_state::is_text; + item.reset(); + break; + } + break; + + /* inside the token we have ':' to seperate the format */ + case ':': + switch (state) + { + /* if we are in 'is_in_token' we extract the token and switch to 'is_in_format' */ + case parser_state::is_in_token: + state = parser_state::is_in_format; + item.token = value_token_map.get_token(std::string(x, static_cast(c - x))); + if (item.token == value_token::unknown) + { + state = parser_state::is_text; + item.reset(); + } + x = c + 1; + break; + /* in an other case switch back to 'is_text' */ + default: + state = parser_state::is_text; + item.reset(); + break; + } + break; + + /* inside the format parsing we can have '-' to indicate right align */ + case '-': + switch (state) + { + /* if we are in 'is_in_format' we switch to 'is_in_format_align' */ + case parser_state::is_in_format: + state = parser_state::is_in_format_align; + item.right_align = true; + x = c + 1; + break; + /* in an other case switch back to 'is_text' */ + default: + state = parser_state::is_text; + item.reset(); + break; + } + break; + + /* inside the format parsing we can have '0' to indicate zero fill */ + case '0': + switch (state) + { + /* if we are in state 'is_in_format' or 'is_in_format_align' we switch to 'is_in_format_zero' */ + case parser_state::is_in_format: + case parser_state::is_in_format_align: + state = parser_state::is_in_format_zero; + item.zero_fill = true; + x = c + 1; + break; + /* in an other case switch back to 'is_text' */ + default: + state = parser_state::is_text; + item.reset(); + break; + } + break; + + /* inside the format parsing we can have '.' to indicate the precision */ + case '.': + switch (state) + { + /* if we are in state 'is_in_format', 'is_in_format_align' or 'is_in_format_zero' we switch to 'is_in_format_dot' */ + case parser_state::is_in_format: + case parser_state::is_in_format_align: + case parser_state::is_in_format_zero: + u = std::strtoull(x, &tmp, 10); + if (x == tmp || x > c) + { + /* if parsing the with failed, we switch back to 'is_text' */ + state = parser_state::is_text; + item.reset(); + } + else + { + item.width = static_cast(u); + x = c + 1; + state = parser_state::is_in_format_dot; + } + break; + /* in an other case switch back to 'is_text' */ + default: + state = parser_state::is_text; + item.reset(); + break; + } + break; + + /* if 'n' follows '$' we write a newline */ + case 'n': + switch (state) + { + case parser_state::is_token_begin: + item.token = value_token::newline; + ret.emplace_back(item.as_string()); + item.reset(); + state = parser_state::is_text; + s = c + 1; + break; + default: + break; + } + break; + + /* inside the format parsing we can have 's', 'f', 'd', 'x' or 'X' to indicate the format */ + case 's': + case 'S': + case 'f': + case 'd': + case 'x': + case 'X': + switch (state) + { + /* if we are in state 'is_in_token' everything is fine */ + case parser_state::is_in_token: + break; + /* if we are in state 'is_in_format', 'is_in_format_align' or 'is_in_format_zero' we switch to 'is_in_format_type' */ + case parser_state::is_in_format: + case parser_state::is_in_format_align: + case parser_state::is_in_format_zero: + case parser_state::is_in_format_dot: + u = std::strtoull(x, &tmp, 10); + if (x != c && (x == tmp || x > c)) + { + /* if parsing the with failed, we switch back to 'is_text' */ + state = parser_state::is_text; + item.reset(); + } + else + { + if (state == parser_state::is_in_format_dot) + item.precision = static_cast(u); + else + item.width = static_cast(u); + + /* set the type and switch to 'is_in_format_type' */ + x = c + 1; + state = parser_state::is_in_format_type; + switch(*c) + { + case 's': item.format = value_format::string; break; + case 'S': item.format = value_format::string_upper; break; + case 'f': item.format = value_format::real; break; + case 'd': item.format = value_format::decimal; break; + case 'x': item.format = value_format::hex_lower; break; + case 'X': item.format = value_format::hex_upper; break; + default: break; + } + } + break; + /* in an other case switch back to 'is_text' */ + default: + state = parser_state::is_text; + item.reset(); + break; + } + break; + + /* inside the token/format parsing '}' indicates the end */ + case '}': + switch (state) + { + /* if we are in 'is_in_token' we extract the token, add the item and switch to 'is_text' */ + case parser_state::is_in_token: + state = parser_state::is_text; + item.token = value_token_map.get_token(std::string(x, static_cast(c - x))); + if (item.token == value_token::unknown) + { + state = parser_state::is_text; + } + else + { + ret.emplace_back(item.as_string()); + } + item.reset(); + s = c + 1; + break; + /* if we are in 'is_in_format_type' we extract the token, add the item and switch to 'is_text' */ + case parser_state::is_in_format_type: + state = parser_state::is_text; + ret.emplace_back(item.as_string()); + item.reset(); + s = c + 1; + break; + /* if we are in 'is_in_format', is_in_format_zero' or 'is_in_format_align' + we extract the with, add the item and switch to 'is_text'*/ + case parser_state::is_in_format: + case parser_state::is_in_format_zero: + case parser_state::is_in_format_align: + case parser_state::is_in_format_dot: + u = std::strtoull(x, &tmp, 10); + s = c + 1; + if (x != tmp && x <= c) + { + if (state == parser_state::is_in_format_dot) + item.precision = static_cast(u); + else + item.width = static_cast(u); + ret.emplace_back(item.as_string()); + } + item.reset(); + state = parser_state::is_text; + break; + /* in an other case switch back to 'is_text' */ + default: + state = parser_state::is_text; + item.reset(); + break; + } + break; + + /* any non parsing relevant char */ + default: + switch (state) + { + /* in case 'is_in_format', 'is_in_format_align' and 'is_in_format_zero' + we need to check if the current char is a digit, if not we switch to 'is_text' */ + case parser_state::is_in_format: + case parser_state::is_in_format_align: + case parser_state::is_in_format_zero: + if ( *c != '.' + && (*c < '0' || *c > '9')) + { + state = parser_state::is_text; + item.reset(); + } + break; + /* any other case is fine */ + default: + break; + } + break; + } + ++c; + } + + return ret; +} + +static const auto start_time = std::chrono::steady_clock::now(); + +void consumer::write_formatted(const log_entry& e, const format_type& f, std::ostream& os) +{ + using duration_f = std::chrono::duration>; + using duration_u = std::chrono::duration>; + + static const std::string LogLevelUpperDebug("DEBUG", 5); + static const std::string LogLevelUpperInfo ("INFO", 4); + static const std::string LogLevelUpperWarn ("WARN", 4); + static const std::string LogLevelUpperError("ERROR", 5); + + static const std::string LogLevelLowerDebug("debug", 5); + static const std::string LogLevelLowerInfo ("info", 4); + static const std::string LogLevelLowerWarn ("warn", 4); + static const std::string LogLevelLowerError("error", 5); + + bool has_line_end = false; + for (auto& x : f) + { + /* check if element is empty */ + if (x.empty()) + { + continue; + } + + /* check if element is normal item */ + if (*x.c_str() != '\0') + { + os << x; + continue; + } + + /* setup the stream format */ + const char * s; + auto& i = *reinterpret_cast(x.c_str()); + os << (i.right_align ? std::right : std::left) + << std::setfill(i.zero_fill ? '0' : ' ') + << std::setw(i.width) + << std::setprecision(i.precision); + + switch (i.token) + { + /* level */ + case value_token::level: + has_line_end = false; + switch (i.format) + { + case value_format::decimal: + os << static_cast(e.level); + break; + case value_format::string_upper: + switch (e.level) + { + case log_level::debug: os << LogLevelUpperDebug; break; + case log_level::info: os << LogLevelUpperInfo; break; + case log_level::warn: os << LogLevelUpperWarn; break; + case log_level::error: os << LogLevelUpperError; break; + default: break; + } + break; + default: + switch (e.level) + { + case log_level::debug: os << LogLevelLowerDebug; break; + case log_level::info: os << LogLevelLowerInfo; break; + case log_level::warn: os << LogLevelLowerWarn; break; + case log_level::error: os << LogLevelLowerError; break; + default: break; + } + break; + } + break; + + /* uptime */ + case value_token::uptime: + has_line_end = false; + switch (i.format) + { + case value_format::decimal: + os << std::chrono::duration_cast(e.uptime.time_since_epoch()).count(); + break; + default: + os << std::fixed + << std::chrono::duration_cast(e.uptime.time_since_epoch()).count() + << std::dec; + break; + } + break; + + /* runtime */ + case value_token::runtime: + has_line_end = false; + switch (i.format) + { + case value_format::decimal: + os << std::chrono::duration_cast(e.uptime - start_time).count(); + break; + default: + os << std::fixed + << std::chrono::duration_cast(e.uptime - start_time).count() + << std::dec; + break; + } + break; + + /* systemtime */ + case value_token::systime: + has_line_end = false; + switch (i.format) + { + case value_format::decimal: + os << e.systime; + break; + default: + os << std::fixed + << e.systime + << std::dec; + break; + } + break; + + /* sender */ + case value_token::sender: + has_line_end = false; + switch (i.format) + { + case value_format::decimal: + os << reinterpret_cast(e.sender); + break; + case value_format::hex_lower: + os << "0x" + << std::hex + << reinterpret_cast(e.sender) + << std::dec; + break; + default: + os << "0x" + << std::hex + << std::uppercase + << reinterpret_cast(e.sender) + << std::nouppercase + << std::dec; + break; + } + break; + + /* thread */ + case value_token::thread: + has_line_end = false; + switch (i.format) + { + case value_format::decimal: + os << e.thread; + break; + case value_format::hex_lower: + os << "0x" + << std::hex + << e.thread + << std::dec; + break; + default: + os << "0x" + << std::hex + << std::uppercase + << e.thread + << std::nouppercase + << std::dec; + break; + } + break; + + /* filename */ + case value_token::filename: + has_line_end = false; + s = e.file; + if (s) + { + auto tmp = strrchr(s, '/'); + if (tmp) + s = tmp + 1; + } + else + s = "unknown"; + os << s; + break; + + /* filepath */ + case value_token::filepath: + has_line_end = false; + os << (e.file ? e.file : "unknown"); + break; + + /* line */ + case value_token::line: + has_line_end = false; + switch (i.format) + { + case value_format::decimal: + os << e.line; + break; + case value_format::hex_lower: + os << "0x" + << std::hex + << e.line + << std::dec; + break; + default: + os << "0x" + << std::hex + << std::uppercase + << e.line + << std::nouppercase + << std::dec; + break; + } + break; + + /* name */ + case value_token::name: + if (!e.name.empty()) + { + os << e.name; + has_line_end = (e.name.back() == '\n'); + } + break; + + /* message */ + case value_token::message: + if (!e.message.empty()) + { + os << e.message; + has_line_end = (e.message.back() == '\n'); + } + break; + + /* newline */ + case value_token::newline: + has_line_end = true; + os << std::endl; + break; + + default: + break; + } + } + + if (!has_line_end) + os << std::endl; +} diff --git a/src/cpplogging/manager/logger_impl.cpp b/src/cpplogging/manager/logger_impl.cpp new file mode 100644 index 0000000..cffd338 --- /dev/null +++ b/src/cpplogging/manager/logger_impl.cpp @@ -0,0 +1,51 @@ +#include + +using namespace ::cpplogging; + +void logger_impl::add_rule(rule& rule) +{ + std::lock_guard lk(_mutex); + _rules.insert(&rule); + update_log_level(); +} + +void logger_impl::remove_rule(rule& rule) +{ + std::lock_guard lk(_mutex); + _rules.erase(&rule); + update_log_level(); +} + +bool logger_impl::is_enabled(log_level level) const +{ + return _min_level <= level + && _max_level >= level; +} + +void logger_impl::write_entry(const log_entry_ptr_s& entry) const +{ + std::lock_guard lk(_mutex); + + if (!is_enabled(entry->level)) + return; + + for (auto& r : _rules) + r->write_entry(entry); +} + +void logger_impl::update_log_level() +{ + _min_level = log_level::unknown; + _max_level = log_level::unknown; + + for (auto& r : _rules) + { + if ( _min_level == log_level::unknown + || _min_level > r->min_level) + _min_level = r->min_level; + + if ( _max_level == log_level::unknown + || _max_level < r->max_level) + _max_level = r->max_level; + } +} diff --git a/src/cpplogging/manager/manager.cpp b/src/cpplogging/manager/manager.cpp new file mode 100644 index 0000000..2be836c --- /dev/null +++ b/src/cpplogging/manager/manager.cpp @@ -0,0 +1,140 @@ +#include +#include + +using namespace ::cpplogging; + +std::mutex manager::_mutex; +logger_impl manager::_default(""); +manager::logger_map manager::_logger; +manager::rule_list manager::_rules; +manager::consumer_set manager::_consumer; + +logger& manager::get_logger(const std::string& name) +{ + std::lock_guard lg(_mutex); + if (name.empty()) + return _default; + + auto it = _logger.find(name); + if (it == _logger.end()) + { + it = _logger.emplace_hint(it, name, std::make_unique(name)); + manager::init_logger(*it->second); + } + + return *it->second; +} + +void manager::register_consumer(consumer& consumer) +{ + std::lock_guard lg(_mutex); + auto it = _consumer.emplace(consumer).first; + auto& c = **it; + for (auto& rule : _rules) + { + if (rule.consumer_matcher->match(c)) + rule.add_consumer(c); + } +} + +const consumer* manager::register_consumer(consumer_ptr_u&& consumer) +{ + std::lock_guard lg(_mutex); + auto it = _consumer.emplace(std::move(consumer)).first; + auto& ret = **it; + for (auto& rule : _rules) + { + if (rule.consumer_matcher->match(ret)) + rule.add_consumer(ret); + } + return &ret; +} + +void manager::unregister_consumer(consumer& consumer) +{ + std::lock_guard lg(_mutex); + unregister_consumer(&consumer); +} + +void manager::unregister_consumer(consumer* consumer) +{ + std::lock_guard lg(_mutex); + auto it = _consumer.find(consumer_wrapper(*consumer)); + if (it == _consumer.end()) + return; + + auto& c = **it; + for (auto& rule : _rules) + rule.remove_consumer(c); +} + +rule_handle manager::define_rule( + matcher_ptr_u&& logger_matcher, + matcher_ptr_u&& consumer_matcher, + log_level min_level, + log_level max_level) +{ + std::lock_guard lg(_mutex); + + /* create rule */ + _rules.emplace_back(std::move(logger_matcher), std::move(consumer_matcher), min_level, max_level); + auto& rule = _rules.back(); + + /* update consumers */ + for (auto& c : _consumer) + { + if (rule.consumer_matcher->match(*c)) + rule.add_consumer(*c); + } + + /* update default logger */ + if (rule.logger_matcher->match(_default)) + _default.add_rule(rule); + + /* update loggers */ + for (auto& l : _logger) + { + if (rule.logger_matcher->match(*l.second)) + l.second->add_rule(rule); + } + return &rule; +} + +void manager::undefine_rule(rule_handle handle) +{ + std::lock_guard lg(_mutex); + + /* find the rule in the list */ + auto r = static_cast(handle); + auto it = _rules.begin(); + while (&*it != r && it != _rules.end()) + ++it; + if (it == _rules.end()) + return; + + /* remove the rule from the loggers */ + _default.remove_rule(*it); + for (auto& l : _logger) + l.second->remove_rule(*it); + + /* remove the rule from the manager */ + _rules.erase(it); +} + +void manager::reset() +{ + std::lock_guard lg(_mutex); + _logger.clear(); + _rules.clear(); + _consumer.clear(); +} + +logger_impl& manager::init_logger(logger_impl& logger) +{ + for (auto& rule : _rules) + { + if (rule.logger_matcher->match(logger)) + logger.add_rule(rule); + } + return logger; +} diff --git a/src/cpplogging/manager/matcher/matcher.cpp b/src/cpplogging/manager/matcher/matcher.cpp new file mode 100644 index 0000000..ee1565f --- /dev/null +++ b/src/cpplogging/manager/matcher/matcher.cpp @@ -0,0 +1,9 @@ +#include + +using namespace ::cpplogging; + +bool matcher::match(const logger& logger) const + { return false; } + +bool matcher::match(const consumer& consumer) const + { return false; } diff --git a/src/cpplogging/manager/matcher/matcher_all.cpp b/src/cpplogging/manager/matcher/matcher_all.cpp new file mode 100644 index 0000000..e1ed381 --- /dev/null +++ b/src/cpplogging/manager/matcher/matcher_all.cpp @@ -0,0 +1,9 @@ +#include + +using namespace ::cpplogging; + +bool matcher_all::match(const logger& logger) const + { return true; } + +bool matcher_all::match(const consumer& consumer) const + { return true; } diff --git a/src/cpplogging/manager/matcher/matcher_default_logger.cpp b/src/cpplogging/manager/matcher/matcher_default_logger.cpp new file mode 100644 index 0000000..29496d1 --- /dev/null +++ b/src/cpplogging/manager/matcher/matcher_default_logger.cpp @@ -0,0 +1,11 @@ +#include +#include + +using namespace ::cpplogging; + +matcher_default_logger::matcher_default_logger() : + _default(logger::get()) + { } + +bool matcher_default_logger::match(const logger& logger) const + { return &_default == &logger; } diff --git a/src/cpplogging/manager/matcher/matcher_regex.cpp b/src/cpplogging/manager/matcher/matcher_regex.cpp new file mode 100644 index 0000000..ec6c86a --- /dev/null +++ b/src/cpplogging/manager/matcher/matcher_regex.cpp @@ -0,0 +1,14 @@ +#include + +using namespace ::cpplogging; + +matcher_regex::matcher_regex(const std::string regex, bool invert) + : _regex (regex) + , _invert (invert) + { } + +bool matcher_regex::match(const logger& logger) const + { return !logger.name().empty() && std::regex_match(logger.name(), _regex) != _invert; } + +bool matcher_regex::match(const consumer& consumer) const + { return !consumer.name().empty() && std::regex_match(consumer.name(), _regex) != _invert; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..b478f92 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,43 @@ +# Initialize ###################################################################################### + +Include ( cotire OPTIONAL RESULT_VARIABLE HAS_COTIRE ) +Include ( pedantic OPTIONAL RESULT_VARIABLE HAS_PEDANTIC ) +Include ( cmake_tests OPTIONAL RESULT_VARIABLE HAS_CMAKE_TESTS ) + +# Test ############################################################################################ + +Find_Package ( GTest ) +If ( NOT "${GTest_FOUND}" ) + Return ( ) +EndIf ( ) + +File ( GLOB_RECURSE CPPLOGGING_TEST_HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.h ) +File ( GLOB_RECURSE CPPLOGGING_TEST_INLINE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.inl ) +File ( GLOB_RECURSE CPPLOGGING_TEST_SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp ) + +Add_Executable ( cpplogging-test + EXCLUDE_FROM_ALL + ${CPPLOGGING_TEST_HEADER_FILES} + ${CPPLOGGING_TEST_INLINE_FILES} + ${CPPLOGGING_TEST_SOURCE_FILES} ) +Target_Link_Libraries ( cpplogging-test + PUBLIC + cpplogging-objects + GMock::Main ) + +# pedantic +If ( HAS_PEDANTIC ) + Pedantic_Apply_Flags_Target ( cpplogging-test ALL ) +EndIf ( ) + +# optimization +If ( HAS_COTIRE ) + Cotire ( cpplogging-test ) +EndIf ( ) + +# test +If ( HAS_CMAKE_TESTS ) + Add_CMake_Test ( NAME cpplogging TARGET cpplogging-test ) +Else ( ) + Add_Test ( NAME cpplogging COMMAND cpplogging-test ) +EndIf ( ) diff --git a/test/cpplogging/cpplogging_tests.cpp b/test/cpplogging/cpplogging_tests.cpp new file mode 100644 index 0000000..b35c37a --- /dev/null +++ b/test/cpplogging/cpplogging_tests.cpp @@ -0,0 +1,127 @@ +#include +#include +#include + +using namespace ::testing; +using namespace ::cpplogging; + +struct LoggingReset +{ + ~LoggingReset() + { manager::reset(); } +}; + +struct consumer_mock + : public consumer +{ + MOCK_CONST_METHOD1(write_entry, void (const log_entry_ptr_s& entry)); + + consumer_mock(const std::string& n) : + consumer(n, true) + { } +}; + +MATCHER_P5(MatchLogData, level, sender, thread, name, message, "") +{ + if (!arg) + return false; + + auto& d = *arg; + return d.level == level + && d.sender == sender + && d.thread == thread + && d.name == name + && d.message == message; +} + +TEST(LoggingTests, matcher_all) +{ + LoggingReset loggingReset; + consumer_stream c0("TestConsumer1", std::cout, false); + consumer_stream c1("TestConsumer2", std::cout, false); + + auto& l0 = logger::get(); + auto& l1 = logger::get("TestLogger"); + + matcher_all matcher; + + EXPECT_TRUE(matcher.match(c0)); + EXPECT_TRUE(matcher.match(c1)); + EXPECT_TRUE(matcher.match(l0)); + EXPECT_TRUE(matcher.match(l1)); +} + +TEST(LoggingTests, matcher_default) +{ + LoggingReset loggingReset; + consumer_stream c0("TestConsumer1", std::cout, false); + consumer_stream c1("TestConsumer2", std::cout, false); + + auto& l0 = logger::get(); + auto& l1 = logger::get("TestLogger"); + + matcher_default_logger matcher; + + EXPECT_FALSE(matcher.match(c0)); + EXPECT_FALSE(matcher.match(c1)); + EXPECT_TRUE (matcher.match(l0)); + EXPECT_FALSE(matcher.match(l1)); +} + +TEST(LoggingTests, matcher_regex) +{ + LoggingReset loggingReset; + consumer_stream c0("TestConsumer1", std::cout, false); + consumer_stream c1("TestConsumer2", std::cout, false); + + auto& l0 = logger::get(); + auto& l1 = logger::get("TestLogger"); + + matcher_regex matcher0("TestConsumer1"); + matcher_regex matcher1("ASEF", true); + + EXPECT_TRUE (matcher0.match(c0)); + EXPECT_FALSE(matcher0.match(c1)); + EXPECT_FALSE(matcher1.match(l0)); + EXPECT_TRUE (matcher1.match(l1)); +} + +TEST(LoggingTests, log_base) +{ + LoggingReset loggingReset; + StrictMock c0("consumer0"); + StrictMock c1("Consumer1"); + + EXPECT_CALL(c0, write_entry(MatchLogData( + log_level::info, + (void*)0x12, + std::this_thread::get_id(), + std::string("logger0"), + std::string("test1 info")))); + + EXPECT_CALL(c0, write_entry(MatchLogData( + log_level::warn, + (void*)0x13, + std::this_thread::get_id(), + std::string("logger0"), + std::string("test1 warn")))); + + manager::define_rule( + std::make_unique("logger0"), + std::make_unique("consumer0"), + log_level::info, + log_level::warn); + + auto& l0 = logger::get("logger0"); + auto& l1 = logger::get("logger1"); + + cpplogging_log(l0, debug).sender((void*)0x11).message("test1") << " debug"; + cpplogging_log(l0, info ).sender((void*)0x12).message("test1") << " info"; + cpplogging_log(l0, warn ).sender((void*)0x13).message("test1") << " warn"; + cpplogging_log(l0, error).sender((void*)0x14).message("test1") << " error"; + + cpplogging_log(l1, debug).sender((void*)0x21).message("test2") << " debug"; + cpplogging_log(l1, info ).sender((void*)0x22).message("test2") << " info"; + cpplogging_log(l1, warn ).sender((void*)0x23).message("test2") << " warn"; + cpplogging_log(l1, error).sender((void*)0x24).message("test2") << " error"; +}