HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

C++11 Logging Architecture

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
loggingarchitecturestackoverflow

Problem

To create a Logging system for a larger application, I came up with the following draft. The log sinks aren't detailed yet, but the factory method for logger creation and a rough draft of the logger itself are. Is this a good start and worth continuing? Or are there pitfalls you can see?

class LoggingSink; // abstract base class
class ConsoleSink : public LoggingSink; // some concrete logging sink
class LogfileSink : public LoggingSink; // ... too

/***********************************************/
class Logger {
public:
    void log(const std::string& msg) {
        for (LoggingSink ls : Sinks)
            ls.log(msg);
    }

private:
    std::list> Sinks;
    friend class LoggerFactory;
};

/***********************************************/
class LoggerFactory {
public:
    std::unique_ptr create() {
        std::unique_ptr result = std::make_unique();
        result->Sinks.assign(StandardAssignedSinks.begin(), StandardAssignedSinks.end());
        return result;
    }

    void addStandardSink(LoggingSink& ls) {
        Sinks.push_back(std::reference_wrapper(ls));
    }

private:
    std::list> StandardAssignedSinks;
};

/***********************************************/
int main() {
    ConsoleSink cs;
    LogfileSink lfs("mylogfile.txt");

    LoggerFactory lf;
    lf.addStandardSink(cs);  // adding as reference for polymorphism
    lf.addStandardSink(lfs); // perhaps the addition of some standard sinks should be done in the constructor of LoggerFactory???

    std::unique_ptr myLogger = lf.create();

    myLogger->log("Hello Log, I'm going to be injected soon!");
    MyClass myClassInstance(myLogger); // constructor dependency injection

    MyClass myOtherInstance(lf.create()); // direct DI
}


I'm also unsure about the friend class. This is something I never really liked with factories. Any ideas how to improve here?

Solution

As noted in the comments:

I would make the factory retain ownership:

class LoggerFactory {

typedef std::map>  Container;
typedef Container::iterator                             iterator;
typedef Container::const_iterator                       const_iterator;
typedef Container::value_type                           value_type;

typedef std::list>  SinkHolder;

public:
    Logger& get(std::string const& loggerName = "default")
    {
        iterator find = cont.find(loggerName);
        if (find == cont.end())
        {

            std::unique_ptr result = std::make_unique();

            // Personally I would not do this.
            // I would pass the iterators to the constructor
            // of the Logger object.
            result->Sinks.assign(std::begin(standardAssignedSinks),
                                 std::end(standardAssignedSinks));

            auto insertR = cont.insert(value_type(loggerName, result));
            if (!insertR.second)
            {    throw std::runtime_error("Insertion Failed");
            }
            find = insertR.first;           
        }
        return *(find->second);
    }

    void addStandardSink(LoggingSink& ls) {
        standardAssignedSinks.empace_back(ls);
    }

private:
    SinkHolder standardAssignedSinks;
    Container  cont;
};


Now you can have multiple loggers. But you can re-use loggers (as each logger has its own name. Most of the time people should be using the default logger.

Building a streamming logger is not that difficult it just requires an intermediate buffer object to accumulate the message. When it is destroyed it sends the message to the logger.

class Logger {
    public:
        template
        LogStreamBuffer  operator
        LogStreamBuffer&  operator<<(T const& data)
        {
            // Anything you log is just appended to the message.
            buffer << data;
            return *this;   // return a reference to yourself.
                            // So you can chain messages.
        }
};

// Usage:
int main()
{
     LoggerFactory   factory;
     Logger          logger = factory.get("MyLogger");

     logger << "This is a test: " << 1 << " Got it";
       //   ^^^
       //   Creates a LogStreamBuffer
       //   As an invisable temporary object.
       //   The subsequent `operator<<` will accumulate
       //   data in the buffer. When the statement is
       //   finished (at the ;) all temporaies will be
       //   destroyed. This calls the LogStreamBuffer
       //   destructor and logs your data to the Logger
       //   object. 

}

Code Snippets

class LoggerFactory {

typedef std::map<std::string, std::unique_ptr<Logger>>  Container;
typedef Container::iterator                             iterator;
typedef Container::const_iterator                       const_iterator;
typedef Container::value_type                           value_type;

typedef std::list<std::reference_wrapper<LoggingSink>>  SinkHolder;

public:
    Logger& get(std::string const& loggerName = "default")
    {
        iterator find = cont.find(loggerName);
        if (find == cont.end())
        {

            std::unique_ptr<Logger> result = std::make_unique<Logger>();

            // Personally I would not do this.
            // I would pass the iterators to the constructor
            // of the Logger object.
            result->Sinks.assign(std::begin(standardAssignedSinks),
                                 std::end(standardAssignedSinks));

            auto insertR = cont.insert(value_type(loggerName, result));
            if (!insertR.second)
            {    throw std::runtime_error("Insertion Failed");
            }
            find = insertR.first;           
        }
        return *(find->second);
    }

    void addStandardSink(LoggingSink& ls) {
        standardAssignedSinks.empace_back(ls);
    }

private:
    SinkHolder standardAssignedSinks;
    Container  cont;
};
class Logger {
    public:
        template<typename T>
        LogStreamBuffer  operator<<(T const& data) // Member so left hand side  implied as Logger
        {
            // This is not perfect
            // This may cause a copy (and thus a destruction)
            // If there is a copy/destruction cycle an extra message will be
            // sent. You can code around this problem.
            // This is just to get you started.

            return LogStreamBuffer(*this) << data;
        }
    // OTHER STUFF
};
class LogStreamBuffer
{
    Logger&            parent;
    std::stringstream  buffer;

    public:
        LogStreamBuffer(Logger& parent)
           : parent()
        {}
        ~LogStreamBuffer()
        {
            // When the object is destroyed.
            // Log the accumulated message with the parent.
            parent.log(buffer.str());
        }
        template<typename T>
        LogStreamBuffer&  operator<<(T const& data)
        {
            // Anything you log is just appended to the message.
            buffer << data;
            return *this;   // return a reference to yourself.
                            // So you can chain messages.
        }
};

// Usage:
int main()
{
     LoggerFactory   factory;
     Logger          logger = factory.get("MyLogger");


     logger << "This is a test: " << 1 << " Got it";
       //   ^^^
       //   Creates a LogStreamBuffer
       //   As an invisable temporary object.
       //   The subsequent `operator<<` will accumulate
       //   data in the buffer. When the statement is
       //   finished (at the ;) all temporaies will be
       //   destroyed. This calls the LogStreamBuffer
       //   destructor and logs your data to the Logger
       //   object. 

}

Context

StackExchange Code Review Q#46499, answer score: 3

Revisions (0)

No revisions yet.