This lightweight logging module for C++ projects provides dual output to the console and files, chained log message composition, thread-safe writes, and automatic resource cleanup. It addresses cumbersome logging APIs, tightly coupled output strategies, and out-of-order writes in multithreaded environments. Keywords: C++ logging module, Strategy Pattern, RAII.
This logging module uses strategy-based decoupling and RAII-based commit as its core architecture
| Parameter | Description |
|---|---|
| Language | C++17 |
| License | Not explicitly stated in the article; the original page is CC 4.0 BY-SA |
| Stars | Not provided |
| Core dependencies | ` |
,,,,, mutex wrappersMutex/LockGuard` |
|
| Output backends | ConsoleStrategy, FileLogStrategy |
| Key features | Thread safety, microsecond timestamps, strategy switching, RAII-based automatic flush |
The focus of this implementation is not simply to “print a line of text,” but to separate log creation, output media, and resource management. This keeps the API minimal while leaving room for future extensions such as network logging, asynchronous logging, and level-based routing.
The original design uses Logger as the central class to manage output strategies uniformly, LogMessage to commit logs automatically during destruction, and an abstract strategy interface to hide the differences between console output and file persistence. This is a classic engineering-oriented design rather than ad hoc string concatenation.
The architecture has very clear responsibility boundaries
class LogStrategy {
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string& message) = 0; // Define a unified log flush interface
};
class Logger {
public:
void UseConsoleStrategy(); // Switch to console output
void UseFileStrategy(); // Switch to file output
private:
std::unique_ptr
<LogStrategy> _strategy; // Manage the strategy object with a smart pointer
};
This code defines the most important abstraction boundary in the logging system: the strategy determines how a log is written, not the caller.
Log metadata encapsulation defines the lower bound of observability
A usable log entry should contain at least the timestamp, level, process, file, and line number. The original implementation uses an enum to manage log levels and a separate function to generate microsecond-level timestamps, which makes the formatting logic reusable and prepares the system for future structured fields.
One detail especially worth keeping is localtime_r. It is a reentrant function that avoids timestamp formatting corruption caused by shared static buffers in multithreaded scenarios. Many logging implementations that “seem to work” leave concurrency risks exactly here.
Time and level conversion form the foundational stability layer
enum class LogLevel {
INFO, WARNING, ERROR, FATAL, DEBUG
};
std::string LogLevel2Message(LogLevel level) {
switch (level) {
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
case LogLevel::DEBUG: return "DEBUG";
default: return "UNKNOWN"; // Fallback for unknown levels
}
}
This code maps log levels to human-readable text and serves as the entry point for standardized log output.
Output strategy abstraction makes the module truly follow the Open/Closed Principle
Console output and file output share one core behavior: both receive a complete string. Their differences lie in the write target and flush behavior. Unifying them behind the SyncLog interface gives the design the right abstraction level.
The console strategy protects std::cerr with a mutex to prevent interleaved output in multithreaded scenarios. Choosing cerr also aligns better with logging semantics because it is unbuffered by default and better suited for immediate output of errors, debugging, and diagnostic information.
The console strategy outputs logs immediately and safely
class ConsoleStrategy : public LogStrategy {
public:
void SyncLog(const std::string& message) override {
LockGuard lockguard(_mutex); // Lock to prevent interleaving during multithreaded output
std::cerr << message << std::endl;
}
private:
Mutex _mutex;
};
This code implements the most basic and most frequently used terminal logging backend.
The file strategy is closer to real-world engineering needs. It not only writes logs but also handles missing directories, append mode, exception-sensitive file access, and timely handle release. These details determine whether the module is truly fit for production use.
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const std::string& path = "./log", const std::string& name = "log.txt")
: _logpath(path), _logfilename(name) {
if (!std::filesystem::exists(_logpath)) {
std::filesystem::create_directories(_logpath); // Create the log directory automatically
}
}
void SyncLog(const std::string& message) override {
LockGuard lockguard(_mutex); // Protect the critical section for file writes
std::ofstream out(_logpath + "/" + _logfilename, std::ios::app); // Append mode preserves historical logs
if (!out.is_open()) return;
out << message << "\n";
}
private:
std::string _logpath, _logfilename;
Mutex _mutex;
};
This code implements persistent file logging and ensures predictable directory initialization and append-write behavior.
RAII-style log commit significantly reduces call-site complexity
The most valuable part of this module is that it delegates the log commit timing to the object lifecycle. Callers only compose the message and no longer need to manually call flush, commit, or write. When the statement ends and the object is destroyed, the log is written automatically.
This approach reduces the chance of missed commits and makes the API feel closer to std::cout. For business code, writing logs becomes a stream-like operation rather than interaction with a complex component.
LogMessage automatically commits the full log during destruction
class LogMessage {
public:
LogMessage(LogLevel level, std::string filename, int line, Logger& logger)
: _logger(logger) {
std::stringstream ss;
ss << "[" << GetCurrentTime() << "] "
<< "[" << LogLevel2Message(level) << "] "
<< "[" << getpid() << "] "
<< "[" << filename << "] "
<< "[" << line << "] - "; // Append fixed metadata first
_loginfo = ss.str();
}
template <typename T>
LogMessage& operator<<(const T& info) {
std::stringstream ss;
ss << info; // Append custom business content
_loginfo += ss.str();
return *this;
}
~LogMessage() {
if (_logger._strategy) {
_logger._strategy->SyncLog(_loginfo); // Automatically commit the log during destruction
}
}
private:
std::string _loginfo;
Logger& _logger;
};
This code extends RAII from “lock management” to “log commit,” making it the module’s most important usability feature.
Macro wrappers further reduce integration cost
If every log call must manually pass __FILE__ and __LINE__, the API quickly becomes cumbersome. The value of macros here is not clever syntax, but automatic injection of context information and reduced boilerplate.
Macros make logging nearly effortless to call
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy()
This code compresses a complex entry point into a single unified calling surface, making it easy to roll out across business projects.
This implementation already provides a solid foundation for small engineering projects
From a capability perspective, it already covers the core foundation of a logging module: unified formatting, thread safety, strategy switching, file persistence, RAII-based commit, and simplified usage. It is especially suitable for Linux server applications, lightweight middleware, and command-line tools.
If you continue evolving it, the recommended priority order is: log level filtering, date-based log rotation, asynchronous queue-based writes, templated formatting, and JSON structured logging output. That path moves the module from “usable” to “production-grade.”
FAQ
Why does this logging module use both the Strategy Pattern and RAII?
The Strategy Pattern solves the extensibility problem of “where to write logs,” while RAII solves the reliability problem of “when to commit and how to release resources safely.” Together, they produce a clean and extensible interface.
Why use localtime_r instead of localtime?
localtime_r is the thread-safe reentrant version and is well suited for multithreaded logging scenarios. localtime uses a shared static buffer internally, which can lead to corrupted time formatting under concurrency.
What is still missing before this becomes a production-grade logging system?
It still needs log level filtering, asynchronous writing, log rotation, failure recovery, performance benchmarking, and structured output. But as a lightweight foundational module, its architectural direction is correct.
AI Readability Summary
This article reconstructs a lightweight C++ logging module built around two core ideas: using the Strategy Pattern to decouple output backends and using RAII for automatic flushing and resource management. The design delivers console and file backends, thread safety, high-precision timestamps, and low-coupling extensibility, making it a strong logging foundation for small and mid-sized engineering projects.