15

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

0

1 Answer 1

5

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.

8
  • Where, in all of this, is my_message written anywhere?
    – Chap
    Commented Mar 16, 2016 at 19:31
  • @Chap, Message_ is an argument to the macro LOG_DEBUG. When you call LOG_DEBUG(my_message) Message_ is substituted with actual paramter - my_message in your case.
    – SergeyA
    Commented Mar 16, 2016 at 19:32
  • Yes, but I don't see where it's actually being outputted. Is that being done by the part that begins "static cast<...>"?
    – Chap
    Commented Mar 16, 2016 at 19:37
  • @Chap, I do not see any static_cast in the LOG macro. I would assume, operator()() for Logger object takes care of actual output.
    – SergeyA
    Commented Mar 16, 2016 at 20:04
  • My apologies, I was referring to the code in the SO article I referenced, which I thought was irrelevant in my case. I have edited my OP so that #define LOG is exactly like the referenced answer.
    – Chap
    Commented Mar 16, 2016 at 20:22

Not the answer you're looking for? Browse other questions tagged or ask your own question.