A single-header, zero-dependency C++20 logging library with a pluggable LogSink abstraction, built-in console and file sinks, RAII stream logging, and debug message boxes — all with thread-safe sink management and atomic log levels.
- Pluggable Sink Architecture —
LogSinkabstract base class withConsoleSinkandFileSinkbuilt in. Register custom sinks at runtime. - RAII Stream Logging —
LogMessageaccumulates viaoperator<<and commits on destruction. Zero manual flush. - Six Log Levels —
Trace,Debug,Info,Warning,Error,Criticalwith colored console output. - Debug Message Boxes —
MessageBoxHelperpops up Windows message boxes for critical errors during development. - Compile-time Level Elision —
TLOG/DLOGcompile to no-ops in release builds viaNullLog. - Atomic Log Levels — Each sink independently manages its threshold with
std::atomic<Level>. - Header-only —
#include "logger.hpp"is all you need (requires<windows.h>).
#include "logger.hpp"
int main() {
// Default: ConsoleSink at Trace level — just works.
ELOG << "Something went wrong: " << e.what();
// Add file logging
stdpp::log::prepare_file_logging("logs/");
WLOG << "This goes to both console and file";
// Debug-only logging (compiled away in Release)
DLOG << "Debug info: x = " << x;
// Message box for critical errors (Debug only)
EMSG(ELOG) << "Fatal error — check logs";
// ^ Pops up a message box AND writes to ELOG stream
}Logger (namespace stdpp::log)
├── log() dispatches to all registered sinks
├── LogSink (abstract base)
│ ├── ConsoleSink — ANSI-colored std::print output
│ ├── FileSink — timestamped file output
│ └── [Custom] — user-defined sinks
├── LogMessage — RAII stream builder → calls log() on destruction
├── MessageBoxHelper — debug popup + optional log duplication
├── NullLog — no-op sink for compile-time elision
└── ConsoleManager — Windows console setup (VT processing, encoding)
namespace stdpp::log {
enum class Level : std::uint8_t { None, Trace, Debug, Info, Warning, Error, Critical };| Macro | Level | Release Behavior |
|---|---|---|
CLOG |
Critical | Active |
ELOG |
Error | Active |
WLOG |
Warning | Active |
ILOG |
Info | Active |
TLOG |
Trace | NullLog (no-op) |
DLOG |
Debug | NullLog (no-op) |
ELOG << "Connection failed: " << err;
WLOG << "Retrying in " << delay << "ms";| Macro | Level | Release Behavior |
|---|---|---|
CMSG / EMSG / WMSG / IMSG |
C/E/W/I | NullLog (no-op) |
DMSG / TMSG |
D/T | NullLog (no-op) |
EMSG(log_macro) shows a message box AND duplicates output to log_macro:
EMSG(ELOG) << "Fatal error"; // popup + ELOG stream// Register a custom sink (thread-safe)
stdpp::log::add_sink(std::make_unique<MyCustomSink>());
// Remove all sinks
stdpp::log::clear_sinks();
// Prepare file logging (replaces previous FileSink)
stdpp::log::prepare_file_logging("logs/");class LogSink {
public:
virtual ~LogSink() = default;
virtual auto write(Level level, std::string_view message) -> void = 0;
auto set_level(Level lvl) -> void;
[[nodiscard]] auto get_level() const -> Level;
protected:
std::atomic<Level> level_{Level::Trace};
};stdpp::log::ConsoleManager::open_console(); // Alloc/attach console window
stdpp::log::ConsoleManager::clear_console(); // Clear console screen
stdpp::log::ConsoleManager::set_io_fast(); // Disable stdio bufferingclass JsonSink final : public stdpp::log::LogSink {
public:
explicit JsonSink(std::filesystem::path path) : file_(path, std::ios::app) {
set_level(stdpp::log::Level::Warning);
}
auto write(stdpp::log::Level level, std::string_view msg) -> void override {
if (!file_.is_open()) return;
file_ << std::format(R"({{"level":"{}","msg":"{}"}})", get_level_text(level), msg) << '\n';
}
private:
std::ofstream file_;
};
// Register it
stdpp::log::add_sink(std::make_unique<JsonSink>("app.json"));
// Now WLOG, ELOG, CLOG will also write JSON output- Sink registration (
add_sink,clear_sinks) — protected by internal mutex. - Sink
write()calls — dispatched under a shared lock. EachFileSinkhas its own mutex for ofstream access. - Log level changes (
set_level) — atomic stores, lock-free. LogMessagestream — single-threaded by nature (local variable). Do not share oneLogMessageacross threads.
// test_logger.cpp — compile: cl /std:c++20 /EHsc test_logger.cpp
#include "logger.hpp"
#include <cassert>
#include <sstream>
void test_console_sink() {
// ConsoleSink writes to stdout (verify visually or capture)
auto sink = std::make_unique<stdpp::log::ConsoleSink>();
assert(sink->get_level() == stdpp::log::Level::Trace);
sink->set_level(stdpp::log::Level::Error);
assert(sink->get_level() == stdpp::log::Level::Error);
}
void test_file_sink() {
auto sink = std::make_unique<stdpp::log::FileSink>("test_logs");
assert(sink->get_level() == stdpp::log::Level::Trace);
// Verify test_logs/logs.log is created
}
void test_log_message() {
stdpp::log::LogMessage msg(stdpp::log::Level::Info,
std::source_location::current());
msg << "Hello " << 42;
// msg destructor calls log() — writes to registered sinks
}
void test_null_log() {
stdpp::log::NullLog nl;
nl << "this is " << "ignored"; // no-op, no side effects
}
int main() {
test_console_sink();
test_file_sink();
test_log_message();
test_null_log();
}