vinnylinux
vinnylinux

Reputation: 7024

Elegant approach to error logging in C++11 application?

I'm working on a small C++11 application (an SDL2 game) and i'm having a hard time "porting" some of my object-oriented knowledge from PHP/Java to C++. For example, in order to create an elegant error logging approach, i would create a class with various adapters and centralize logging there. I already did that in C++, but i have no idea on how my classes should be using the Logger class.

In Java and PHP, i would use dependency injection, and put the Logger as a class member variable in them. But in C++, what's the proper way? I don't really think that going static would be nice.

Upvotes: 10

Views: 5282

Answers (4)

Escualo
Escualo

Reputation: 42072

Oh man.

To me logging is similar to date/time handling: the basic case is trivial, but anything more than trivial is extremely complicated: no middle ground.

Let me advise you to look into a general purpose logging library such as Pantheios or Boost.Log.

The reason why I advice for this approach as opposed to making "your own effort", is that I know first hand how the "logging situation" goes:

  • you start with a simple "write to file" or "write to screen"
  • then you need to also log to another device
  • then you want to filter out severity levels
  • then you want to send your logs via pipes
  • then you want to turn off logging

And it all becomes very, very difficult, and the logging classes start polluting your code.

So, like I said: based on my limited experience, I would encourage you to look into the suggested libraries.

Good luck.

Edit: Boost.Log examples

Just for completeness of the post (refer to page for details).

Trivial case:

#include <boost/log/trivial.hpp>
int main(int, char*[]) {
    BOOST_LOG_TRIVIAL(trace) << "A trace severity message";
    BOOST_LOG_TRIVIAL(debug) << "A debug severity message";
    BOOST_LOG_TRIVIAL(info) << "An informational severity message";
    BOOST_LOG_TRIVIAL(warning) << "A warning severity message";
    BOOST_LOG_TRIVIAL(error) << "An error severity message";
    BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message";

    return 0;
}

Upvotes: 7

Matthieu M.
Matthieu M.

Reputation: 299730

My current approach is to use a kind of dependency injection, using C++ strengths instead of magic. It does not require on anything specific to C++11 (except that __thread which is an extension could be replaced by thread_local if you wished to be Standard).

class LoggerEngine {
public:
    static LoggerEngine* Current() { return CurrentE; }

    virtual bool isActive(Level) { return true; }

    virtual void log(char const* function,
                     char const* file,
                     int line,
                     std::string message) = 0;

    // cuz' decorators rock
    LoggerEngine* previous() const { return _previous; }

protected:
    LoggerEngine(): _previous(CurrentE) { CurrentE = this; }
    ~LoggerEngine() { CurrentE = _previous; }

private:
    static __thread LoggerEngine* CurrentE;

    LoggerEngine* const _previous;
}; // class LoggerEngine

// in some .cpp file:
__thread LoggerEngine* LoggerEngine::CurrentE = 0;

And then, provide macros (to capture function, file and line):

#define LOG(L_, Message_)                                                     \
    do { if (LoggerEngine* e = LoggerEngine::Current() and e->isActive(L_)) { \
        std::ostringstream _28974986589657165;                                \
        _28974986589657165 << Message_;                                       \
        e->log(__func__, __FILE__, __LINE__, _28974986589657165.str());       \
    }} while(0);

However it could certainly be made better by using shims instead, because even though it prevents any computation in case the level is not active it still requires formatting of the full message (and the necessary memory allocation) even if it is going to truncate the message anyway (for example because it uses fixed-size buffers) and does not easily allow customization of the formatting.

The combination of stacking engines (and popping them off automatically using RAII) with thread-local behavior is really pretty neat. Most code only ever see an interface, without having to thread it by (cool when you have 4/5 different engines), and any level of the stack can switch the engine to something more appropriate.

There is one caveat, as is, no logging occurs before a first Engine is defined. For this reason I've often thought of defaulting to writing to the console if no engine is setup but... I've mostly changed my style to avoid computation before main is called since I cannot dependency-inject during this phase (and it's awkward if an exception fires...)


Usage is like this:

 void benchmark() {
     LOG(INFO, "Hello, World!");

     Timer t;
     {
         MySinkLogger const _; (void)_; // a logger with "isActive" always false
         for (size_t i = 0; i != 10000; ++i) {
             LOG(INFO, "Flood!");
         }
     }
     LOG(INFO, "Elapsed: " << t.elapsed());
 }

 int main() {
     MyFileLoggerEngine const _("somefile.log"); (void)_; // a file logger

     benchmark();
 }

And normally this could create a file "somefile.log" containing:

2013-10-03T18:38:04.645512 mylaptop INFO <test.cpp#42> Hello, World!
2013-10-03T18:38:04.865765 mylaptop INFO <test.cpp#47> Elapsed: 0.220213s

Upvotes: 0

Wagner Patriota
Wagner Patriota

Reputation: 5674

I always use something like this:

class Log
{
public:
    Log()
        : m_filename( "dafault.log" )
    {}

    // if you wanna give other names eventually...
    Log( const std::string & p_filename )
        : m_filename( p_filename )
    {}

    virtual ~Log()
    {
        // implement  your writeToFile() with std::ofstream 
        writeToFile( m_filename, m_stream, true );
    } 

    template< typename T >
    Log & operator<<( const T & p_value )
    {
        m_stream << p_value;
        return *this;
    }

private:
    std::string         m_filename;
    std::ostringstream  m_stream;
};

So this way I am able to log like this:

Log() << "My message in the log with numbers " << 1 << 2 << 3 << " and so on...";

Log( "other.log" ) << "Log in another file eventually...";

Upvotes: 2

Maxim Egorushkin
Maxim Egorushkin

Reputation: 136208

One approach would be to pass a reference to a logger object around function calls. However, logging is a sort of an orthogonal aspect to application logic, so that explicitly passing that logger and having it as a member quickly becomes a nuisance and only adds artificial complexity.

I prefer having one global logger in the application. Modules can create its own loggers as child loggers of the main logger forming a hierarchy (I think this is similar to Python logging module) and control its output sink and verbosity independently if necessary.

Upvotes: 2

Related Questions