This is a pet project done as an exercise in C++.
The following are currently out of the scope of my project but I am considering them. If you deem it important don't hesitate to comment on it.
- Multiple output streams corresponding to log levels for safety and better control over output.
- Thread safety. Logging is currenty not thread safe.
- Tee stream that allows logging the same logs to multiple streams at once. Should provide additional safety and useful functionality.
- Signal handle flush. Flushing in the case of program crash.
I have just started using doxygen so I'm leaving the comments in.
Logger.h
#include <iostream>
#include <string>
#include <functional>
/// logLevels define logging priority - trace being lowest and off being highest - off enables us to turn off logging easily to save on performance.
/// a log cannot be made with a logLevel off - see LOG(level).
enum class logLevel
{
trace = 0,
debug,
info,
warn,
error,
off
};
/// \brief Handles stream input and linebreaking.
///
/// Intended use is as a temporary object. This is important because line breaking is done on object destruction, to be precise - parameter endlineStrategy is executed in the destructor.
/// Input to the ostream is done using the operator <<. The operator forwards the input to the std::ostream, so all standard overloads are available.
class StreamDelegate
{
public:
StreamDelegate(std::ostream& os, std::function<void(std::ostream&)> endlineStrategy /**<executed on object destruction, it should handle line breaking*/);
~StreamDelegate();
StreamDelegate(const StreamDelegate&) = delete;
StreamDelegate& operator=(const StreamDelegate&) = delete;
StreamDelegate(StreamDelegate&&) = default;
StreamDelegate& operator=(StreamDelegate&&) = default;
template<class T>
inline StreamDelegate& operator<<(T&& output)
{
ostream << std::forward<T>(output);
return *this;
}
private:
std::function<void(std::ostream&)> endline;
std::ostream& ostream;
};
class Logger
{
public:
///\brief instantiates a global Logger object
///
///For now it is not possible to change globalLogLevel and output stream dynamically.
/// \todo dynamic setting of Loggers members globalLogLevel and logStream
static Logger& instance(logLevel globalLogLevel = logLevel::off, std::ostream& output = std::cout);
~Logger();
Logger(Logger const&) = delete;
void operator=(Logger const&) = delete;
///\brief this is it
StreamDelegate log(logLevel level);
logLevel getGlobalLogLevel();
protected:
void timeStamp();
void logLevelStamp(logLevel level);
std::function<void(std::ostream&)> endlineStrategy(logLevel level);
std::string logLevelString(logLevel level);
private:
Logger();
Logger(logLevel globalLogLevel, std::ostream& output);
std::ostream& logStream;
const logLevel globalLogLevel;
};
/// \brief Use to initialize the Logger.
///
/// Once set globalLogLevel and output can't be changed.
#define LOGGER_INIT(globalLogLevel, output) Logger::instance(globalLogLevel, output);
/// \brief Call for a Log of a certain level. Should be used only after initialization.
///
/// if the check passes a call logging is enabled and input is done with the operator <<.
/// If the check fails code Logger::instance.log() isn't called and the following operator << calls arent made.
#define LOG(level) if(Logger::instance().getGlobalLogLevel() <= (level) && (level) < logLevel::off) Logger::instance().log(level)
I have read it might be wise to avoid macros. Macro LOG(level) shows a performance benefit over other solutions known to me. To avoid the use of Macros on log priority check fail a dummy stream would have to be created, and sent as an argument to StreamDelegate, so that following operator << calls aren't bad syntax.
Logger.cpp
#include "Logger.h"
#include <iomanip>
//PUBLIC LOGGER
Logger & Logger::instance(logLevel globalLogLevel, std::ostream& output)
{
static Logger instance(globalLogLevel, output);
return instance;
}
Logger::~Logger()
{
}
StreamDelegate Logger::log(logLevel level)
{
timeStamp();
logLevelStamp(level);
return StreamDelegate(this->logStream, this->endlineStrategy(level));
}
logLevel Logger::getGlobalLogLevel()
{
return this->globalLogLevel;
}
//PROTECTED LOGGER
void Logger::logLevelStamp(logLevel level)
{
// magic number - should I avoid it and how
this->logStream << std::setw(7) << logLevelString(level) + ": ";
}
void Logger::timeStamp()
{
this->logStream << __TIME__ << " ";
}
/// \todo avoid switch statement repetition
std::string Logger::logLevelString(logLevel level)
{
//strategy?
switch (level)
{
case logLevel::trace: return "TRACE";
case logLevel::debug: return "DEBUG";
case logLevel::info: return "INFO";
case logLevel::warn: return "WARN";
case logLevel::error: return "ERROR";
case logLevel::off: break;
default: break;
}
}
/// \todo avoid switch statement repetition
std::function<void(std::ostream&)> Logger::endlineStrategy(logLevel level)
{
std::function<void(std::ostream&)> endlineNoFlush = [](std::ostream& os) {os << "\n"; };
std::function<void(std::ostream&)> endlineFlush = [](std::ostream& os) {os << std::endl; };
switch (level)
{
case logLevel::trace: return endlineNoFlush;
case logLevel::debug: return endlineNoFlush;
case logLevel::info: return endlineNoFlush;
case logLevel::warn: return endlineFlush;
case logLevel::error: return endlineFlush;
case logLevel::off: break;
default: break;
}
}
//PRIVATE LOGGER
Logger::Logger(logLevel globalLogLevel, std::ostream& output)
: globalLogLevel(globalLogLevel), logStream(output)
{
}
//PUBLIC STREAM DELEGATE
StreamDelegate::StreamDelegate(std::ostream & os, std::function<void(std::ostream&)> endline)
:
ostream(os), endline(endline)
{
}
StreamDelegate::~StreamDelegate()
{
endline(ostream);
}
I have the same switch case repeating in two methods - Logger::logLevelString and Logger::endlineStrategy. It seems small and I don't see much room for it to grow and cause readability issues. For the sake of good code, what would be a good way of avoiding this?
To demonstrate the use:
main.cpp
#include "Logger.h"
int main()
{
LOGGER_INIT(logLevel::debug, std::cout);
LOG(logLevel::trace) << "Dummy line. " << "No input.";
LOG(logLevel::info) << "First line.";
LOG(logLevel::warn) << "Second: first input. " << "Second line: second input.";
std::cin.get();
}