7
\$\begingroup\$

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<std::reference_wrapper<LoggingSink>> Sinks;
    friend class LoggerFactory;
};


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

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

private:
    std::list<std::reference_wrapper<LoggingSink>> 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<Logger> 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?

\$\endgroup\$
  • \$\begingroup\$ Have you looked at Boost.log? \$\endgroup\$ – Guy Sirton Apr 6 '14 at 21:33
  • \$\begingroup\$ Yes, I looked at it. Seems rational to me. The main reason for this draft is to try out something new and try to get a better understanding for factories, the std::reference_wrapper, dependency injection, ... But thank you for the comment! \$\endgroup\$ – Martin Apr 6 '14 at 21:38
  • 2
    \$\begingroup\$ Two points: 1) In this case I think it is preferable for the factory to retain ownership of a log (thus it would return a reference to Logger. 2) I would prefer to be able to use them like a stream rather than calling log(std::string const&). Expected usage: LoggerFactory::get().log(Logger::Critical) << "Having fun: FileDS " << fileDescriptor << " has generated the error( " << strerror(errno) << ")"; \$\endgroup\$ – Martin York Apr 6 '14 at 22:34
  • \$\begingroup\$ Correct me if I'm wrong, but std::make_unique makes this code C++14, not C++11. \$\endgroup\$ – Morwenn Apr 7 '14 at 8:30
  • \$\begingroup\$ @LokiAstari 1) You mean something like a static reference? I'd like to have multiple Loggers, each one with a name. Then I would be able to differentiate between Loggers for subsystems. 2) Yeah, an interface like std::ostream would be nice, that's right! \$\endgroup\$ – Martin Apr 7 '14 at 10:21
3
\$\begingroup\$

As noted in the comments:

I would make the factory retain ownership:

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;
};

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

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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