Initialize a logger once

87 Views Asked by At

I am implementing a library and would like to provide just simple logging facilities without using a logging framework.

I have come to this small piece of code but it does a bit more than i expect :

// logging.h
#ifndef LOGGING_H 
#define LOGGING_H 

#include <iostream>

enum LogLevel {
    // no log is the default
    NoLog       = 0,
    // written to stderr
    Err         = 1,
    Warning     = 2,
    // written to stdout
    Info        = 3,
    Debug       = 4
};

void initLogger(LogLevel level);

LogLevel loggerLevel();

#define LOG(level, msg) \
    do { \
        if (level > NoLog && level <= loggerLevel()) { \
            if (level <= Warning) \
                std::cerr << __FILE__ << "(" << __LINE__ << ") " << msg << std::endl; \
            else \
                std::cout << __FILE__ << "(" << __LINE__ << ") " << msg << std::endl; \
        } \
    } while (0)
        
#define LOG_ERR(msg)    LOG(Err, msg) 
#define LOG_WARN(msg)   LOG(Warning, msg)
#define LOG_INFO(msg)   LOG(Info, msg)
#define LOG_DEBUG(msg)  LOG(Debug, msg)

#endif // LOGGING_H 

and

// logging.cpp
include "logging.h"

static LogLevel logLevel = NoLog;

void initLogger(LogLevel level)
{
    logLevel = level;
}

LogLevel loggerLevel()
{
    return logLevel;
}

Here, the user is able to change the logging level at run-time which i don't want :

initLogger(Err);
    
// not printed
LOG_INFO("info");
  
initLogger(Info);
    
// printed
LOG_INFO("info");

I would prefer to give the user the choice to initialize the logging level once and for all or skip this step completely and fallback to a default level. I have a singleton in mind.

I guess there should be something more straightforward. What would be the best way to achieve that ?

Thank you.

3

There are 3 best solutions below

1
Jarod42 On BEST ANSWER

With singleton, you might do:

static LogLevel loggerLevelInstance(LogLevel init)
{
    static const LogLevel logLevel = init; // Initialized only once

    return logLevel;
}

void initLogger(LogLevel level) { loggerLevelInstance(level); }
LogLevel loggerLevel() { return loggerLevelInstance(LogLevel::NoLog); }

So first call to loggerLevelInstance initialize for once.

0
Pepijn Kramer On

E.g. something like this :

#include <string_view>
#include <iostream>

namespace your_library
{
    struct reporting_itf
    {
        virtual ~reporting_itf() = default;
        virtual void report_method_called(const std::string_view method_name) const noexcept = 0;

        // add more report options here...
        // virtual void report_internal_error() const noexcept = 0;
    };

    struct no_reporting_t : public reporting_itf
    {
        void report_method_called(const std::string_view method_name) const noexcept override {};

        //void report_internal_error() const noexcept override {};
    };

    no_reporting_t no_reporting;
    const reporting_itf* g_reporting = &no_reporting;

    void set_reporting(const reporting_itf& reporting)
    {
        g_reporting = &reporting;
    }

    void some_function()
    {
        g_reporting->report_method_called(__FUNCTION__);
    }

}


namespace client_code
{
    struct client_logger_t : public your_library::reporting_itf
    {
        void report_method_called(const std::string_view method_name) const noexcept override
        {
            std::cout << "called : " << method_name << "\n";
            // should be something like LOG(Informational....)
        };

        // Internal logger classes
        // Logger m_log;
    };
}


int main()
{
    client_code::client_logger_t client_logger; 
    your_library::set_reporting(client_logger);

    your_library::some_function();
}
2
Jerry Coffin On

Here's a version using a template and if constexpr expressions, so a logger can only be initialized with a specific level one time, and logging to a level that's disabled should result in no code being generated for that call at all.

// logging.h
#ifndef LOGGING_H 
#define LOGGING_H 

#include <iostream>

enum LogLevel {
    // no log is the default
    NoLog       = 0,
    // written to stderr
    Err         = 1,
    Warning     = 2,
    // written to stdout
    Info        = 3,
    Debug       = 4
};

template <int currentLevel, int logLevel>
class Logger {
public:
    static void write(char const *file, int line, char const *msg) {
        if constexpr (logLevel > currentLevel)
            return;

        if constexpr (logLevel <= Warning)
            std::cerr << file << "(" << line << ") " << msg << std::endl;
        else
            std::cout << file << "(" << line << ") " << msg << std::endl;
    }    
};


#define initLogging(level)                  \
    using ErrLog = Logger<level, Err>;      \
    using WarnLog = Logger<level, Warning>; \
    using InfoLog = Logger<level, Info>;    \
    using DebugLog = Logger<level, Debug>;

#define LOG_ERR(msg)    ErrLog::write(__FILE__, __LINE__, msg) 
#define LOG_WARN(msg)   WarnLog::write(__FILE__, __LINE__, msg)
#define LOG_INFO(msg)   InfoLog::write(__FILE__, __LINE__, msg)
#define LOG_DEBUG(msg)  DebugLog::write(__FILE__, __LINE__, msg)

#endif // LOGGING_H 

Here's a quick demo:


#include "logging.hpp"

initLogging(Warning);

int main() {
    LOG_ERR("Error Message");
    LOG_WARN("Warning Message");
    LOG_INFO("Info Message");
    LOG_DEBUG("Debug Message");
}

At least as the code currently stands, the use of initLogging is required, not optional (and as shown, it should normally be at global scope, not inside a function).

Doing a quick check on Godbolt, it appears that calls to disabled logging levels are optimized out completely (at least in a simple situation like the test program):

https://godbolt.org/z/G57E5xhs6

One point though: just like almost anything else the Logger objects do observe scope. As such, you can create different levels of logging in different scopes if you really want to, and the objects in each scope will observe the level they were initialized to in that scope. For example:

#include "logging.hpp"

initLogging(Warning);

void breaklevel() { 

    // in this scope, logging will happen at debug level:
    initLogging(Debug);

    // so *all* of these will print:
    LOG_ERR("Error Message");
    LOG_WARN("Warning Message");
    LOG_INFO("Info Message");
    LOG_DEBUG("Debug Message");
}

int main() {

    // but these use the global logger, so only the error and
    // warning messages will print:

    LOG_ERR("Error Message");
    LOG_WARN("Warning Message");
    LOG_INFO("Info Message");
    LOG_DEBUG("Debug Message");
    breaklevel();
}

Each logger is only initialized once, but since they observe scope, each scope can have its own logger objects...