Implementing logging using globals and functors

Viewed 910

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.)

1 Answers
Related