I made a multithreaded logger years ago when I was still a total newbie. It "worked" (when it didn't deadlock). Now that I'm slightly less newbie I'd like to get some criticism on this new version.
The logger spawns an internal thread on its own method writer()
, whose purpose is writing (to console and to file) the logging information.
There are two queues, one from which the writer writes data (queue_write
), the other where the rest of the program can push data (queue_log
). Upon pushing, the writer is awakened, it takes control of a mutex to make sure no other thread is accessing the queue_log
queue, and it swaps the content with queue_write
; then releases the lock. That way, threads using the logger would at worst have to wait for a simple pointer swap between two queues, which should be efficient.
After the swap, the writer thread empties his new queue on the output streams, until it's empty, then proceeds to check if the logger is still running (false when destructed or explicitly closed), takes the lock again, and if the queue_log
is empty it goes asleep releasing the lock.
A couple of notes:
- The "
utils
" namespace is a namespace of utilities I made for myself and willingly reflects the STL implementation. The choice of having the class start in lower case is made on purpose. - The logger is made with one thread producing and the inner thread consuming; it's not made for multiple consumers. It can have multiple producers but it's not optimized with that in mind.
- The
Message
class I use in the usage example is stripped down of all the non-relevant content (I've another portion ofutils
taking care of colouring output to console).
logger.h
#pragma once
#include <fstream>
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
namespace utils
{
//TODO
// With C++20 replace the time_points with actual readable time https://stackoverflow.com/questions/62954221/c-print-chronotime-pointchronohigh-resolution-clock-to-be-readable
template <typename Message_type>
class logger
{
private:
std::ofstream file;
std::queue<Message_type> queue_log;
std::queue<Message_type> queue_write;
std::thread thread;
std::atomic_bool running = true;
std::mutex queue_free;
std::condition_variable work_available;
void writer()
{
while (running)
{
if (true)
{
std::unique_lock lock{queue_free};
if (queue_log.empty()) { work_available.wait(lock); }
queue_write.swap(queue_log);
}
write_all();
}
}
void write_all()
{
while (!queue_write.empty())
{
Message_type& message = queue_write.front();
std::cout << message << std::endl;
file << message << std::endl;
queue_write.pop();
}
}
public:
logger(const std::string& file_name = "log.txt")
: thread(&logger::writer, this) { file.open(file_name); }
logger(const logger& copy) = delete; // don't copy threads please, thank you
logger& operator=(const logger& copy) = delete; // don't copy threads please, thank you
logger(logger&& move) noexcept = default; //could make those (running is already a flag for the instance being alive or not) but I'm lazy
logger& operator=(logger&& move) = default; //could make those (running is already a flag for the instance being alive or not) but I'm lazy
~logger() { if (running) { close(); } }
//Push messages begin
void operator<<(const Message_type& message) { push(message); }
void operator()(const Message_type& message) { push(message); }
template <typename ...Args>
void emplace(Args&&... args)
{
Message_type message{args...};
push(message);
}
void push(const Message_type& message)
{
if (true)
{
std::lock_guard lock(queue_free);
queue_log.push(message);
}
work_available.notify_one();
}
//Push messages end
void close()
{
running = false;
work_available.notify_one();
thread.join();
queue_write.swap(queue_log);
write_all();
file.close();
}
};
}
Usage example
Message.h
#pragma once
#include <chrono>
#include <iomanip>
#include <string>
#include <string_view>
class Message
{
private:
static std::string filter_last_newline(const std::string& string)
{
if (string.length() > 0 && string[string.length() - 1] == '\n') { return string.substr(0, string.length() - 1); }
else { return string; }
}
public:
enum class Type { log, dgn, inf, wrn, err };
Message(Type type, const std::string& string, std::chrono::time_point<std::chrono::system_clock> time) noexcept
: type(type), string(string), time(time)
{}
Message(Type type, const std::string& string) noexcept
: type(type), string(string), time(std::chrono::system_clock::now())
{}
Message(const std::string& string) noexcept
: type(Type::log), string(string), time(std::chrono::system_clock::now())
{}
Type type = Type::log;
std::string string = "";
std::chrono::time_point<std::chrono::system_clock> time;
const char* out_type() const noexcept
{
switch (type)
{
case Message::Type::log: return "[LOG]"; // [LOG]
case Message::Type::dgn: return "[DGN]"; // [DIAGNOSTIC]
case Message::Type::inf: return "[INF]"; // [INFO]
case Message::Type::wrn: return "[WRN]"; // [WARNING]
case Message::Type::err: return "[ERR]"; // [ERROR]
}
}
static Message log(const std::string& string) { return {Type::log, filter_last_newline(string), std::chrono::system_clock::now()}; }
static Message dgn(const std::string& string) { return {Type::dgn, filter_last_newline(string), std::chrono::system_clock::now()}; }
static Message inf(const std::string& string) { return {Type::inf, filter_last_newline(string), std::chrono::system_clock::now()}; }
static Message wrn(const std::string& string) { return {Type::wrn, filter_last_newline(string), std::chrono::system_clock::now()}; }
static Message err(const std::string& string) { return {Type::err, filter_last_newline(string), std::chrono::system_clock::now()}; }
friend std::ostream& operator<<(std::ostream& os, const Message& m)
{
size_t beg = 0;
size_t end = m.string.find_first_of('\n', beg);
if (end == std::string::npos) { end = m.string.length(); }
//First line
os << std::setw(18) << m.time.time_since_epoch().count() << ' ';
os << m.out_type();
os << ' ' << std::string_view(m.string).substr(beg, end - beg) << '\n';
//Other lines
while (true)
{
if (end == m.string.length()) { break; }
else
{
beg = end + 1;
end = m.string.find_first_of('\n', beg);
if (end == std::string::npos) { end = m.string.length(); }
}
os << std::setw(24) << '|';
os << ' ' << std::string_view(m.string).substr(beg, end - beg) << '\n';
}
return os;
}
};
Main.cpp
#include "utils/logger.h"
#include "Message.h"
int main()
{
utils::logger<Message> logger;
logger.push(Message(Message::Type::err, "This is a verbose way to write an error."));
logger.emplace(Message::Type::err, "This is a still quite verbose to write an error.");
logger.push(Message::err("This is a less verbose way to write an error."));
logger << Message::err("This is the slightly shorter way to write an error.");
logger(Message::err("This is the shortest I could think of, although far from the \"standard\" way of doing things."));
logger(Message::wrn("Multi\nline\nwarning\njust\nto\nmake\nsure\nit\nworks."));
return 0;
}
std::cout
rather than tostd::clog
? \$\endgroup\$