#ifndef __LOGGING_HPP__ #define __LOGGING_HPP__ /* Test this with something like: g++ -std=c++11 -x c++ -pthread -DLOGGING_LEVEL_ALL -DTEST_LOGGING logging.hpp -o logging_test ./logging_test */ #include #include #include #include #include #include #include #include #include #include #include namespace logging { //TODO: use macros (again) so __FILE__ __LINE__ could be automatically added to certain error levels? //the log levels we support enum class log_level : uint8_t { TRACE = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4 }; struct enum_hasher { template std::size_t operator()(T t) const { return static_cast(t); } }; const std::unordered_map uncolored { {log_level::ERROR, " [ERROR] "}, {log_level::WARN, " [WARN] "}, {log_level::INFO, " [INFO] "}, {log_level::DEBUG, " [DEBUG] "}, {log_level::TRACE, " [TRACE] "} }; const std::unordered_map colored { {log_level::ERROR, " \x1b[31;1m[ERROR]\x1b[0m "}, {log_level::WARN, " \x1b[33;1m[WARN]\x1b[0m "}, {log_level::INFO, " \x1b[32;1m[INFO]\x1b[0m "}, {log_level::DEBUG, " \x1b[34;1m[DEBUG]\x1b[0m "}, {log_level::TRACE, " \x1b[37;1m[TRACE]\x1b[0m "} }; //all, something in between, none or default to info #if defined(LOGGING_LEVEL_ALL) || defined(LOGGING_LEVEL_TRACE) constexpr log_level LOG_LEVEL_CUTOFF = log_level::TRACE; #elif defined(LOGGING_LEVEL_DEBUG) constexpr log_level LOG_LEVEL_CUTOFF = log_level::DEBUG; #elif defined(LOGGING_LEVEL_WARN) constexpr log_level LOG_LEVEL_CUTOFF = log_level::WARN; #elif defined(LOGGING_LEVEL_ERROR) constexpr log_level LOG_LEVEL_CUTOFF = log_level::ERROR; #elif defined(LOGGING_LEVEL_NONE) constexpr log_level LOG_LEVEL_CUTOFF = log_level::ERROR + 1; #else constexpr log_level LOG_LEVEL_CUTOFF = log_level::INFO; #endif //returns formated to: 'year/mo/dy hr:mn:sc.xxxxxx' inline std::string timestamp() { //get the time std::chrono::system_clock::time_point tp = std::chrono::system_clock::now(); std::time_t tt = std::chrono::system_clock::to_time_t(tp); std::tm gmt{}; gmtime_r(&tt, &gmt); std::chrono::duration fractional_seconds = (tp - std::chrono::system_clock::from_time_t(tt)) + std::chrono::seconds(gmt.tm_sec); //format the string std::string buffer("year/mo/dy hr:mn:sc.xxxxxx0"); snprintf(&buffer.front(), buffer.length(), "%04d/%02d/%02d %02d:%02d:%09.6f", gmt.tm_year + 1900, gmt.tm_mon + 1, gmt.tm_mday, gmt.tm_hour, gmt.tm_min, fractional_seconds.count()); //remove trailing null terminator added by snprintf buffer.pop_back(); return buffer; } //logger base class, not pure virtual so you can use as a null logger if you want using logging_config_t = std::unordered_map; class logger { public: logger() = delete; logger(const logging_config_t&) {}; virtual ~logger() {}; virtual void log(const std::string&, const log_level) {}; virtual void log(const std::string&) {}; protected: std::mutex lock; }; //logger that writes to standard out class std_out_logger : public logger { public: std_out_logger() = delete; std_out_logger(const logging_config_t& config) : logger(config), levels(config.find("color") != config.end() ? colored : uncolored) {} virtual void log(const std::string& message, const log_level level) { if(level < LOG_LEVEL_CUTOFF) return; std::string output; output.reserve(message.length() + 64); output.append(timestamp()); output.append(levels.find(level)->second); output.append(message); output.push_back('\n'); log(output); } virtual void log(const std::string& message) { //cout is thread safe, to avoid multiple threads interleaving on one line //though, we make sure to only call the << operator once on std::cout //otherwise the << operators from different threads could interleave //obviously we dont care if flushes interleave //std::lock_guard lk{lock}; std::cout << message; std::cout.flush(); } protected: const std::unordered_map levels; }; //TODO: add log rolling //logger that writes to file class file_logger : public logger { public: file_logger() = delete; file_logger(const logging_config_t& config):logger(config) { //grab the file name auto name = config.find("file_name"); if(name == config.end()) throw std::runtime_error("No output file provided to file logger"); file_name = name->second; //if we specify an interval reopen_interval = std::chrono::seconds(300); auto interval = config.find("reopen_interval"); if(interval != config.end()) { try { reopen_interval = std::chrono::seconds(std::stoul(interval->second)); } catch(...) { throw std::runtime_error(interval->second + " is not a valid reopen interval"); } } //crack the file open reopen(); } virtual void log(const std::string& message, const log_level level) { if(level < LOG_LEVEL_CUTOFF) return; std::string output; output.reserve(message.length() + 64); output.append(timestamp()); output.append(uncolored.find(level)->second); output.append(message); output.push_back('\n'); log(output); } virtual void log(const std::string& message) { lock.lock(); file << message; file.flush(); lock.unlock(); reopen(); } protected: void reopen() { //TODO: use CLOCK_MONOTONIC_COARSE //check if it should be closed and reopened auto now = std::chrono::system_clock::now(); lock.lock(); if(now - last_reopen > reopen_interval) { last_reopen = now; try{ file.close(); }catch(...){} try { file.open(file_name, std::ofstream::out | std::ofstream::app); last_reopen = std::chrono::system_clock::now(); } catch(std::exception& e) { try{ file.close(); }catch(...){} throw e; } } lock.unlock(); } std::string file_name; std::ofstream file; std::chrono::seconds reopen_interval; std::chrono::system_clock::time_point last_reopen; }; //a factory that can create loggers (that derive from 'logger') via function pointers //this way you could make your own logger that sends log messages to who knows where using logger_creator = logger *(*)(const logging_config_t&); class logger_factory { public: logger_factory() { creators.emplace("", [](const logging_config_t& config)->logger*{return new logger(config);}); creators.emplace("std_out", [](const logging_config_t& config)->logger*{return new std_out_logger(config);}); creators.emplace("file", [](const logging_config_t& config)->logger*{return new file_logger(config);}); } logger* produce(const logging_config_t& config) const { //grab the type auto type = config.find("type"); if(type == config.end()) throw std::runtime_error("Logging factory configuration requires a type of logger"); //grab the logger auto found = creators.find(type->second); if(found != creators.end()) return found->second(config); //couldn't get a logger throw std::runtime_error("Couldn't produce logger for type: " + type->second); } protected: std::unordered_map creators; }; //statically get a factory inline logger_factory& get_factory() { static logger_factory factory_singleton{}; return factory_singleton; } //get at the singleton inline logger& get_logger(const logging_config_t& config = { {"type", "std_out"}, {"color", ""} }) { static std::unique_ptr singleton(get_factory().produce(config)); return *singleton; } //configure the singleton (once only) inline void configure(const logging_config_t& config) { get_logger(config); } //statically log manually without the macros below inline void log(const std::string& message, const log_level level) { get_logger().log(message, level); } //statically log manually without a level or maybe with a custom one inline void log(const std::string& message) { get_logger().log(message); } //these standout when reading code inline void TRACE(const std::string& message) { get_logger().log(message, log_level::TRACE); } inline void DEBUG(const std::string& message) { get_logger().log(message, log_level::DEBUG); } inline void INFO(const std::string& message) { get_logger().log(message, log_level::INFO); } inline void WARN(const std::string& message) { get_logger().log(message, log_level::WARN); } inline void ERROR(const std::string& message) { get_logger().log(message, log_level::ERROR); } } #endif //__LOGGING_HPP__ #ifdef TEST_LOGGING #include #include #include #include void work() { std::ostringstream s; s << "hi my name is: " << std::this_thread::get_id(); std::string id = s.str(); for(size_t i = 0; i < 10000; ++i) { logging::ERROR(id); logging::WARN(id); logging::INFO(id); logging::DEBUG(id); logging::TRACE(id); } } int main(void) { //configure logging, if you dont it defaults to standard out logging with colors //logging::configure({ {"type", "file"}, {"file_name", "test.log"}, {"reopen_interval", "1"} }); //start up some threads std::vector> threads(std::thread::hardware_concurrency()); for(auto& thread : threads) { thread.reset(new std::thread(work)); } //wait for finish for(auto& thread : threads) { thread->join(); } return 0; } #endif