1

After a long time I started doing some C++ development again. Right now I'm struggling with my logging class. It's already working quite nicely, but I want to introduce some log levels. To be honest, I'm not quite sure how to continue in the right way.

My logger works with five simple macros:

#define PLUGINLOG_INIT(path, fileName) logInstance.initialise(path, fileName);

#define LOG_ERROR (logInstance << logInstance.prefix(error))
#define LOG_WARN  (logInstance << logInstance.prefix(warn))
#define LOG_INFO  (logInstance << logInstance.prefix(info))
#define LOG_DEBUG (logInstance << logInstance.prefix(debug))

The first one opens the file stream and the other four write log entries into the file. It's a rather simple approach. The prefix methods writes a datetime stamp and the log level as text:

2021-05.26 12:07:23 WARN File not found!

For the log level, I created an enum and store the current log level in my class. My questions is however, how can I avoid logging if the log level is set lower?

Example: LogLevel in class = warn

When I log an info entry, I'd put the following line into my source code:

LOG_INFO << "My info log entry" << std::endl;

Since the LogLevel is set to warning, this info entry should not be logged. I could try putting an if statement into the LOG_INFO macro, but I would rather avoid complicated macros. Is there a better way to achieve what I need?

Many thanks, Marco

Complete header file of my logger:

#ifndef PLUGINLOGGER_H_
#define PLUGINLOGGER_H_

// Standard
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include <string_view>


// Forward declarations
class PluginLogger;
extern PluginLogger logInstance;


// Enumerations
enum LogLevel {
    error = 0,
    warn  = 1,
    info  = 2,
    debug = 3
};


// Macros
#define PLUGINLOG_INIT(path, fileName) logInstance.initialise(path, fileName);

#define LOG_ERROR (logInstance << logInstance.prefix(error))
#define LOG_WARN  (logInstance << logInstance.prefix(warn))
#define LOG_INFO  (logInstance << logInstance.prefix(info))
#define LOG_DEBUG (logInstance << logInstance.prefix(debug))


class PluginLogger {
public:
    PluginLogger();

    void initialise(std::string_view path, std::string_view fileName);
    void close();

    template<typename T> PluginLogger& operator<<(T t);

    // to enable std::endl
    PluginLogger& operator<<(std::ostream& (*fun) (std::ostream&));

    std::string prefix(const LogLevel logLevel);

private:
    std::string m_fileName;

    std::ofstream m_stream;

    LogLevel m_logLevel;
};


template<typename T> inline PluginLogger& PluginLogger::operator<<(T t) {
    if (m_stream.is_open())
        m_stream << t;
        
    return* this;
}


inline PluginLogger& PluginLogger::operator<<(std::ostream& (*fun)( std::ostream&)) {
    if (m_stream.is_open())
        m_stream << std::endl;
    
    return* this;
}

#endif // PLUGINLOGGER_H_

Complete source file of my logger:

#include <chrono>

#include "PluginLogger.h"

PluginLogger logInstance;


PluginLogger::PluginLogger() {
    m_fileName = "";
    m_logLevel = error;
}


void PluginLogger::initialise(std::string_view path, std::string_view fileName) {
    if (!path.empty() && !fileName.empty()) {
        m_fileName = std::string(path) + std::string(fileName) + "_log.txt";
        m_stream.open(m_fileName);
        
        unsigned char bom[] = { 0xEF,0xBB,0xBF };
        m_stream.write((char*)bom, sizeof(bom));
    }
}


void PluginLogger::close() {
    if (m_stream.is_open())
        m_stream.close();
}


std::string PluginLogger::prefix(const LogLevel logLevel) {
    // add a date time string
    std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());

    std::string dateTimeStr(25, '\0');
    std::strftime(&dateTimeStr[0], dateTimeStr.size(), "%Y-%m-%d %H:%M:%S", std::localtime(&now));

    // add log level
    std::string logLevelText;
    
    switch (logLevel) {
        case error:
            logLevelText = "    ERROR    ";
            break;

        case warn:
            logLevelText = "    WARN     ";
            break;

        case info:
            logLevelText = "    INFO     ";
            break;

        case debug:
            logLevelText = "    DEBUG    ";
            break;
    }

    return dateTimeStr + logLevelText;
}
Marco
  • 11
  • 1
  • 3
  • instead of generating the prefix as string and pass that to `operator<<` you can use an enum that you pass to `operator<<` and put the `if` inside `operator<<`. Btw this question is purely opinion based. "I know a solution but I dont like it" does not have one correct answer. – 463035818_is_not_an_ai May 26 '21 at 14:16
  • `std::ofstream m_stream;` -- I suggest not to do this. The first reason is that `std::ofstream` is not copyable and should not be a class member. The second reason is that you forced yourself to log to a file stream -- what if you want to log to another stream type? – PaulMcKenzie May 26 '21 at 14:20
  • I'm not a big fan of `operator<<` here, because it's hard (impossible?) to make this "threadsafe", meaning you don't get mixed log lines from several threads logging concurrently. You already have a singleton, so a static `PluginLogger::Log(Level level, const std::string& fmt, ...)` seems better for me – Stefan Riedel May 26 '21 at 14:23
  • You could create a `NullLogger` whose `operator<<` does nothing. Then conditionally (re)define `LOG_INFO` to write to a `NullLogger` instead of the `PluginLogger` and let the compiler optimize it out. – Thomas May 26 '21 at 14:24
  • @StefanRiedel It's possible to make this thread-safe; see for example how Qt logging works. The macro creates a temporary object whose `operator<<` writes to an internal buffer, and that object's destructor flushes the buffered text to the actual log. – Thomas May 26 '21 at 14:25
  • I was thinking about using a static method instead, but the `operator<<` seemed to be easier to work with, as you can just use all kinds of variables with it. – Marco May 26 '21 at 14:28
  • 1
    @Thomas good to know, I still like printf stlye logging more, but I guess that's just personal preference – Stefan Riedel May 26 '21 at 14:33
  • I'm having a very similar question, not yet had a solution other than the ugly multiple #if statements ... What makes that more terrible is the differentiated sources of debugging as in this [question](https://stackoverflow.com/questions/73600818/indentation-level-management-in-c-logger-class). Does one have a better answer here? – Hamza Hajeir Nov 19 '22 at 18:55

0 Answers0