I know that this already has an excepted answer but I will demonstrate the current Logger
that I am using and what it looks like. Mind you though; it inherits from a Singleton
class so that only one Logger
can be constructed per a single application instance. It also relies on a few other classes in which I can not show here.
There was some functionality to make it thread safe in the original design, however I am currently in the process of updating this class from using Windows
headers for CRITICAL_SECTION
and replacing them with mutex
and lock_guard
.
There is also an ExceptionHandler
class where the two work together in an integrated manner, however, no exceptions can be thrown from within the Logger
since the ExceptionHandler
itself uses the Logger
. This way if an exception is thrown and caught; it will write that exception both to the console and the log file.
The declaration of the Logger
class looks like this:
-Logger.h-
#pragma once
#include "Singleton.h"
#include <array>
#include <string>
#include <sstream>
namespace util {
class Logger final : public Singleton {
public:
enum Type {
INFO = 0,
WARNING,
ERROR
};
private:
std::string filename_;
unsigned maxCharLength_;
std::array<std::string, 3> logTypes_;
const std::string unknownLogType_;
// CRICTICAL_SECTION // replace with mutex and lockguard...
public:
explicit Logger(const std::string& filename);
virtual ~Logger();
Logger(const Logger& c) = delete;
Logger& operator=(const Logger& c) = delete;
static void log(const std::string& text, Type type = INFO);
static void log(const std::ostringstream& stream, Type type = INFO);
static void log(const char* text, Type type = INFO);
};
} // namespace util
And it's implementation looks like this:
-Logger.cpp-
#include "Logger.h"
#include "TextFileWriter.h"
#include <conio.h>
#include <iomanip>
#include <iostream>
// include mutex - thread
namespace util {
static Logger* spLogger_ = nullptr;
} // namespace util
using namespace util;
Logger::Logger(const std::string & filename) :
Singleton( LOGGER ),
filename_( filename ),
maxCharLength_( 0 ),
unknownLogType_( "UNKNOWN" ) {
// Order MUST MATCH Types in Logger::Type
logTypes_[0] = "Info";
logTypes_[1] = "Warning";
logTypes_[2] = "Error";
// Find widest log type string
maxCharLength_ = static_cast<unsigned int>( unknownLogType_.size() );
for( const std::string& logType : logTypes_) {
if (maxCharLength_ < logType.size()) {
maxCharLength_ = static_cast<unsigned int>( logType.size() );
}
}
// critical section - mutex - thread lock
// Start Log File
TextFileWriter file(filename, false, false);
spLogger_ = this;
}
Logger::~Logger() {
spLogger_ = nullptr;
// remove critical section or destroy mutex - lockguard, thread etc.
}
void Logger::log(const std::string & text, Type type) {
log(text.c_str(), type);
}
void Logger::log(const std::ostringstream & stream, Type type) {
log(stream.str().c_str(), type);
}
#include <Windows.h>
void Logger::log(const char * text, Type type) {
if (nullptr == spLogger_) {
std::cout << "Logger has not been initialized, can not log " << text << std::endl;
}
// block thread
// Choose Log Type text string, display "UNKNOWN" if Type is out of range.
std::ostringstream stream;
stream << std::setfill(' ') << std::setw(spLogger_->maxCharLength_);
try {
stream << spLogger_->logTypes_.at(type);
} catch (...) {
stream << spLogger_->unknownLogType_;
}
// I am currently in the process of removing Windows specific code:
// I am trying to do something similar to the date & time code below
// but in a more generic, portable and cross - platform way using
// only stand library code.
// Date & Time
SYSTEMTIME time;
GetLocalTime(&time);
stream << " [" << time.wYear << '.'
<< std::setfill('0') << std::setw(2) << time.wMonth << '.'
<< std::setfill('0') << std::setw(2) << time.wDay << ' '
<< std::setfill(' ') << std::setw(2) << time.wHour << ':'
<< std::setfill('0') << std::setw(2) << time.wMinute << ':'
<< std::setfill('0') << std::setw(2) << time.wSecond << '.'
<< std::setfill('0') << std::setw(3) << time.wMilliseconds << "] ";
stream << text << std::endl;
std::cout << stream.str();
// Save message to log file
try {
TextFileWriter file(spLogger_->filename_, true, false);
file.write(stream.str());
} catch (...) {
// Not saved in log file, write message to console
std::cout << __FUNCTION__ << " failed to write to file: " << stream.str() << std::endl;
}
}
A few things to be aware of with this class is that this depends on a TextFileWriter
class which I can not disclose.
I am also in the process of replacing the Date & Time
section to be something more portable and standard instead of using Windows
headers.
If you look closely at the design above, You can see that the Logger
class contains an enum
for basic types of errors. Their severity is already in order: {INFO
, WARNING
, ERROR
}. I can easily expand this class to have different types of errors. The class stores a string
for its filename, simple enough. There is an array
of strings where the size of the array matches the types of errors.
The constructor simply takes a string as a filename. It initializes the names of the error types with a string value. The maxCharLength
is currently used for formatting the output to the console and written text file. I use the TextFileWriter
class to create a file instead of having this responsibility within the Logger
. This way I can have many different things read and write to files through the FileHandler
classes. There is also a static pointer to this class that gets set as the last operation being done within the constructor.
The destructor set's the pointer back to null, and will eventually remove or destroy any mutexes or locks.
There are 3 overloaded log
functions that all get passed to the one that takes a const char*
. They have a second parameter which allows the user or caller to specify the type of error and it has a default value of INFO
just incase you want to quickly write something to the console that isn't an error. The log
function will also write the same message with the same format to a log file.
Also Logger
must be the first thing created because there is a stipulation in its parent class Singleton
that Logger
must be created before any other Singleton
type. It was originally designed this way because there are several Manager
type classes where you only ever want a single Manager
class. So the Logger
, Settings
and various Manager - classes
not shown here other than the Logger
all inherit from Singleton
. Once they are constructed an error will be thrown if you try to construct another per run of application.
Here is what a sample application would look like while using the Logger
class:
-main.cpp-
#include "Logger.h"
#include "ExceptionHandler.h"
int main() {
try {
Logger log( "log.txt" );
Logger::log( "Hello World!" ); // Default Info Type
Logger::log( "Hello World!, Logger::INFO ); // Explicitly Stating Info Type
Logger::log( "Hello World!, Logger::WARNING ); // Warning Type
Logger::log( "Hello World!, Logger::ERROR ); // Error Type
// And since my ExceptionHandler also uses the Logger
ExceptionHandler( "Good Bye Cruel World!" );
} catch( ExceptionHandler& e ) {
std::cout << "Exception Thrown: " << e.getMessage() << std::endl;
return EXIT_FAILURE;
} catch( ... ) {
// Mind you I believe __FUNCTION__ is Windows or Visual Studio specific.
// I've tried using __PRETTY_FUNCTION__ but for my purposes the formatting
// wasn't quite right. You can use your desired choice.
std::cout << __FUNCTION__ << " Caught Unknown Exception" << std::endl;
}
}
My output to the console and text file would look like this from the above main.cpp.
-Output-
Info [2019.02.20 6:54:17.282] Hello World!
Info [2019.02.20 6:54:17.283] Hello World!
Warning [2019.02.20 6:54:17.284] Hello World!
Error [2019.02.20 6:54:17.285] Hello World!
Error [2019.02.20 6:54:17.285] Good Bye Cruel World!
Another thing that was also incorporated but is Window's specific is that the code works with the standard console and it's property settings. When the logger writes to the console depending on what kind of text is being printed, basic cout
will always white text against a black background, but as for the Logger
types, each different type of error has a different set of parameters for the console's text color and background color. This way while debugging an application and some error is generated and or thrown; I can easily tell what type of error I'm reading just by the color of the text and background.
-Disclosure-
I hope that this gives you some insight towards a design of a versatile Logger. Now this code is not 100% mine as I did not fully write this. The overall structure and implementation has come from Marek A. Krzeminski
as the original code is (c)
. You can find the full source material here.
He has given me permission to share parts of his code in situations like this provided that: I do not show fully compliable source code and that I give him credit when I do so. I am free to use his original code for my own personal uses since I have subscribed to his content in that past as long as I don't use any of it for commercial purposes. I do however, like to use parts of his code when applicable when someone needs some advice towards specific algorithms, functionality or just overall code design. I like to take what I have learned from him and to share that learning experience. I am also in the process of redesigning his original class to be my own that is fully generic, portable, cross-platform capable while using modern C++ standards and techniques.