/** * @brief Provides logging facilities * * Log entries are associated with user defined topics. A user can define a * topic using DEFINE_LOG_TOPIC(topicname) and activate the topic for the * current scope using USE_LOG_TOPIC(topicname). * * Currently all log entries are posted to stderr in a thread-safe way. * * Whether an event is actually logged depends on the current log verbosity of * the corresponding topic. The log verbosity is defined by the following * sources (in order of their precedence). * * 0. maximum log verbosity setting (see below) * 1. runtime environment variable "FIBRE_LOG_[topicname]" * 2. runtime environment variable "FIBRE_LOG" * 3. topic specific default log verbosity defined using CONFIG_LOG_TOPIC(...) * 4. FIBRE_DEFAULT_LOG_VERBOSITY defined before this file (using #define or -D compiler flag) * 5. FIBRE_DEFAULT_LOG_VERBOSITY defined in this file * * The maximum log verbosity can be defined separately from the default log * verbosity. The maximum log verbosity bound always applies, regardless of how * the actual log verbosity is specified. This allows keeping the binary small * by optimizing away unnecessary log entries at compile time. * The maximum log verbosity is defined by the following sources (in order of * their precedence): * * 1. topic specific max log verbosity defined using CONFIG_LOG_TOPIC(...) * 2. FIBRE_MAX_LOG_VERBOSITY defined before this file (using #define or -D compiler flag) * 3. FIBRE_MAX_LOG_VERBOSITY defined in this file * * TODO: ensure that the optimizer can indeed strip the unused strings (currently not the case) * * * Example: * * @code * * DEFINE_LOG_TOPIC(MAIN); * USE_LOG_TOPIC(MAIN); * * int main(void) { * FIBRE_LOG(D) << "Hello Log!"; * if (open("inexistent_file", O_RDONLY) < 0) { * FIBRE_LOG(E) << "Could not open file: " << sys_err(); * } * return 0; * } * * @endcode * * Using logging in header files is possible but undocumented (TODO: fix) */ #ifndef __FIBRE_LOGGING_HPP #define __FIBRE_LOGGING_HPP /** * @brief Tag type to print the last system error * * The statement `std::out << sys_err();` will print the last system error * in the following format: "error description (errno)". * This is based on `GetLastError()` (Windows) or `errno` (all other systems). */ struct sys_err {}; // TODO: support lite-version of logging on embedded systems #if FIBRE_MAX_LOG_VERBOSITY #include #include #include #if defined(_WIN32) || defined(_WIN64) #include "windows.h" #endif #if defined(_WIN32) || defined(_WIN64) || defined(__linux__) || defined(__APPLE__) || defined(EMSCRIPTEN) #include #include #else // We don't want included on an embedded system as it makes the // binary huge. struct StdoutStream : std::ostream { void operator <<(const char * str) { printf("%s", str); } }; namespace std { extern StdoutStream cerr; } #endif #if defined(_WIN32) || defined(_WIN64) || defined(__linux__) #include using TMutex = std::mutex; using TLock = std::unique_lock; #else using TMutex = int; struct TLock { TLock() {} TLock(TMutex) {} }; #endif namespace fibre { // Maximum log verbosity that should be compiled into the binary. // Log entries with a higher verbosity should be optimized away. #ifndef FIBRE_MAX_LOG_VERBOSITY # define FIBRE_MAX_LOG_VERBOSITY LOG_LEVEL_T #endif // Default log verbosity that should be used for all topic. This may be // overridden by other sources, see description in the beginning of this file. #ifndef FIBRE_DEFAULT_LOG_VERBOSITY # define FIBRE_DEFAULT_LOG_VERBOSITY LOG_LEVEL_W #endif /** * @brief Generates one log entry. * * The log entry will be associates with the topic specified in "USE_LOG_TOPIC". * * Note that the log entry must only be used in the statement it is generated. (TODO: fix) * * @param level: Can be one of "E", "W", "D", or other levels defined in * log_level_t. * @returns a stream for writing into the log entry */ #define FIBRE_LOG(level) \ fibre::make_log_entry( \ fibre::get_file_name(MAKE_SSTRING(__FILE__){}), __LINE__, __func__ \ ).get_stream() /** * @brief Defines a log topic. A log topic must be defined exactly once in every * translation unit it is used. */ #define DEFINE_LOG_TOPIC(name) \ struct LOG_TOPIC_ ## name { \ static const char * get_label() { \ static const char label[] = #name; \ return label; \ } \ } /** * @brief Activates the use of the specified log topic for the current scope * (and all subscopes) */ #define USE_LOG_TOPIC(name) using current_log_topic = LOG_TOPIC_ ## name /** * @brief Overrides the general log verbosity settings for a specific topic. * If used, this should be placed in the same scope as the corresponding * DEFINE_LOG_TOPIC. */ #define CONFIG_LOG_TOPIC(topic, default_verbosity, max_verbosity) \ template<> constexpr log_level_t get_default_log_verbosity() { return (default_verbosity); } \ template<> constexpr log_level_t get_max_log_verbosity() { return (max_verbosity); } /** @brief Log verbosity levels */ enum log_level_t { LOG_LEVEL_F = 0, // fatal LOG_LEVEL_E = 1, // error LOG_LEVEL_W = 2, // warning LOG_LEVEL_I = 3, // info LOG_LEVEL_D = 4, // debug LOG_LEVEL_T = 5, // trace }; class NullBuffer : public std::streambuf { public: int overflow(int c) { return c; } }; // Source: https://stackoverflow.com/questions/15845505/how-to-get-higher-precision-fractions-of-a-second-in-a-printout-of-current-tim static std::string get_local_time() { auto now(std::chrono::system_clock::now()); auto seconds_since_epoch( std::chrono::duration_cast(now.time_since_epoch())); // Construct time_t using 'seconds_since_epoch' rather than 'now' since it is // implementation-defined whether the value is rounded or truncated. std::time_t now_t( std::chrono::system_clock::to_time_t( std::chrono::system_clock::time_point(seconds_since_epoch))); char temp[10]; if (!std::strftime(temp, 10, "%H:%M:%S.", std::localtime(&now_t))) return ""; return std::string(temp) + std::to_string((now.time_since_epoch() - seconds_since_epoch).count()); } class Logger { public: class Entry { public: Entry() : base_stream_(null_stream), lock_() {} Entry(std::ostream& base_stream, log_level_t level, const char* topic, const char* filename, size_t line_no, const char *funcname, TMutex& mutex) : base_stream_(base_stream), lock_(mutex) { switch (level) { case LOG_LEVEL_W: base_stream << "\x1b[93;1m"; break; case LOG_LEVEL_E: case LOG_LEVEL_F: base_stream << "\x1b[91;1m"; break; default: break; } base_stream << get_local_time() << " "; base_stream << std::dec << "[" << topic << "] "; //base_stream << std::dec << filename << ":" << line_no << " in " << funcname << "(): "; } ~Entry() { get_stream() << "\x1b[0m" << std::endl; } std::ostream& get_stream() { return base_stream_; }; private: NullBuffer null_buffer{}; std::ostream null_stream{&null_buffer}; std::ostream& base_stream_; TLock lock_; }; TMutex mutex_; }; template constexpr log_level_t get_default_log_verbosity() { return (log_level_t)FIBRE_DEFAULT_LOG_VERBOSITY; } template constexpr log_level_t get_max_log_verbosity() { return (log_level_t)FIBRE_MAX_LOG_VERBOSITY; } /** * @brief Resolves the currently active log verbosity for the given topic. * See top of this file for a detailed description of the algorithm. */ template log_level_t get_current_log_verbosity() { char var_name[sizeof("FIBRE_LOG_") + strlen(TOPIC::get_label())]; strcpy(var_name, "FIBRE_LOG_"); strcat(var_name, TOPIC::get_label()); // TODO: provide a way to disable the const char * var_val = std::getenv(var_name); if (!var_val) { var_val = std::getenv("FIBRE_LOG"); } log_level_t log_level = get_default_log_verbosity(); if (var_val) { unsigned long num = strtoul(var_val, nullptr, 10); log_level = (log_level_t)num; } if (log_level > get_max_log_verbosity()) { log_level = get_max_log_verbosity(); } return log_level; } /* template void send_to_stream(TStream&& stream); template void send_to_stream(TStream&& stream) { } template void send_to_stream(TStream&& stream, T&& value, Ts&&... values) { send_to_stream(std::forward(stream) << std::forward(value), std::forward(values)...); }*/ Logger* get_logger(); // defined in logging.cpp template Logger::Entry make_log_entry(const char *filename, size_t line_no, const char *funcname) { if (get_current_log_verbosity() < LEVEL) { return {}; } else { Logger* logger = get_logger(); return { std::cerr, LEVEL, TOPIC::get_label(), filename, line_no, funcname, logger->mutex_ }; } } template constexpr const char * get_file_name(TFilepath file_path) { return (file_path /*file_path.after_last_index_of('/')*/).c_str(); // TODO: extract file name (without path) } } namespace std { static inline std::ostream& operator<<(std::ostream& stream, const sys_err&) { #if defined(_WIN32) || defined(_WIN64) auto error_code = GetLastError(); #else auto error_code = errno; #endif return stream << strerror(error_code) << " (" << error_code << ")"; } } #else #define DEFINE_LOG_TOPIC(topic) #define USE_LOG_TOPIC(topic) struct NullStream { template NullStream& operator<<(T val) { return *this; } }; #define FIBRE_LOG(level) NullStream() #endif // FIBRE_MAX_LOG_VERBOSITY #endif // __FIBRE_LOGGING_HPP