Skip to content

Instantly share code, notes, and snippets.

@SlowPokeInTexas
Forked from kevinkreiser/logging.hpp
Last active June 6, 2021 21:41
Show Gist options
  • Save SlowPokeInTexas/cfb38dcd947d33af567b541a2cc982e5 to your computer and use it in GitHub Desktop.
Save SlowPokeInTexas/cfb38dcd947d33af567b541a2cc982e5 to your computer and use it in GitHub Desktop.
Thread-safe logging singleton c++11
#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 <string>
#include <stdexcept>
#include <iostream>
#include <fstream>
#include <sstream>
#include <mutex>
#include <unordered_map>
#include <memory>
#include <chrono>
#include <ctime>
#include <cstdlib>
// Friggin' Microsoft and their global generic macros.
#if defined(_WIN32)
#include "color.hpp" // Pull in header from https://github.com/imfl/color-console
#if defined(ERROR)
#undef ERROR // Unfortunately ERROR is defined as a macro in windgi.h...
#endif
#if defined( _MSC_VER)
#pragma warning(disable : 4996)
#endif
#endif
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 <typename T> std::size_t operator() ( T t ) const
{
return static_cast<std::size_t> ( t );
}
};
const std::unordered_map<log_level, std::string, enum_hasher> uncolored
{
{log_level::ERROR, " [ERROR] "}, {log_level::WARN, " [WARN] "}, {log_level::INFO, " [INFO] "},
{log_level::DEBUG, " [DEBUG] "}, {log_level::TRACE, " [TRACE] "}
};
#if !defined( _WIN32 )
const std::unordered_map<log_level, std::string, enum_hasher> 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 "}
};
#else
const std::unordered_map<log_level, std::string, enum_hasher> colored
{
{log_level::ERROR, " [ERROR] "},
{log_level::WARN, " [WARN] "},
{log_level::INFO, " [INFO] "},
{log_level::DEBUG, " [DEBUG] "},
{log_level::TRACE, " [TRACE] "}
};
#endif
//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
#if defined(_WIN32)
struct tm *gmtime_r ( const time_t *timer, struct tm *buf )
{
#if defined( _MSC_VER )
gmtime_s ( buf, timer );
#elif defined(__BORLANDC__)
gmtime_s ( timer, buf );
#endif
return buf;
}
#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<double> 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.xxxxxx" );
sprintf ( &buffer.front(), "%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() );
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<std::string, std::string>;
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 ) {}
#if !defined(_WIN32)
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 );
}
#else
virtual void log(const std::string& message, const log_level level)
{
if (level < LOG_LEVEL_CUTOFF)
{
return;
}
std::lock_guard<std::mutex> lk{lock};
cout << timestamp();
switch( level )
{
case log_level::ERROR:
cout << dye::red( levels.find(level)->second );
break;
case log_level::WARN:
cout << dye::yellow( levels.find(level)->second );
break;
case log_level::INFO:
cout << dye::green( levels.find(level)->second );
break;
case log_level::DEBUG:
cout << dye::blue( levels.find(level)->second );
break;
case log_level::TRACE:
cout << dye::white( levels.find(level)->second );
break;
}
cout << dye::white( message ) << std::endl;
}
#endif
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<std::mutex> lk{lock};
std::cout << message;
std::cout.flush();
}
protected:
const std::unordered_map<log_level, std::string, enum_hasher> 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<std::string, logger_creator> 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<logger> 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 );
}
} // end namespace logging
#endif //__LOGGING_HPP__
#ifdef TEST_LOGGING
#include <thread>
#include <vector>
#include <functional>
#include <memory>
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<std::shared_ptr<std::thread>> 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
@SlowPokeInTexas
Copy link
Author

SlowPokeInTexas commented Jun 6, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment