
Prologue
Not one programmer, starting to develop an application, does not pass by the question about logs. It seems to be a simple question, but going through the existing options, you understand that something is inconvenient in everyone: there is no run-time log disconnection (only during compilation), sometimes you need to redirect the log to a file, sometimes to the communication port or somewhere else etc. etc. There is not enough time to write a full-fledged version, but to create a hastily another implementation - the hand does not rise. And it turns out, as they say, a shoemaker without shoes, even worse, because the logs are a development tool ... And what if you come to this question slowly? As a developer, I would like to see the debugging tool like this:
- Easy and easy to use - so that you can include one h file by default in the project and it all works, whether it is an old or new application.
- Expandable - so that by adding one h file to the project, it was possible to increase the functionality as much as you need without affecting the application itself (after all, often the application already runs on the client and is not desirable to touch it).
- Configurable in full - the developer, unlike the user must control the development tool to the fullest extent.
Extensibility
One of the basic principles of
extensibility is to maximize opportunities for change, while minimizing the impact on existing system functions. Firstly, this means that if we want to make the log extensible, we must make it a system, i.e. separate it from the application. Such a mechanism in windows is the dynamic link library: the application does not care which library it works with if the library provides the necessary interface. Those. All library requirements are reduced to interface requirements. Interface extensibility can be achieved using the C ++ interface mechanism (this is the basis of the Component Object Model). To do this, in dll you need to define only two functions:
')
int GetLogInterfaceVersion(); ILog* CreateLogObject();
Where ILog is the required interface and is defined as:
interface ILog { virtual void Log( unsigned int messageId, char *fmt, ... ) = 0; };
If we need to add a new function to our interface:
interface ILog { virtual void Log( unsigned int messageId, char *fmt, ... ) = 0; virtual void RedirectLog( void (*log) (char *)) = 0; };
We just need to consistently add it to the interface and increase the version of the interface, while the old applications will be compatible with the new version of the library. Thus, all the functionality of the logger is hidden behind the abstract interface ILog and what the Log function does is write the data to the file or to the flash memory of the application.
Secondly, if we want to make the log extensible, we must organize its internal structure in such a way that the addition of a new one does not force us to change the already existing functionality within the library. To do this, you need to separate the mutable and immutable functionality.
It is shown that for logs in c ++, this is well done by applying for the immutable part class templates that take strategy parameters as parameters — classes that implement the variable part. Moreover, the strategy can be not only the output strategy to the log (LogPolicy), but also the configuration (LogConfigPolicy), since it can also be parametrized easily:
template < class LogConfigPolicy, class LogPolicy > class TLog : public LogConfigPolicy, public LogPolicy, public ILog { TLog() : LogConfigPolicy(), LogPolicy( this ) // { defaultFilterLevel = LOG_DEBUG; if( GetString("common","filterLevel",out,sizeof(out),"debug") ) // GetString – , , , … };
Thus, you can easily change not only where we output the log, but where the configuration is read from the registry or from the file. The CreateLogObject function will look like this:
ILog* CreateLogObject() { try { #if defined(LOG_REG_CONFIG_POLICY) && defined(LOG_DEBUG_POLICY) return new TLog< LogRegConfigPolicy, LogDebugPolicy >(); #elif defined(LOG_REG_CONFIG_POLICY) && defined(LOG_FILE_POLICY) return new TLog< LogFileConfigPolicy, LogFilePolicy >(); #elif defined(LOG_FILE_CONFIG_POLICY) && defined(LOG_DEBUG_POLICY) return new TLog< LogRegConfigPolicy, LogDebugPolicy >(); #elif defined(LOG_FILE_CONFIG_POLICY) && defined(LOG_FILE_POLICY) return new TLog< LogFileConfigPolicy, LogFilePolicy >(); #else #error Log policies weren't defined #endif } catch(...) { return NULL; } }
Now, by combining the preprocessor definitions, you can get the log.dll log.dll with various properties.
Ease of use
Download dll in the application is not such a difficult task, but if you do this in every project, it can get annoying, and most importantly, in existing applications it is unlikely that someone, using the log, used the library ... Is it possible to do this automatically? Obviously, we need some kind of language tools. The Singleton pattern is the first thing that begs for the implementation of the log (while the dll is loaded in the Singleton constructor). The need to use this pattern is dictated not only by convenience, but also by necessity, because global objects can also access the log, and the order of their creation is not defined. The attempt to implement Singleton as a class template with strategies led me to an interesting fact - instantiation of a static class variable is made in the h file! It turns out that the entire logger, on the application side, can be implemented in just one h file, without adding either cpp or lib:
template <class CreatePolicy, class MainInterface, class NamedMutexObject> class TSingleton: public CreatePolicy { private: static TSingleton *instance; TSingleton():CreatePolicy(){} TSingleton( const TSingleton& ){} TSingleton& operator=( TSingleton& ){} virtual ~TSingleton(){} public: static MainInterface* GetSingletonObject() { if(!instance) { NamedMutexObject mutex( CreatePolicy::GetMutexName() ); if(!instance) instance = new TSingleton(); } return instance->mainInterface; } static void ReleaseSingletonObject() { if(instance) { NamedMutexObject mutex( CreatePolicy::GetMutexName() ); if(instance) { delete instance; instance = NULL; } } } }; template <class CreatePolicy, class MainInterface, class NamedMutexObject> TSingleton< CreatePolicy, MainInterface, NamedMutexObject > * TSingleton< CreatePolicy, MainInterface, NamedMutexObject >::instance = 0;
CreatePolicy is a strategy that, in our case, loads the log.dll library and creates a logger object, while maintaining its interface in mainInterface. The MainInterface interface is ILog, NamedMutexObject is a synchronization object used when creating an instance object. The macro of the error logging will look like this:
#define log_err(fmt,...) TSingleton<LogFromDllPolicy, ILog, CNamedMutexObject>::GetSingletonObject()->Log( LOG_ERROR, fmt, ##__VA_ARGS__)
Thus, the example.cpp application using this logger will look like this:
#include "log.h" int main(int argc, char* argv[]) { log_inf("Some info...\n"); return 0; }
The build command will look like this:
cl.exe example.cpp
ConfigurationThe completeness of the configuration capabilities is provided by the principle of providing the developer with the ability to configure at all levels, be it the compilation level or the run-time.
Compiler settings determine whether a particular piece of library or application code is enabled. As was shown, for the log.dll library (see the implementation of the CreateLogObject function), the settings determine which strategies are used in the template logger class. Thus, by creating the necessary strategies for the logger and combining them with the help of the nastrek compiler, you can implement the necessary functionality in the logger library without altering the existing code.
For an application, the compiler settings are defined in such a way that the programmer does not need to add any settings to the application in order for the log to work:
- LOG_DISABLE - prohibits the output to the log of all types of messages;
- LOG_DISABLE_INFO - disables information type messages;
- LOG_DISABLE_WARNING - disables warning messages,
- LOG_DISABLE_ERROR - prohibits error messages.
At the run-time level, the settings are completely determined by the strategy, which means that the programmer implements. For example, the strategy of configuring a logger from a file implements the configuration of a message filter, which is located in the ini file:
[common]
filterLevel = info
Now that the programmer has been given all the debugging capabilities, and if they are not there, they are easy to add, you can start developing the application itself, because by adding just one h file to the application, you can then easily change the log without changing the application itself.