This implementation provides a thread-safe logging system for Linux/C++ server applications. Its core capabilities include log levels, dual console/file output strategies, RAII-based automatic flushing, and a glog-style streaming interface. It addresses common pain points such as interleaved logs in multithreaded environments, tight coupling between log content and output destinations, and non-thread-safe time functions. Keywords: C++ logging system, thread safety, Strategy pattern.
Technical Specifications at a Glance
| Parameter | Description |
|---|---|
| Language | C++11/17 |
| Runtime Environment | Linux / POSIX |
| Concurrency Primitive | pthread_mutex |
| File Support | std::filesystem |
| Logging Style | Compatible with glog-style streaming |
| Output Targets | Console stdout/stderr, file append writes |
| Core Dependencies | pthread, sstream, fstream, filesystem, unistd |
| GitHub Stars | Not provided in the original article |
The design goals of this logging system are clear
An industrial-grade logging system is not just a wrapper around std::cout. It must deliver consistent formatting, thread safety, extensibility, and a low cognitive load for developers. The original design separates log construction from log flushing, and that is the most important abstraction in this article.
A complete log entry consists of a timestamp, severity level, process information, file name, line number, and business message. Once assembled, the message is handed off to different strategies for output to the console or a file. With this design, future extensions such as databases, syslog, or network sinks become straightforward.
AI Visual Insight: This image serves as the article header and primarily introduces the topic. It does not present concrete architecture or code details, so it should not be treated as technical evidence.
Log formatting should be standardized first
Use a unified output format like the following to make grep, log collectors, and incident troubleshooting easier:
[2026-04-16 21:33:18] [DEBUG] [1030871] [Main.cc] [10] - hello world
This format preserves timestamp, severity, process, and source location information, which is enough for most debugging and production troubleshooting scenarios.
Thread safety is the first production requirement
When multiple threads write to the console or a file at the same time, output can easily become interleaved. To solve this, the original implementation first defines Mutex and LockGuard. This is a classic RAII pattern: lock in the constructor and unlock in the destructor, so exception paths never forget to release the mutex.
class Mutex {
public:
Mutex() { pthread_mutex_init(&_lock, nullptr); }
~Mutex() { pthread_mutex_destroy(&_lock); }
void Lock() { pthread_mutex_lock(&_lock); }
void UnLock() { pthread_mutex_unlock(&_lock); }
private:
pthread_mutex_t _lock;
};
class LockGuard {
public:
explicit LockGuard(Mutex* lockptr) : _lockptr(lockptr) {
_lockptr->Lock(); // Core logic: lock immediately when entering the scope
}
~LockGuard() {
_lockptr->UnLock(); // Core logic: unlock automatically when leaving the scope
}
private:
Mutex* _lockptr;
};
This code wraps low-level mutex operations in C++-style scoped resource management.
The timestamp module must use a reentrant function
One of the most easily overlooked issues in a logging system is not formatting, but thread safety in time functions. localtime relies on static internal storage, which gets overwritten in concurrent scenarios, so this implementation must use localtime_r instead.
std::string GetTimeStamp() {
time_t currentTime = time(nullptr);
struct tm dataTime;
localtime_r(¤tTime, &dataTime); // Core logic: use the reentrant version to avoid race conditions
char buf[128];
snprintf(buf, sizeof(buf), "%4d-%02d-%02d %02d:%02d:%02d",
dataTime.tm_year + 1900,
dataTime.tm_mon + 1,
dataTime.tm_mday,
dataTime.tm_hour,
dataTime.tm_min,
dataTime.tm_sec);
return buf;
}
This function generates a thread-safe, human-readable timestamp string suitable for persistent log output.
The Strategy pattern fully decouples log output from log content
The original implementation abstracts the flush behavior into LogStrategy, then provides concrete console and file strategies. This follows the Open/Closed Principle: to add a new destination, you only need to add a new strategy class.
class LogStrategy {
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string& message) = 0; // Core logic: unified flush interface
};
This code defines the common contract for all log output backends.
The console strategy must guarantee atomic output per log entry
class ConsoleLogStrategy : public LogStrategy {
public:
void SyncLog(const std::string& message) override {
LockGuard guard(&_mutex); // Core logic: protect the console critical section
std::cerr << message << std::endl;
}
private:
Mutex _mutex;
};
This strategy ensures that each console log entry is written as a complete unit in multithreaded scenarios, without character-level interleaving.
AI Visual Insight: This diagram shows the logging system initialization flow or module boundaries. It highlights the separation between the log formatting layer and the flush strategy layer, making runtime output switching easier. It serves as structural evidence for the Strategy pattern implementation.
The file strategy handles persistence and directory initialization
A file strategy must do more than write to disk. It should also ensure the target directory exists and use append mode so historical logs are not overwritten.
class FileLogStrategy : public LogStrategy {
public:
void SyncLog(const std::string& message) override {
LockGuard guard(&_mutex); // Core logic: serialize file writes
std::ofstream out("./log/log.txt", std::ios::app);
if (!out.is_open()) return; // Core logic: return immediately on open failure to avoid crashes
out << message << "\n";
}
private:
Mutex _mutex;
};
This strategy appends logs to a disk file in a thread-safe way.
The Logger core uses an RAII temporary object to deliver a glog-style experience
The most elegant part of the design is Logger::LogMessage. It builds the log prefix in the constructor, appends business content in operator<<, and automatically calls SyncLog in the destructor. As a result, users only need one line of code: LOG(...) << ....
class Logger {
public:
class LogMessage {
public:
LogMessage(LogLevel level, const std::string& file, int line, Logger& self)
: _logger(self) {
std::stringstream ss;
ss << "[" << GetTimeStamp() << "] "
<< "[" << LogLevel2String(level) << "] "
<< "[" << getpid() << "] "
<< "[" << file << "] "
<< "[" << line << "] - ";
_loginfo = ss.str(); // Core logic: build the log header during construction
}
~LogMessage() {
if (_logger._strategy) {
_logger._strategy->SyncLog(_loginfo); // Core logic: flush automatically during destruction
}
}
template <class T>
LogMessage& operator<<(const T& value) {
std::stringstream ss;
ss << value;
_loginfo += ss.str(); // Core logic: chain arbitrary streamable types
return *this;
}
private:
std::string _loginfo;
Logger& _logger;
};
};
This code turns logging into a low-friction API that feels similar to std::cout.
Macro wrapping reduces the usage cost even further
#define LOG(level) logger(level, __FILE__, __LINE__)
This macro automatically injects the source file name and line number, so debugging does not depend on developers manually adding context.
This implementation already has an industrial-grade foundation, but it still has room to grow
The current design already covers four essential capabilities: synchronous logging, thread safety, strategy-based extensibility, and standardized formatting. It is a solid foundation for internal infrastructure or for understanding how libraries like glog and spdlog are designed.
If you want to move this design into high-concurrency production systems, prioritize adding an asynchronous queue, log rotation, severity filtering, thread IDs, batched flushing, and crash-safe persistence. In particular, synchronous file writes can easily become a source of tail latency in high-QPS services.
AI Visual Insight: This image shows the final test results. It validates console output, file persistence, and log integrity in multithreaded scenarios, so it can be treated as a result snapshot demonstrating both thread safety and end-to-end functionality.
FAQ
1. Why does the log formatting stage usually not require locking?
Because each thread creates its own temporary LogMessage object, string concatenation happens in thread-local context. The truly shared resources are the console and file handles, so only the flush stage requires mutex protection.
2. Why can’t you use localtime directly?
localtime returns a pointer to a static internal object, which can be overwritten when multiple threads call it concurrently. localtime_r uses a caller-provided buffer, so it is reentrant and much better suited for high-concurrency infrastructure such as a logging system.
3. What is the value of a custom logging system compared with glog or spdlog?
The value of a custom implementation is not in replacing mature libraries. Its real value lies in understanding the underlying mechanisms of a logging system, including RAII, the Strategy pattern, mutex-based synchronization, log format design, and I/O cost. Once you understand these details, diagnosing log disorder or performance issues in production becomes much easier.
Core summary: This article rebuilds an industrial-grade, thread-safe C++ logging system around the Strategy pattern, RAII, reentrant time handling, and a streaming interface design. It supports dual console/file output, matches the glog usage style, and outlines both the key implementation details and future extension directions.