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.


loggervscout, and then exiting the program, then you're creating the static logger (astd::ostringstream) every time... \$\endgroup\$std::ostringstreamabove, my bad). \$\endgroup\$