In any distributed system, logging is an important part as it allows for finding bugs, tracking how services are working and figuring out why things may fail. But the logger itself must be fast, thread-safe, and not block other parts/ add delay to your program. This article will go over different techniques you can use to build a high-performance logger in C++
Why not just use fprintf or cout?
- Not thread-safe: Logs from multiple threads can get mixed up or corrupted. Threads can try to print at the same time, which might result in overlapping of messages.
- Hard to redirect or format: changing logs from files to another format (like JSON) will take a lot of manual work.
Basic logger
A basic logger should always be a Singleton if you want it to be accessed by other parts of your bigger application. This is because we only want one instance to handle all our logs so as not to cause any mess.
Code Snippet
#include<iostream> using namespace std; class Logger { public: static Logger& getInstance() { static Logger instance; return instance; } void log(const string& msg) { cout << "Log: " << msg << endl; } private: Logger() {} }; int main(){ Logger::getInstance().log("Logger was successfully utilised."); return 0; }
Output
Log: Logger was successfully utilised.
Asynchronous logging
This logger has a dedicated thread for logging, making it lightning fast. Combined with a thread-safe implementation, this logging method is a good choice for logging.
Code Snippet
#include <iostream> #include <queue> #include <mutex> #include <thread> #include <condition_variable> #include <string> #include <atomic> using namespace std; class AsynchronousLogger { queue<string> logger; mutex mtx; condition_variable cv; thread worker; atomic<bool> running{true}; public: AsynchronousLogger() { worker = thread([&]() { while (running || !logger.empty()) { unique_lock<mutex> lock(mtx); cv.wait(lock, [&]() { return !logger.empty() || !running; }); while (!logger.empty()) { cout << "Log: " << logger.front() << endl; logger.pop(); } } }); } void log(const string& msg) { { lock_guard<mutex> lock(mtx); logger.push(msg); } cv.notify_one(); } ~AsynchronousLogger() { running = false; cv.notify_one(); if (worker.joinable()){ worker.join(); } } }; int main() { AsynchronousLogger logger; logger.log("Async logger started."); logger.log("Working in background."); logger.log("Logging is non-blocking."); return 0; }
Output
Log: Async logger started. Log: Working in background. Log: Logging is non-blocking.
Explaination
Libraries used:
- iostream For output, general functionality.
- queue: Queue for logging messages.
- mutex: To lock/unlock a thread.
- thread: To use threads.
- condition_variable: This allows a thread to be notified when to log.
- string: To make a string.
- atomic: Thread-safe library for variables.
AsynchronousLogger class:
Variables:
- logger: Our main queue storing our logs.
- mtx: mutex variable that will be used to lock/unlock a thread.
- cv: Conditional variable used to alert threads for changes.
- worker: Name of our main thread used for logging.
- running: An atomic bool type variable set to True.
Constructor:
- The worker thread is initialised and it runs till the ‘running’ variable is false and the queue is empty.
- unique_lock variable ‘lock’ is initialised, acquiring the mtx. This allows ‘lock’ to be locked/unlocked.
- cv.wait(lock, [&]() { return !logger.empty() || !running; }): This unlocks the mtx, puts the current ‘worker’ thread to sleep and performs the function inside before going to sleep. This means is the logger is not empty or the program is stopped, it will lock the mtx and perform further operations. Else, go back to sleep, unlocked.
- If the logger was not empty, it would lock the thread and execute the while loop that prints the logger queue.
- This process repeats until the while loop is closed.
log function:
- lock_guard will lock the mtx, then the message can be safely pushed into the logger queue.
- After this, lock_guard goes out of scope, resulting in the unlocking of mtx.
- cv.notify_one() will notify the cv.wait() that, something has changed. cv.wait() will lock the mtx, then run its internal function and find that the logger is not empty, and it will print logs.
Destructor:
- ‘running’ is set to false. So no more logs will enter the logger queue.
- cv.notify_one() will check if the queue is empty; if not, it will print the remaining logs.
- Then it will wait for the ‘worker’ thread to finish its operation before destroying/deallocating the logger.
Main function:
- Initialises the logger.
- Use the logger three times and exit the program.
Conclusion
By using a Singleton logger, we can ensure proper management across our application. But combine it with Asynchronous design, and it will help you make it more responsive. This combination will result in a highly efficient, robust and scalable logging system.