I want to implement C++ logging with the following characteristics:
Things I don't need are:
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:
#ifdef NDEBUG
to keep logging calls from being compiled (although I need to be able to set logging at runtime).__FILE__
and __LINE__
at point where the logger is being called.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:
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.)
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.
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With