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?
1 Answer 1
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.
}
Logger
. 2) I would prefer to be able to use them like a stream rather than callinglog(std::string const&)
. Expected usage:LoggerFactory::get().log(Logger::Critical) << "Having fun: FileDS " << fileDescriptor << " has generated the error( " << strerror(errno) << ")";
\$\endgroup\$std::make_unique
makes this code C++14, not C++11. \$\endgroup\$