Simple code based logging in C++

Simple code based logging in C++

Code logging

Update 24th of June 2021

I made the logging thread safe. It now uses a mutex which is locked when the logger class is called. And the mutex is released when the destructor is called. Since the usage of this logging class is by macros this is acceptable. Each time you call a macro, for example LOG_INFO(). It creates a instance of the logger and when the expression ends the instance is deleted. So the lock/release will happen.

If you want something else, please feel, free to change and send me a comment <3

Description

Centralized logging and logging code behaviour are two different things. Quite often you just want your code and modules to output its behaviour related things to certain output stream and see what your code is doing.

In my projects, I've been using a simple logging class display just that, to print out my code behaviour to standard output (a.g. cerr or cout).

Requirements and design

I wanted the usage to be simple. Mainly meaning I wanted to use the stream << operator of C++ to be able to output logging, just like by using any output streams like std::cout. Secondly, I wanted each logging output to have a time stamp. The last requirement for me was that I wanted it to be easy to remove/strip all the logging if I wanted so, for example in release builds.

Log output should be doable like this: DebugLogger(DebugLogLevel::INFO) << "this is a log line number " << lineNumber;

Implementation

The constructor initializes a std::ostringstream where the whole one line of log output formed into and it appends the initial features, time stamp and the severity level of the event.

            DebugLogger(DebugLogLevel severity = DebugLogLevel::ERROR, bool showtime = true) 
                : m_buffer(), m_stm{}
            {                
                if (showtime) {
                    make_time();
                    m_buffer << "[" << std::put_time(&m_stm, "%F %T") << "] ";
                }

                switch(severity) {
                    case DebugLogLevel::WARN:
                    {
                        m_buffer << KTextLevelWarn;
                        break;
                    }
                case DebugLogLevel::ERROR:
                    {
                        m_buffer << KTextLevelError;
                        break;
                    }
                    default:
                    {
                        m_buffer << KTextLevelInfo;
                        break;
                    }
                }
                m_buffer << ": ";
            }

Finally, in the end of life of the DebugLogger instance, the descructor of the causes the output stream to be flushed to the standard output of our choice.

            virtual ~DebugLogger() {
                std::cerr << m_buffer.str() << std::endl << std::flush;
            }

With the above code, the DebugLogger class, if used by just calling the constructor with DebugLogger(); prints out the time stamp and a default logging level of ERROR.

[2021-05-29 12:54:26] ERROR:

So the constructor, and the desctructor are immendiately called causing the above to be printed out to standard output.

To be able output anything I want, with the stream << operator, I need to implement it like this.

            template <typename T>
            DebugLogger& operator<<(const T& value) {
                m_buffer << value;
                return *this;
            }

It's a templated operator<< method which can accept anything as an input. So now if I use it like this

auto the_answer = 42;
DebugLogger() << "The answer to life, universe and everything is " << the_answer;

I get the following output:

[2021-05-29 12:54:26] ERROR: The answer to life, universe and everything is 42

What happens here is that first the constructor of the logger gets called initializing the stream with initial values, the multiple stream << operators append to the stream and finally at ; the DebugLogger class instance gets destroyed and the desctructor flushes the output to the standard output.

Finally, the last requirement I had was the ability to strip log output if I wanted so. I don't want to go through the code and remove/comment each logging line by hand. Instead I will define a macro (or multiple macros) that I can use. For example:

#define LOG_INFO() DebugLogger(DebugLogLevel::INFO)
#define LOG_WARN() DebugLogger(DebugLogLevel::WARN)
#define LOG_ERROR() DebugLogger(DebugLogLevel::ERROR)

When I want to stip the logging off, I can just define these macros to something that does not output anything.

Full code here

#include <iostream>
#include <iomanip>
#include <sstream>
#include <chrono>
#include <ctime>

namespace codesmith
{
    namespace Debug
    {
        /**
         * Defines the supported warning levels for the macro.
         * The level defines a warning level text displayed by the macro
         */
        enum class DebugLogLevel : int
        {
            INFO = 0,
            WARN = 1,
            ERROR = 2
        };

        /**
         * DebugLogger class definition
         * Class has stream output operation with operator <<
         * Constructor creates the initial output with (or without) a time stamp
         * and the desired warning level (severity).
         * 
         * Default warning level is ERROR
         * 
         * When class instance is destructed
         */
        class DebugLogger
        {
        private: // constants
            static constexpr const char* KTextLevelInfo = "INFO";
            static constexpr const char* KTextLevelWarn = "WARN";
            static constexpr const char* KTextLevelError = "ERROR";

        public:
            DebugLogger(DebugLogLevel severity = DebugLogLevel::ERROR, bool showtime = true) 
                : m_buffer(), m_stm{}
            {                
                if (showtime) {
                    make_time();
                    m_buffer << "[" << std::put_time(&m_stm, "%F %T") << "] ";
                }

                switch(severity) {
                    case DebugLogLevel::WARN:
                    {
                        m_buffer << KTextLevelWarn;
                        break;
                    }
                case DebugLogLevel::ERROR:
                    {
                        m_buffer << KTextLevelError;
                        break;
                    }
                    default:
                    {
                        m_buffer << KTextLevelInfo;
                        break;
                    }
                }
                m_buffer << ": ";
            }

            // Destructor, causes the debug info to be outputted with new line
            virtual ~DebugLogger() {
                std::cerr << m_buffer.str() << std::endl;
            }

            /**
             * Stream output operator, appends a value to the debug output stream
             */
            template <typename T>
            DebugLogger& operator<<(const T& value) {
                m_buffer << value;
                return *this;
            }
        private:
            void make_time() {
                std::time_t t_now = std::chrono::system_clock::to_time_t(
                    std::chrono::system_clock::now());
                localtime_s(&m_stm, &t_now);
            }

        private:
            std::ostringstream m_buffer;
            struct std::tm m_stm;
        };
    }
}

using namespace codesmith::Debug;

/**
 * Helper macros/defines for using the DebugLogger
 */

// Default logger, severity level is ERROR, with a time stamp
#define LOG() DebugLogger()

// Default logger, severity level is ERROR, without a time stamp
#define LOG_NT() DebugLogger()

// These variants show system time
#define LOG_INFO() DebugLogger(DebugLogLevel::INFO)
#define LOG_WARN() DebugLogger(DebugLogLevel::WARN)
#define LOG_ERROR() DebugLogger(DebugLogLevel::ERROR)

// These variants omit the system time and only show the warning level
#define LOG_INFO_NT() DebugLogger(DebugLogLevel::INFO, false)
#define LOG_WARN_NT() DebugLogger(DebugLogLevel::WARN, false)
#define LOG_ERROR_NT() DebugLogger(DebugLogLevel::ERROR, false)


#endif // DEBUGLOGGER_DEFINED_H

Final toughts

I hope you find this helpful and for the least give you some new insights on how to accomplish the task in hand.

The code

The whole code is available in my GitHub with some examples. Debuglogger @ GitHub