Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing logging using globals and functors

I want to implement C++ logging with the following characteristics:

  • It must be available to all source code without needing for every function to have an extra parameter (which I presume requires a global)
  • A logging call can specify a severity level (INFO, DEBUG, WARN, etc.) and the logging facility can be set at runtime to ignore calls below a certain severity level
  • The log sink can be set at runtime to the console, or to a file.

Things I don't need are:

  • Support for multiple log sinks at runtime (i.e., it all goes either to the console, or to a file)
  • Support for multithreaded logging
  • Ability to pass cout-style expressions (e.g. "foo=" << foo) in a logger call. I will only pass a std::string.

I found this answer, which appears to address my needs, but it's quite a bit over my head. I think my confusion centers around functors. (I read the Wikipedia article but it clearly hasn't sunk in.)

Here are the parts I do understand:

  • Using a macro (e.g. LOG_DEBUG) to conveniently specify severity level and call the logger.
  • Using #ifdef NDEBUG to keep logging calls from being compiled (although I need to be able to set logging at runtime).
  • The rationale for using a macro to invoke the logger, so that it can automatically and invisibly add info like __FILE__ and __LINE__ at point where the logger is being called.
  • The LOG macro contains an expression beginning with static_cast<std::ostringstream&>. I think this is purely to do with evaluating the cout-style formatting string, which I don't intend to support.

Here's where I'm struggling:

Logger& Debug() {
  static Logger logger(Level::Debug, Console);
  return logger;
}

Reading about operator(), it looks like class Logger is used for creating "functors". Each Logger functor is instantiated (?) with both a level, and a LogSink. (Do you "instantiate" a functor?) LogSink is described as "a backend consuming preformatted messages", but I don't know what that would look like or how it's "written to". At what point is the static Logger object instantiated? What causes it to be instantiated?

These macro definitions...

#define LOG(Logger_, Message_)                   \
  Logger_(                                       \
    static_cast<std::ostringstream&>(            \
       std::ostringstream().flush() << Message_  \
    ).str(),                                     \
    __FUNCTION__,                                \
    __FILE__,                                    \
    __LINE__                                     \
  );

#define LOG_DEBUG(Message_) LOG(Debug(), Message_)

... and this line of code...

LOG_DEBUG(my_message);

... get preprocessed to:

Debug()(my_message, "my_function", "my_file", 42);

What happens when this executes?

How and where is the formatted string actually written to the "log sink"?

(Note: it was suggested that I look at log4cpp - I found it much bigger and harder to understand than what I need, not to mention the political problems I'd have bringing a third-party library into our environment)


UPDATE:

To understand how the above solution works, I tried writing a minimally complete, working program. I intentionally removed the following:

  • the "magic" involving std::ostringstream
  • the #ifdef NDEBUG
  • the Logger Level class enum
  • the LogSink ctor argument (for now I will just write to std::cout)

Here is the complete source file:

#include <iostream>
#include <string>

class Logger {
public:
    Logger(int l);
    void operator()(std::string const& message,
                    char const* function,
                    char const* file,
                    int line);
private:
    int _level;
};

Logger::Logger(int l) :
    _level(l)
{ }

#define LOG(Logger_, Message_)  \
    Logger_(                    \
        Message_,               \
        __FUNCTION__,           \
        __FILE__,               \
        __LINE__                \
    )

#define LOG_DEBUG(Message_)     \
    LOG(                        \
        Debug(),                \
        Message_                \
    )

Logger& Debug() {
    static Logger logger(1);
    return logger;
}

// Use of Logger class begins here

int main(int argc, char** argv) {
    LOG_DEBUG("Hello, world!");
    return 0;
}

When compiled:

$ c++ main.cpp
Undefined symbols for architecture x86_64:
  "Logger::operator()(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, char const*, char const*, int)", referenced from:
      _main in main-c81cf6.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

I see that there's no definition of a function that takes those four arguments and writes them to std::cout, but what is the name of the function I need to define?

(By now I agree that I should be using Boost::Log, but functors are obviously a topic that I don't fully understand.)

like image 736
Chap Avatar asked Mar 16 '16 19:03

Chap


People also ask

Is it OK to use global variables in C?

Non-const global variables are evil because their value can be changed by any function. Using global variables reduces the modularity and flexibility of the program. It is suggested not to use global variables in the program. Instead of using global variables, use local variables in the program.

When should we use global variable?

Global variables should be used when multiple functions need to access the data or write to an object. For example, if you had to pass data or a reference to multiple functions such as a single log file, a connection pool, or a hardware reference that needs to be accessed across the application.

Should you ever use global variables?

Using global variables causes very tight coupling of code. Using global variables causes namespace pollution. This may lead to unnecessarily reassigning a global value. Testing in programs using global variables can be a huge pain as it is difficult to decouple them when testing.

Is it better to use more number of global variables as they can be easily accessed from different proteins?

Always prefer local over global. If you need to pass the data in as multiple parameters, so be it. At least then you're explicitly saying what data your function depends on. Having too many parameters is certainly a problem, but offloading some of them as globals isn't the answer.


1 Answers

The function Debug returns a Logger object (which is created on the first invocation of this function).

This Logger object seems to have operator()() defined for it (judging by the macro definition), which does make it a functor. By the way, functor is nothing special - it's by definiton any type which has operator()() defined for it. However, your analysis doesn't seem correct. Instead,

LOG_DEBUG(my_message);

will be expanded into

LOG(Debug(), Message_)

And that into

Debug()(Message_, __FUNCTION__, __FILE__, __LINE__);

Here Debug() will return an object which have operator()() defined, and this object will be used for the call.

Some QnA

Why doesn't the Logger& Debug() signature specify four arguments?

Because it doesn't need to. Debug() simply returns (static) Logger object created with specific arguments (log level and output device).

At what point is the static Logger object instantiated? What causes it to be instantiated?

When Debug() function is called first time it initializes it's static objects. This is the basic of static function variables.

The last, but not the least. I personally find it not worth the efforts to write own logger. It is tedious and extremely boring unless you really need something special out of it. While I am not really crazy about both Boost.Log and log4cpp, I would (am in fact) certainly use one of them instead of rolling my own logger. Even suboptimal logging is better than spending weeks on own solution.

like image 150
SergeyA Avatar answered Sep 21 '22 15:09

SergeyA