What is it?
It's a fairly simple logger which utilises a thread.
How it works
Note: this a pretty terrible explanation, perhaps it would be easier to look at the code.
The logger class is a singleton, which contains the function log(). Upon logs()'s first use a static logger instance is created. The function then returns a logstream object (constructed with a reference to the logger), which is a derivative of std::ostringstream. This object is used to format the message. Upon its destruction it sends the formatted std::string back to the logger using the push() function, which locks a mutex and then appends the std::string to a private std::queue belonging to the logger.
When the logger is constructed it creates a thread running the print_routine() function, which is a loop that locks a mutex and prints all the contents of the std::queue, and then sleeps for a set interval. Upon destruction it tells the routine to finish by setting the bool print to false and joins the thread.
Code
log_enum.h
#ifndef ANDROMEDA_LOG_ENUM_H
#define ANDROMEDA_LOG_ENUM_H
namespace andromeda {
enum class log_level {
info,
warning,
severe,
fatal
};
}
#endif
logger.h
#ifndef ANDROMEDA_LOGGER_H
#define ANDROMEDA_LOGGER_H
#include <sstream>
#include <mutex>
#include <queue>
#include <chrono>
#include <thread>
#include "log_enum.h"
namespace andromeda {
class logger;
}
#include "logstream.h"
namespace andromeda {
class logger {
std::queue<std::string> m_q;
std::mutex m_q_mu;
std::mutex m_stdout_mu;
std::mutex m_stderr_mu;
std::thread m_print_thread;
bool m_print = true;
static void print_routine(logger *instance, std::chrono::duration<double, std::milli> interval);
logger();
~logger();
public:
logger(logger const&) = delete;
void operator=(logger const&) = delete;
static logstream log(log_level level = log_level::info) {
static logger m_handler;
return logstream(m_handler, level);
}
void push(std::string fmt_msg);
};
}
#endif
logger.cpp
#include "logger.h"
#include <iostream>
namespace andromeda {
logger::logger()
{
m_print_thread = std::thread(print_routine, this, std::chrono::milliseconds(16));
}
logger::~logger()
{
m_print = false;
m_print_thread.join();
}
void logger::push(std::string fmt_msg)
{
std::lock_guard<std::mutex> lock(m_q_mu);
m_q.push(fmt_msg);
}
void logger::print_routine(logger *instance, std::chrono::duration<double, std::milli> interval)
{
while(instance->m_print || !instance->m_q.empty()) {
auto t1 = std::chrono::steady_clock::now();
{
std::lock_guard<std::mutex> lock(instance->m_q_mu);
while(!instance->m_q.empty()) {
std::cout << instance->m_q.front() << std::endl;
instance->m_q.pop();
}
}
auto t2 = std::chrono::steady_clock::now();
std::chrono::duration<double, std::milli> time_took = t2 - t1;
//sleep
if(time_took < interval && instance->m_print) {
std::this_thread::sleep_for(interval - time_took);
}
}
}
}
logstream.h
#ifndef ANDROMEDA_LOGSTREAM_H
#define ANDROMEDA_LOGSTREAM_H
#include <sstream>
#include "log_enum.h"
namespace andromeda {
class logger;
class logstream : public std::ostringstream {
logger& m_logger;
log_level m_level;
std::string get_level_string();
std::string get_time_string();
public:
logstream(logger& log, log_level);
~logstream();
};
}
#endif
logstream.cpp
#include "logstream.h"
#include <ctime>
#include "logger.h"
namespace andromeda {
logstream::logstream(logger& log, log_level level) : m_logger(log), m_level(level)
{}
logstream::~logstream()
{
//note: not using time yet because it adds 0.015 ms
//m_logger.push(get_time_string() + get_level_string() + str());
m_logger.push(get_level_string() + str());
}
std::string logstream::get_level_string()
{
std::string temp;
switch(m_level) {
case log_level::info: temp = "[INFO]"; break;
case log_level::warning: temp = "[WARNING]"; break;
case log_level::severe: temp = "[SEVERE]"; break;
case log_level::fatal: temp = "[FATAL]"; break;
}
return temp; //copy ellision should be guaranteed with a C++17 compiler
}
std::string logstream::get_time_string()
{
std::time_t t = std::time(nullptr);
#ifdef _WIN32
std::tm time;
localtime_s(&time, &t);
#else
std::tm time = *std::localtime(&t);
#endif
char t_str[20];
std::strftime(t_str, sizeof(t_str), "%T", &time);
return ("[" + std::string(t_str) + "]");
}
}
main.cpp
#include "logger/logger.h"
#include <iostream>
int main(int argc, char **argv) {
{
using namespace andromeda;
auto t1 = std::chrono::steady_clock::now();
logger::log() << "Hello World";
auto t2 = std::chrono::steady_clock::now();
/*
auto t3 = std::chrono::steady_clock::now();
std::cout << "Hello World" << std::endl;
auto t4 = std::chrono::steady_clock::now();
*/
std::chrono::duration<double, std::milli> d1 = t2 - t1;
//std::chrono::duration<double, std::milli> d2 = t4 - t3;
logger::log() << "logger took " << d1.count() << "ms";
//std::cout << "cout took " << d2.count() << "ms" << std::endl;
//This line is here to make test whether everything is printed before program exit
logger::log(log_level::fatal) << "end of program test: " << 33;
}
return 0;
}
Benchmark
I ran a benchmark of this logger vs std::cout without using the time.
run 1: logger = 0.02925ms and cout = 0.007725ms -> log/cout = 3.77
run 2: logger = 0.028469ms and cout = 0.008442ms -> log/cout = 3.37
run 3: logger = 0.027484ms and cout = 0.016155ms -> log/cout = 1.7
run 4: logger = 0.028764ms and cout = 0.007859ms -> log/cout = 3.66
run 5: logger = 0.027457ms and cout = 0.008173ms -> log.cout = 3.36
On average the logger was 3.172 times slower than std::cout. Is tha bad?
What I'm aiming for
I'm aiming for it to be reasonably fast, thread-safe and cross-platform.
What I think could be improved
I think the get_time_string() could be improved. At the moment it worsens performance by about 50%. Another things is the detail. I think it might be a good idea to perhaps include the source and thread id. One last minor thing is the log_level. I don't have much experience so I don't know how many different levels are required for bigger projects.
Any feedback is appreciated.
logger
vscout
, and then exiting the program, then you're creating the static logger (astd::ostringstream
) every time... \$\endgroup\$std::ostringstream
above, my bad). \$\endgroup\$