Last active
August 29, 2025 18:48
-
-
Save Urpagin/4cd87d7ff898c42adb552b1352f07a86 to your computer and use it in GitHub Desktop.
Simple C++17 threaded logger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // Created by Urpagin on 2025-08-28. | |
| // | |
| // A simple logger that does the work inside a dedicated thread so as | |
| // not to block other operations too much. | |
| // | |
| // Licence: MIT | |
| // | |
| // "Multiple Producers, Single Consumer" type of logger. | |
| #pragma once | |
| #include <atomic> | |
| #include <condition_variable> | |
| #include <iomanip> | |
| #include <iostream> | |
| #include <mutex> | |
| #include <queue> | |
| #include <string_view> | |
| #include <thread> | |
| namespace logger::detail { | |
| using item = std::tuple<std::string, bool>; | |
| static std::condition_variable cv; | |
| static std::queue<item> log_q; | |
| static std::mutex log_q_guard{}; | |
| // Mainly for the log() function warning, less for the log_kill_consumer where | |
| // I could've used a sentinel value, where I feel it could've been faster. | |
| static std::atomic<bool> do_stop{false}; | |
| static std::thread worker; | |
| /// @brief Returns a human-readable timestamp. | |
| inline std::string make_timestamp() { | |
| using namespace std::chrono; | |
| const std::time_t time = system_clock::to_time_t(system_clock::now()); | |
| const std::tm tm = *std::localtime(&time); | |
| std::ostringstream oss; | |
| oss << std::put_time(&tm, "%H:%M:%S"); | |
| return oss.str(); | |
| } | |
| static constexpr std::string_view RED{"\033[31m"}; | |
| static constexpr std::string_view BLUE{"\033[34m"}; | |
| static constexpr std::string_view RESET{"\033[0m"}; | |
| /// @brief Colours the string. | |
| inline std::string do_colour(const std::string &msg, const bool is_err) { | |
| std::ostringstream oss; | |
| if (is_err) { | |
| oss << RED << msg << RESET; | |
| } else { | |
| oss << BLUE << msg << RESET; | |
| } | |
| return oss.str(); | |
| } | |
| /// @brief Formats the string to be pretty~ | |
| inline std::string do_format(const std::string &msg, const bool is_err) { | |
| std::ostringstream oss; | |
| if (is_err) { | |
| oss << '[' << make_timestamp() << ']' << " [ERR] " << msg; | |
| return do_colour(oss.str(), is_err); | |
| } | |
| oss << '[' << make_timestamp() << ']' << " [INFO] " << msg; | |
| return do_colour(oss.str(), is_err); | |
| } | |
| /// @brief App logger, instead of std::{cout,cerr} each time. | |
| static void log(const std::string_view msg, const bool is_err) { | |
| if (do_stop) | |
| return; | |
| // Push to queue, making sure to guard. | |
| { | |
| std::lock_guard lk(log_q_guard); | |
| // clone the msg, having a reference put into another thread is very unsafe. | |
| log_q.emplace(std::string(msg), is_err); | |
| } | |
| // notify the worker | |
| cv.notify_one(); | |
| } | |
| } // namespace logger::detail | |
| namespace logger { | |
| namespace d = detail; | |
| /// @brief Starts the logger consumer. | |
| /// Logic inspired from https://en.cppreference.com/w/cpp/thread/condition_variable.html | |
| static void log_start() { | |
| // Our worker thread | |
| d::worker = std::thread([&]() { | |
| while (!d::do_stop || !d::log_q.empty()) { | |
| std::unique_lock lk(d::log_q_guard); | |
| d::cv.wait(lk, [&] { return !d::log_q.empty() || d::do_stop; }); | |
| // After the wait, we own the lock. | |
| // We don't allow an empty queue to be .front()'ed | |
| if (d::log_q.empty() && d::do_stop) | |
| break; | |
| auto [msg, is_stderr] = d::log_q.front(); | |
| d::log_q.pop(); | |
| lk.unlock(); | |
| (is_stderr ? std::cerr : std::cout) << d::do_format(msg, is_stderr) << '\n'; | |
| } | |
| }); | |
| // Don't detach because detaching and joining afterwards is UB. | |
| } | |
| /// @brief Kills logging - seals the log() function and waits for the queue to empty. | |
| static void log_end() { | |
| d::do_stop = true; | |
| d::cv.notify_all(); | |
| // Finally stop the thread. | |
| d::worker.join(); | |
| } | |
| /// @brief Logs a message with a blue colour, INFO mode. To stdout. | |
| /// Behavior: Appends a newline after argument expansions. | |
| /// Usage: info(a, b, c, d) E.g., err("hello", "world", a_var) | |
| template<class... Ts> | |
| static void info(const Ts &...xs) { | |
| std::ostringstream oss; | |
| (oss << ... << xs); | |
| d::log(oss.view(), false); | |
| } | |
| /// @brief Logs a message with a red colour, ERR mode. To stdout. | |
| /// Behavior: Appends a newline after argument expansions. | |
| /// Usage: err(a, b, c, d) E.g., err("hello", "world", a_var) | |
| template<class... Ts> | |
| static void err(const Ts &...xs) { | |
| std::ostringstream oss; | |
| (oss << ... << xs); | |
| d::log(oss.view(), true); | |
| } | |
| } // namespace logger | |
| /// Please remove this function. It is a showcase only. | |
| void main_example() { | |
| using namespace logger; | |
| // Open the sink. | |
| log_start(); | |
| // Do your logging | |
| constexpr int a_var{1337}; | |
| info("Hello, ", "World: ", a_var); | |
| err("Oh no! ", "This is an error: ", a_var); | |
| // Close the sink, waiting on the queue - blocking. | |
| // This function does NOT return until all | |
| // messages are printed on stdout or stderr. | |
| log_end(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment