670 lines
17 KiB
MQL5
670 lines
17 KiB
MQL5
#ifndef __LOG4MQL__
|
|
#define __LOG4MQL__
|
|
|
|
#property copyright "GPL2 Written by Michael Schmidt (m@swayp.net)"
|
|
#property link "https://swayp.net"
|
|
#property strict
|
|
|
|
/**
|
|
* Log4mql
|
|
*
|
|
* Log4mql is a mql4 (MetaQuotes MetaTrader 4 Language) library for flexible
|
|
* logging to files and the terminal console. It is modeled after the Log4j
|
|
* Java library.
|
|
*
|
|
* Usage in your code:
|
|
*
|
|
* CLog4mql::getInstance().error(__FILE__, __LINE__,
|
|
* "Something unexpected happen");
|
|
*
|
|
* or (for more frequent usage)
|
|
*
|
|
* CLog4mql* logger = CLog4mql::getInstance();
|
|
* logger.error(__FILE__, __LINE__, "Something unexpected happen");
|
|
* logger.info(__FILE__, __LINE__, "Calcumation done");
|
|
* logger.debug(__FILE__, __LINE__, StringFormat("The result of %s is %d",
|
|
* string1, value1));
|
|
*
|
|
* Dont forget at the end of your EA / Indicator / Script:
|
|
* CLog4mql::release();
|
|
* or
|
|
* logger.release();
|
|
*
|
|
* You can configure the required log level for output in the configfile
|
|
* For each appending file and / or a global default.
|
|
*
|
|
* Log Levels:
|
|
* TRACE
|
|
* DEBUG
|
|
* INFO
|
|
* WARN
|
|
* ERROR
|
|
* CRIT
|
|
*/
|
|
|
|
/**
|
|
* Log levels
|
|
*/
|
|
#define LOG4MQL_LEVEL_TRACE 6
|
|
#define LOG4MQL_LEVEL_DEBUG 5
|
|
#define LOG4MQL_LEVEL_INFO 4
|
|
#define LOG4MQL_LEVEL_WARN 3
|
|
#define LOG4MQL_LEVEL_ERROR 2
|
|
#define LOG4MQL_LEVEL_CRIT 1
|
|
|
|
/**
|
|
* Configuation file
|
|
*/
|
|
#define LOG4MQL_CONFFILE "log4mql\\log4mql.conf"
|
|
|
|
/**
|
|
* Default configuration
|
|
*/
|
|
#define LOG4MQL_DEFAULT_LOGFILE "log4mql\\log4mql.log"
|
|
#define LOG4MQL_DEFAULT_LEVEL 4
|
|
#define LOG4MQL_DEFAULT_LOG_TO_CONSOLE_WHEN_IN_TESTING true
|
|
#define LOG4MQL_DEFAULT_ROTATE_ENABLE true
|
|
#define LOG4MQL_DEFAULT_ROTATE_CHECK_PERIOD 1000
|
|
#define LOG4MQL_DEFAULT_ROTATE_SIZE_MB 1
|
|
#define LOG4MQL_DEFAULT_ROTATE_GENERATIONS 8
|
|
|
|
#ifdef __MQL5__
|
|
bool IsOptimization(void)
|
|
{
|
|
return( MQLInfoInteger(MQL_OPTIMIZATION) == 1 );
|
|
}
|
|
|
|
bool IsTesting(void)
|
|
{
|
|
return( MQLInfoInteger(MQL_TESTER)==1 );
|
|
}
|
|
#endif //__MQL5__
|
|
/**
|
|
* Class CLog4mql
|
|
*
|
|
* Dynamic logger designed to provide log4j style logging to mql4
|
|
*/
|
|
class CLog4mql {
|
|
private:
|
|
static CLog4mql *instance;
|
|
string confLogFile;
|
|
int confDefaultLevel;
|
|
bool confLogToConsoleWhenInTesting;
|
|
bool confRotateEnabled;
|
|
int confRotateCheckPeriod;
|
|
ulong confRotateSize;
|
|
int confRotateGenerations;
|
|
int logFileHandle;
|
|
int configs;
|
|
string configFile[100];
|
|
int configLevel[100];
|
|
int actionsLastRotate;
|
|
|
|
protected:
|
|
/**
|
|
* Constructor
|
|
*/
|
|
CLog4mql() {
|
|
confLogFile = LOG4MQL_DEFAULT_LOGFILE;
|
|
confDefaultLevel = LOG4MQL_DEFAULT_LEVEL;
|
|
confLogToConsoleWhenInTesting = LOG4MQL_DEFAULT_LOG_TO_CONSOLE_WHEN_IN_TESTING;
|
|
confRotateEnabled = LOG4MQL_DEFAULT_ROTATE_ENABLE;
|
|
confRotateCheckPeriod = LOG4MQL_DEFAULT_ROTATE_CHECK_PERIOD;
|
|
confRotateSize = LOG4MQL_DEFAULT_ROTATE_SIZE_MB * 1024 * 1024;
|
|
confRotateGenerations = LOG4MQL_DEFAULT_ROTATE_GENERATIONS;
|
|
logFileHandle = INVALID_HANDLE;
|
|
configs = 0;
|
|
loadConfig();
|
|
actionsLastRotate = confRotateCheckPeriod;
|
|
}
|
|
|
|
/**
|
|
* Destructor
|
|
*/
|
|
~CLog4mql() {
|
|
closeLogFile();
|
|
}
|
|
|
|
private:
|
|
/**
|
|
* Prevent copy of singleton instance
|
|
*/
|
|
CLog4mql(const CLog4mql&);
|
|
|
|
public:
|
|
/**
|
|
* GetInstance
|
|
*
|
|
* Get singleton instance
|
|
*/
|
|
static CLog4mql * getInstance() {
|
|
if(instance == NULL) {
|
|
instance = new CLog4mql();
|
|
instance.debug(__FILE__, __LINE__, "Instance created");
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
/**
|
|
* Release
|
|
*
|
|
* Release singleton instance
|
|
*/
|
|
static void release() {
|
|
if(instance != NULL) {
|
|
instance.debug(__FILE__, __LINE__, "Instance released");
|
|
delete instance;
|
|
}
|
|
instance = NULL;
|
|
}
|
|
|
|
public:
|
|
/**
|
|
* Trace
|
|
*
|
|
* Log trace message
|
|
*
|
|
* @var string file
|
|
* Origin file name which sends the log message (__FILE__)
|
|
* @var int line
|
|
* Line of log method call in origin file (__LINE__)
|
|
* @var string msg
|
|
* Message to log
|
|
* @return void
|
|
*/
|
|
void trace(string file, int line, string msg) {
|
|
log(LOG4MQL_LEVEL_TRACE, file, line, msg);
|
|
}
|
|
|
|
/**
|
|
* Debug
|
|
*
|
|
* Log debug message
|
|
*
|
|
* @var string file
|
|
* Origin file name which sends the log message (__FILE__)
|
|
* @var int line
|
|
* Line of log method call in origin file (__LINE__)
|
|
* @var string msg
|
|
* Message to log
|
|
* @return void
|
|
*/
|
|
void debug(string file, int line, string msg) {
|
|
log(LOG4MQL_LEVEL_DEBUG, file, line, msg);
|
|
}
|
|
|
|
/**
|
|
* Info
|
|
*
|
|
* Log info message
|
|
*
|
|
* @var string file
|
|
* Origin file name which sends the log message (__FILE__)
|
|
* @var int line
|
|
* Line of log method call in origin file (__LINE__)
|
|
* @var string msg
|
|
* Message to log
|
|
* @return void
|
|
*/
|
|
void info(string file, int line, string msg) {
|
|
log(LOG4MQL_LEVEL_INFO, file, line, msg);
|
|
}
|
|
|
|
/**
|
|
* Warn
|
|
*
|
|
* Log warn message
|
|
*
|
|
* @var string file
|
|
* Origin file name which sends the log message (__FILE__)
|
|
* @var int line
|
|
* Line of log method call in origin file (__LINE__)
|
|
* @var string msg
|
|
* Message to log
|
|
* @return void
|
|
*/
|
|
void warn(string file, int line, string msg) {
|
|
log(LOG4MQL_LEVEL_WARN, file, line, msg);
|
|
}
|
|
|
|
/**
|
|
* Error
|
|
*
|
|
* Log error message
|
|
*
|
|
* @var string file
|
|
* Origin file name which sends the log message (__FILE__)
|
|
* @var int line
|
|
* Line of log method call in origin file (__LINE__)
|
|
* @var string msg
|
|
* Message to log
|
|
* @return void
|
|
*/
|
|
void error(string file, int line, string msg) {
|
|
log(LOG4MQL_LEVEL_ERROR, file, line, msg);
|
|
}
|
|
|
|
/**
|
|
* Crit
|
|
*
|
|
* Log critical message
|
|
*
|
|
* @var string file
|
|
* Origin file name which sends the log message (__FILE__)
|
|
* @var int line
|
|
* Line of log method call in origin file (__LINE__)
|
|
* @var string msg
|
|
* Message to log
|
|
* @return void
|
|
*/
|
|
void crit(string file, int line, string msg) {
|
|
log(LOG4MQL_LEVEL_CRIT, file, line, msg);
|
|
}
|
|
|
|
private:
|
|
/**
|
|
* Log
|
|
*
|
|
* Create a log message (console / log file)
|
|
* Called by trace, debug, info, warn, error and crit methods
|
|
*
|
|
* Different targets per run mode
|
|
*
|
|
* File Console
|
|
* Normal x x
|
|
* Testing x -
|
|
* Optimization - -
|
|
*
|
|
* @var int level
|
|
* Level of log message
|
|
* @var string file
|
|
* Origin file name which sends the log message (__FILE__)
|
|
* @var int line
|
|
* Line of log method call in origin file (__LINE__)
|
|
* @var string msg
|
|
* Message to log
|
|
* @return void
|
|
*/
|
|
void log(int level, string file, int line, string msg) {
|
|
if(IsOptimization()) {
|
|
return;
|
|
}
|
|
if(!IsTesting() || confLogToConsoleWhenInTesting) {
|
|
logToConsole(level, file, line, msg);
|
|
}
|
|
logToFile(level, file, line, msg);
|
|
}
|
|
|
|
/**
|
|
* LogToConsole
|
|
*
|
|
* Log a message to MetaTrader console
|
|
*
|
|
* @var int level
|
|
* Level of log message
|
|
* @var string file
|
|
* Origin file name which sends the log message (__FILE__)
|
|
* @var int line
|
|
* Line of log method call in origin file (__LINE__)
|
|
* @var string msg
|
|
* Message to log
|
|
* @return void
|
|
*/
|
|
void logToConsole(int level, string file, int line, string msg) {
|
|
if(level <= getFileLevel(file)) {
|
|
PrintFormat("%-8s [%-20s:%-3d] %s", levelToString(level), file, line, msg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* LogToFile
|
|
*
|
|
* Log a message to log file
|
|
*
|
|
* @var int level
|
|
* Level of log message
|
|
* @var string file
|
|
* Origin file name which sends the log message (__FILE__)
|
|
* @var int line
|
|
* Line of log method call in origin file (__LINE__)
|
|
* @var string msg
|
|
* Message to log
|
|
* @return void
|
|
*/
|
|
void logToFile(int level, string file, int line, string msg) {
|
|
if(confLogFile == "off") {
|
|
return;
|
|
}
|
|
openLogFile();
|
|
if(logFileHandle == INVALID_HANDLE) {
|
|
return;
|
|
}
|
|
if(level <= getFileLevel(file)) {
|
|
int bytes = (int) FileWriteString(logFileHandle, StringFormat("%s %-8s [%-20s:%-3d] %s\n", getDate(),
|
|
levelToString(level), file, line, msg));
|
|
if(bytes <= 0) {
|
|
Print("LOG4MQL ERROR: Failed to write to logfile " + confLogFile);
|
|
closeLogFile();
|
|
}
|
|
FileFlush(logFileHandle);
|
|
actionsLastRotate++;
|
|
if(confRotateEnabled && actionsLastRotate >= confRotateCheckPeriod) {
|
|
actionsLastRotate = 0;
|
|
if(FileSize(logFileHandle) >= confRotateSize) {
|
|
rotate();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GetDate
|
|
*
|
|
* Generate date string for log file message
|
|
*
|
|
* @return string
|
|
* Date in format YYYY-MM-DD hh:mm:ss
|
|
*/
|
|
string getDate() {
|
|
datetime t = TimeLocal();
|
|
//return StringFormat("%04d-%02d-%02d %02d:%02d:%02d", TimeYear(t), TimeMonth(t), TimeDay(t), TimeHour(t),
|
|
// TimeMinute(t), TimeSeconds(t));
|
|
return TimeToString(t, TIME_DATE|TIME_SECONDS);
|
|
}
|
|
|
|
/**
|
|
* OpenLogFile
|
|
*
|
|
* @return void
|
|
*/
|
|
void openLogFile() {
|
|
if(logFileHandle != INVALID_HANDLE) {
|
|
return;
|
|
}
|
|
logFileHandle = FileOpen(confLogFile, FILE_WRITE|FILE_SHARE_WRITE|FILE_READ|FILE_SHARE_READ|FILE_TXT);
|
|
if(logFileHandle == INVALID_HANDLE) {
|
|
Print("LOG4MQL ERROR: Failed to open logfile " + confLogFile);
|
|
return;
|
|
}
|
|
FileSeek(logFileHandle, 0, SEEK_END);
|
|
}
|
|
|
|
/**
|
|
* CloseLogFile
|
|
*
|
|
* @return void
|
|
*/
|
|
void closeLogFile() {
|
|
if(logFileHandle != INVALID_HANDLE) {
|
|
FileClose(logFileHandle);
|
|
logFileHandle = INVALID_HANDLE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rotate
|
|
*
|
|
* Rotate logfiles / delete old rotated log files
|
|
*
|
|
* Example for 4 Generations:
|
|
* log4mql.log -> log4mql.log.1
|
|
* log4mql.log.1 -> log4mql.log.2
|
|
* log4mql.log.2 -> log4mql.log.3
|
|
* log4mql.log.3 -> log4mql.log.4
|
|
* log4mql.log.4 -> delete
|
|
*
|
|
* @return void
|
|
*/
|
|
void rotate() {
|
|
if(!confRotateEnabled) {
|
|
return;
|
|
}
|
|
if(!FileIsExist(confLogFile)) {
|
|
return;
|
|
}
|
|
info(__FILE__, __LINE__, "Rotate");
|
|
closeLogFile();
|
|
string delFile = confLogFile + "." + IntegerToString(confRotateGenerations);
|
|
if(FileIsExist(delFile)) {
|
|
info(__FILE__, __LINE__, " delete %s" + delFile);
|
|
FileDelete(delFile);
|
|
}
|
|
for(int i = confRotateGenerations - 1; i >= 0; i--) {
|
|
string fileFrom;
|
|
if(i > 0) {
|
|
fileFrom = confLogFile + "." + IntegerToString(i);
|
|
} else {
|
|
fileFrom = confLogFile;
|
|
}
|
|
if(!FileIsExist(fileFrom)) {
|
|
continue;
|
|
}
|
|
string fileTo = confLogFile + "." + IntegerToString(i + 1);
|
|
info(__FILE__, __LINE__, StringFormat(" %-20s => %s", fileFrom, fileTo));
|
|
FileMove(fileFrom, 0, fileTo, 0);
|
|
}
|
|
FileDelete(confLogFile);
|
|
openLogFile();
|
|
}
|
|
|
|
/**
|
|
* LoadConfig
|
|
*
|
|
* Read log4mql configuration from
|
|
* Normal mode: <MetaTrader DataDir>/Files/log4mql.conf
|
|
* Test mode: <MetaTrader DataDir>/tester/files/log4mql.conf
|
|
*
|
|
* @return void
|
|
*/
|
|
void loadConfig() {
|
|
if(!FileIsExist(LOG4MQL_CONFFILE)) {
|
|
warn(__FILE__, __LINE__, "No configuration file " + LOG4MQL_CONFFILE + " found");
|
|
return;
|
|
}
|
|
int confFileHandle = FileOpen(LOG4MQL_CONFFILE, FILE_READ);
|
|
if(confFileHandle == INVALID_HANDLE) {
|
|
warn(__FILE__, __LINE__, "Failed to read configuration file " + LOG4MQL_CONFFILE);
|
|
return;
|
|
}
|
|
while(!FileIsEnding(confFileHandle)) {
|
|
string line = FileReadString(confFileHandle);
|
|
StringReplace(line, " ", "");
|
|
StringReplace(line, "\t", "");
|
|
string key = getConfigEntry(line, 0);
|
|
string value = getConfigEntry(line, 1);
|
|
if(key == "" || value == "") {
|
|
continue;
|
|
}
|
|
if(key == "defaultLevel") {
|
|
confDefaultLevel = stringToLevel(value);
|
|
} else if(key == "logfile") {
|
|
confLogFile = value;
|
|
debug(__FILE__, __LINE__, StringFormat("Config %-20s = %s", key, value));
|
|
} else if (key == "logToConsoleWhenInTesting") {
|
|
confLogToConsoleWhenInTesting = "true" == value;
|
|
debug(__FILE__, __LINE__, StringFormat("Config %-20s = %s", key, (confLogToConsoleWhenInTesting ? "ENABLED" : "DISABLED")));
|
|
} else if(key == "rotateEnable") {
|
|
if(value == "true") {
|
|
confRotateEnabled = true;
|
|
debug(__FILE__, __LINE__, StringFormat("Config %-20s = %s", key, "ENABLED"));
|
|
} else {
|
|
confRotateEnabled = false;
|
|
debug(__FILE__, __LINE__, StringFormat("Config %-20s = %s", key, "DISABLED"));
|
|
}
|
|
} else if(key == "rotateCheckPeriod") {
|
|
confRotateCheckPeriod = (int)StringToInteger(value);
|
|
debug(__FILE__, __LINE__, StringFormat("Config %-20s = %s", key, value));
|
|
} else if(key == "rotateSizeMb") {
|
|
confRotateSize = (ulong) StringToInteger(value) * 1024 * 1024;
|
|
debug(__FILE__, __LINE__, StringFormat("Config %-20s = %s", key, value));
|
|
} else if(key == "rotateGenerations") {
|
|
confRotateGenerations = (int)StringToInteger(value);
|
|
debug(__FILE__, __LINE__, StringFormat("Config %-20s = %s", key, value));
|
|
} else {
|
|
int level = stringToLevel(value);
|
|
StringToUpper(key);
|
|
StringToUpper(value);
|
|
configFile[configs] = key;
|
|
configLevel[configs] = level;
|
|
configs++;
|
|
debug(__FILE__, __LINE__, StringFormat("Config file %-15s = %s", key, levelToString(level)));
|
|
}
|
|
}
|
|
FileClose(confFileHandle);
|
|
}
|
|
|
|
/**
|
|
* GetConfigEntry
|
|
*
|
|
* Parse a config line
|
|
* Cut comments and whitspaces
|
|
*
|
|
* Example
|
|
* Log line: foo = bar # Comment
|
|
* Key (col 0) is 'foo'
|
|
* Value (col 1) is 'bar'
|
|
*
|
|
* @var string line
|
|
* Line from configuration file
|
|
* @var int col
|
|
* 1 = key
|
|
* 2 = value
|
|
* @return string
|
|
* Key or value
|
|
* Empty string if
|
|
* Parse failed
|
|
* Line empty
|
|
* Comment line
|
|
*/
|
|
string getConfigEntry(string line, int col) {
|
|
if(StringFind(line, "=") == -1) {
|
|
return "";
|
|
}
|
|
int comment = StringFind(line, "#");
|
|
if(comment == 0) {
|
|
return "";
|
|
} else if(comment > 0) {
|
|
line = StringSubstr(line, 0, comment - 1);
|
|
}
|
|
string spl[];
|
|
StringSplit(line, StringGetCharacter("=", 0), spl);
|
|
string str = spl[col];
|
|
StringTrimLeft(str);
|
|
StringTrimRight(str);
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
* GetFileLevel
|
|
*
|
|
* Get loglevel for origin file of log message from configuration / default
|
|
*
|
|
* @var string file
|
|
* Origin file of log message
|
|
* @return int
|
|
* Log level
|
|
*/
|
|
int getFileLevel(const string file) {
|
|
string f = file;
|
|
StringReplace(f, ".mqh", "");
|
|
StringReplace(f, ".mq4", "");
|
|
StringToUpper(f);
|
|
for(int i = 0; i < configs; i++) {
|
|
if(configFile[i] == f) {
|
|
return configLevel[i];
|
|
}
|
|
}
|
|
return confDefaultLevel;
|
|
}
|
|
|
|
/**
|
|
* LevelToString
|
|
*
|
|
* Convert level integer to string
|
|
*
|
|
* @var int l
|
|
* Level to convert
|
|
* @return string
|
|
* Log level string
|
|
*/
|
|
string levelToString(int l) {
|
|
switch(l) {
|
|
case LOG4MQL_LEVEL_TRACE:
|
|
return "TRACE";
|
|
case LOG4MQL_LEVEL_DEBUG:
|
|
return "DEBUG";
|
|
case LOG4MQL_LEVEL_INFO:
|
|
return "INFO";
|
|
case LOG4MQL_LEVEL_WARN:
|
|
return "WARNING";
|
|
case LOG4MQL_LEVEL_ERROR:
|
|
return "ERROR";
|
|
case LOG4MQL_LEVEL_CRIT:
|
|
return "CRITICAL";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* StringToLevel
|
|
*
|
|
* Convert string to level integer
|
|
*
|
|
* @var string s
|
|
* Level string to convert
|
|
* @return int
|
|
* Log level
|
|
*/
|
|
int stringToLevel(string str) {
|
|
StringToUpper(str);
|
|
if(str == "TRACE") {
|
|
return LOG4MQL_LEVEL_TRACE;
|
|
} else if(str == "DEBUG") {
|
|
return LOG4MQL_LEVEL_DEBUG;
|
|
} else if(str == "WARN" || str == "WARNING") {
|
|
return LOG4MQL_LEVEL_WARN;
|
|
} else if(str == "ERROR") {
|
|
return LOG4MQL_LEVEL_ERROR;
|
|
} else if(str == "CRIT" || str == "CRITICAL") {
|
|
return LOG4MQL_LEVEL_CRIT;
|
|
} else {
|
|
return confDefaultLevel;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Initialize singleon
|
|
*/
|
|
CLog4mql * CLog4mql::instance = NULL;
|
|
|
|
#ifndef FOR_OPTIMIZATION
|
|
//--- For Log4MQL
|
|
#define _TRACE(msg) if(CheckPointer(log4) != POINTER_INVALID) log4.trace(__FILE__, __LINE__, msg)
|
|
#define _DEBUG(msg) if(CheckPointer(log4) != POINTER_INVALID) log4.debug(__FILE__, __LINE__, msg)
|
|
#define _INFO(msg) if(CheckPointer(log4) != POINTER_INVALID) log4.info(__FILE__, __LINE__, msg)
|
|
#define _WARN(msg) if(CheckPointer(log4) != POINTER_INVALID) log4.warn(__FILE__, __LINE__, msg)
|
|
#define _ERROR(msg) if(CheckPointer(log4) != POINTER_INVALID) log4.error(__FILE__, __LINE__, msg)
|
|
#define _CRIT(msg) if(CheckPointer(log4) != POINTER_INVALID) log4.crit(__FILE__, __LINE__, msg)
|
|
#define _B2S(B) ((B)?"True":"False")
|
|
#define _CHECK_TRACE(name,cache, val) if(cache != val) \
|
|
{ _IN5(""); \
|
|
cache = val; \
|
|
string msg = name+" changed to "+_B2S(val); \
|
|
_TRACE(msg); \
|
|
}
|
|
//---
|
|
#else
|
|
|
|
#define _TRACE(msg) ;
|
|
#define _DEBUG(msg) ;
|
|
#define _INFO(msg) ;
|
|
#define _WARN(msg) ;
|
|
#define _ERROR(msg) ;
|
|
#define _CRIT(msg) ;
|
|
#define _B2S(B) ((B)?"True":"False")
|
|
#define _CHECK_TRACE(name,cache, val) ;
|
|
|
|
#endif //FOR_OPTIMIZATION
|
|
#endif /* __LOG4MQL__ */
|