Sunday 12 May 2013

Logger wrapper revisited

Note: You can find the code for this post and all the following posts here.

Almost a year ago (time really flies), I wrote about wrapping POCO's Logger class. I've ended that post with "As I use it, I'll post here about its merits - and faults."

One of those faults became apparent a few weeks later, as I started using it on my SSH classes. I see those classes as something of a lib (I'm using the widest possible interpretation here, since my code can hardly be qualified as such), i.e., code that a client app can use (hopefully) with little knowledge of what's going on inside. And I wanted to add logging to these classes - sometimes, things go wrong, and logging is a simple first step at getting at the cause (when it works, it's faster then debugging).

While I believe lib code should have logging, it should also present the client app with a simple way of replacing that logging. And that's where my initial design broke. In order to replace the default logging implementation, the client would have to replace my Logger class with his own Logger class (i.e., with the exact same name), and then compile/build. Not an earth-shattering requirement, but it didn't feel particularly clean. I wanted something where the client could add his own implementation to the build without the need to touch the supplied default.

So, I've revisited this subject, and came up with a different design, based on two classes:
  • Logger. Our default logger implementation. Very similar to the one I presented back in June, but simpler. I removed logging levels from this class.
  • LoggerBridge - A template class, it's our logger's interface class, we'll use it as bridge to the implementation class, i.e., our Logger class.

LoggerBridge isn't actually necessary, and the mechanism presented here will work without it. I've included it because it simplifies the task of adding more functionality to the logger, via other template arguments - e.g., we may add a class that generates IDs for each logged operation.
So, what does LoggerBridge look like?

template <typename LoggerImpl>
class LoggerBridge
{
public:
    explicit LoggerBridge(std::string const& loggerName);
 

    static void CreateLogger(std::string const& loggerName,  
        std::string const& configFile);
 

    void Critical(std::string const& msg);
    void Debug(std::string const& msg1);
    void Error(std::string const& msg);
    void Fatal(std::string const& msg);
    void Information(std::string const& msg);
    void Notice(std::string const& msg);
    void Trace(std::string const& msg);
    void Warning(std::string const& msg);
 
private:
    LoggerImpl l;
};

The static CreateLogger() method is called by the client code to configure the logger. You can ignore this and just create a Logger (i.e., create a LoggerBridge instance) and use it, if POCO's Logger defaults are good enough for your needs.

Logger's declaration is similar.

class Logger
{
public:
    explicit Logger(std::string const& loggerName);
 

    static void CreateLogger(std::string const& loggerName, 
        std::string const& configFile);
 

    void Critical(std::string const& msg);
    void Debug(std::string const& msg1);
    void Error(std::string const& msg);
    void Fatal(std::string const& msg);
    void Information(std::string const& msg);
    void Notice(std::string const& msg);
    void Trace(std::string const& msg);
    void Warning(std::string const& msg);
 

private:
    Poco::Logger& m_logger;
};


The third piece of the puzzle is the header file we use in our lib, called lib_logger.h.

#include "logger.h"
#include "loggerbridge.h"
 
typedef Lib::LoggerBridge<Lib::Logger> LibLogger;


This is why LoggerBridge isn't necessary. The typedef could be typedef Lib::Logger LibLogger, and everything would work exactly the same way.

Then, in our lib's code, we use it like this:

#include "lib_logger.h"
...
LibClassMain::~LibClassMain()
{
    LibLogger l("lib");
    l.Debug("LibClassMain dtor");
}


Now, supposing we wanted to use a different log implementation, such as this little gem:

class LoggerCout
{
public:
    explicit LoggerCout(std::string const& /*loggerName*/) {}
 

    static void CreateLogger(std::string const& /*loggerName*/, 
        std::string const& /*configFile*/) {}
 

    void Critical(std::string const& msg) { print("Critical - " + msg); }
    void Debug(std::string const& msg) { print("Debug - " + msg); }
    void Error(std::string const& msg) { print("Error - " + msg); }
    void Fatal(std::string const& msg) { print("Fatal - " + msg); }
    void Information(std::string const& msg) 
        { print("Information - " + msg); }
    void Notice(std::string const& msg) { print("Notice - " + msg); }
    void Trace(std::string const& msg) { print("Trace - " + msg); }
    void Warning(std::string const& msg) { print("Warning - " + msg); }
 
private:
    void print(std::string const& msg) { std::cout << msg << std::endl; }
};


All we require is a change to lib_logger.h:

#include "logger.h"
#include "loggerbridge.h"
 
typedef Lib::LoggerBridge<SomethingElse::LoggerCout> LibLogger;

Note we don't touch the default logger implementation, and we're even putting our alternate logger in a different namespace. This is all transparent to the lib classes, which will happily use our new logger. Just rebuild and give my regards to Uncle Bob.

This solution allows the client to add his own logging implementation in a very simple fashion. However, there's still one thing that needs doing in lib_logger.h, namely, using it to centralize our strategy for including/excluding logging code. If we don't centralize it, we'll have to spread #ifdefs all over our code, and that's definitely not what we want. More on this on a future post, as I'm working on it.

A final note: POCO's Logger bases its poco_debug and poco_trace macros on _DEBUG, i.e., if _DEBUG is not defined (which is usually the case on release builds), these macros are #defined out.

I wouldn't have done it like this, I'd have created my own flag and give my clients the choice of including/excluding whatever code they decided. Why? Speaking as someone with experience on supporting live systems, I've lost count to the times I needed to change the logging level to DEBUG to quickly get a clearer view on an incident's cause.

Still, I like POCO's logger functionality, and I'll use it as my default implementation.

No comments:

Post a Comment