Русский 中文 Español Deutsch 日本語 Português
Error Handling and Logging in MQL5

Error Handling and Logging in MQL5

MetaTrader 5Examples | 17 November 2015, 14:08
14 123 2
Sergey Eremin
Sergey Eremin

Introduction

During the operation of most programs, errors may occasionally occur. Their adequate processing is one of the important aspects of a high-quality and sustainable software. This article will cover main methods of error handling, recommendations for their use, as well as logging via MQL5 tools.

Error handling is a relatively difficult and controversial subject. There are many ways of error handling, and each of them has certain advantages and disadvantages. Many of these methods can be used together, but there is no universal formula — each specific task requires an adequate approach.


Basic methods of error handling

If a program encounters errors during its operation, then usually for its proper functioning it should perform some action (or several actions). The following examples of such actions are provided:

Stop a program. If there are any errors, the most appropriate action would be to stop the running program. Normally these are critical errors that disable program operation, because it becomes either pointless or simply dangerous. MQL5 provides a mechanism of interruption for time execution errors: for example, in the case of "a division by zero" or "an array out of range", the program ceases its operation. In other cases of termination the programmer must take care of them himself. For example, for Expert Advisors the ExpertRemove() function should be utilized:

Example of stopping an Expert Advisor with the ExpertRemove() function:

void OnTick()
  {
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      Alert("fail");
      ExpertRemove();
      return;
     }
  }


Convert incorrect values to the range of correct values. A certain value frequently has to fall within the specified range. However, in some cases values ​​outside of this range may appear. Then it is possible to have a value's forced return to the acceptable boundary. A calculation of the open position volume can be used as an example. If the resulting volume is outside the minimum and maximum available values, it can be forced to return within these borders:

Example of converting incorrect values to the correct value range

double VolumeCalculation()
  {
   double result=...;

   result=MathMax(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN));
   result=MathMin(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX));

   return result;
  }

However, if for some reason a volume turned out higher than the maximum border, and the deposit is unable to sustain such load, then it is advisable to log and to abort the program execution. Fairly frequently this particular error is threatening for an account.


Return an error value. In this case if an error occurs, then a certain method or function must return a predetermined value that will signal an error. For example, if our method or function has to return a string, then NULL may be returned in the case of an error.

Example of an error value return:

#define SOME_STR_FUNCTION_FAIL_RESULT (NULL)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      return SOME_STR_FUNCTION_FAIL_RESULT;
     }
   return result;
  }

void OnTick()
  {
   string someStr=SomeStrFunction();

   if(someStr==SOME_STR_FUNCTION_FAIL_RESULT)
     {
      Print("fail");
      return;
     }
  }

Nevertheless, such approach can lead to programming mistakes. If this action is not documented, or if a programmer doesn't familiarize himself with a document or a code implementation, then he will not be aware of the possible error value. Moreover, problems may occur, if a function or a method can return almost any value in the normal mode of operation, including the one with an error.


Assign the execution result to a special global variable. Frequently this approach is applied for methods and functions that do not return any values. The idea is that the result of this method or function is assigned to a certain global variable, and then the value of this variable is checked in the calling code. For this purpose there is a default function (SetUserError()) in MQL5.

Example of assigning an error code with SetUserError()

#define SOME_STR_FUNCTION_FAIL_CODE (123)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE);
      return "";
     }
   return result;
  }

void OnTick()
  {
   ResetLastError();
   string someStr=SomeStrFunction();

   if(GetLastError()==ERR_USER_ERROR_FIRST+SOME_STR_FUNCTION_FAIL_CODE)
     {
      Print("fail");
      return;
     }
  }

In this case a programmer may be unaware of the possible errors, however, this approach allows to inform not only about an error, but also to indicate its specific code. This is particular important, if there are several sources of error.


Return the execution result as a bool and the resulting value as a variable passed by a reference. Such approach is slightly better than the previous two, as it leads to a lower probability of programming mistakes. It's difficult not to notice, that a method or a function may be unable to operate properly:

Example of returning a function operation result as a bool

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail");
      return;
     }
  }

This and the previously mentioned option can be combined, if there are several different errors, and we need to identify the exact one. A false can be returned, and a global variable have an error code assigned.

Example of returning a function result as a bool and assigning an error code with SetUserError()

#define SOME_STR_FUNCTION_FAIL_CODE_1 (123)
#define SOME_STR_FUNCTION_FAIL_CODE_2 (124)

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";

      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_1);

      return false;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_2);
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail, code = "+(string)(GetLastError()-ERR_USER_ERROR_FIRST));
      return;
     }
  }

However, this option is difficult for interpretation (when reading the code) and further support.


Return the result as a value from the enumeration (enum), and the resulting value (if any) as a variable passed by a reference. If there are several types of possible errors, in case of failure this option allows to return a specific error type without the use of global variables. Only one value will correspond to the correct execution, and the rest will be considered as error.

Example of returning a function operation result as an enumeration value (enum)

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

ENUM_SOME_STR_FUNCTION_RESULT SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_1;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_2;
     }

   value=resultValue;
   return SOME_STR_FUNCTION_SUCCES;
  }

void OnTick()
  {
   string someStr="";

   ENUM_SOME_STR_FUNCTION_RESULT result=SomeStrFunction(someStr);

   if(result!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result));
      return;
     }
  }

Eliminating global variables is a very important advantage of this approach, as incompetent or negligent handling may cause serious issues.


Return a result as a structure instance consisting of a Boolean variable or an enumeration value (enum) and a resulting value. This option is related to the previous method which eliminates the necessity for passing variables by a reference. The use of enum is preferable here, as it will allow to extend the list of possible execution results in the future.

Example of returning a function operation result as a structure instance consisting of enumeration values​ (enum) and resulting values

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

struct SomeStrFunctionResult
  {
   ENUM_SOME_STR_FUNCTION_RESULT code;
   char              value[255];
  };

SomeStrFunctionResult SomeStrFunction()
  {
   SomeStrFunctionResult result;

   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_1;
      return result;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_2;
      return result;
     }

   result.code=SOME_STR_FUNCTION_SUCCES;
   StringToCharArray(resultValue,result.value);
   return result;
  }

void OnTick()
  {
   SomeStrFunctionResult result=SomeStrFunction();

   if(result.code!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result.code));
      return;
     }
   string someStr=CharArrayToString(result.value);
  }


Attempt an execution of operation few times. It is frequently worth attempting an operation several times before considering it unsuccessful. For example, if a file can't be read as it is used by another process, then several attempts with increasing time intervals should be made. There is a high possibility that another process will free the file, and our method or function will be able to refer to it.

Example of making several attempts to open a file

string fileName="test.txt";
int fileHandle=INVALID_HANDLE;

for(int iTry=0; iTry<=10; iTry++)
  {
   fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);

   if(fileHandle!=INVALID_HANDLE)
     {
      break;
     }
   Sleep(iTry*200);
  }

Note: The example above shows only the essence of this approach, a practical application requires the occurring errors to be analyzed. If, for example, an error 5002 (Invalid file name) or 5003 (The file name is too long) occurs, then there is no point to make any further attempts. Furthermore, it must be considered that such approach shouldn't be applied in the systems where any overall performance slowdown is undesirable.


Notify a user explicitly. Users have to be explicitly notified about certain errors (via pop-up window, chart label etc). Explicit notifications can be frequently used in combination with suspending or stopping a program completely. For example, if an account has insufficient funds, or a user has entered incorrect values of input parameters, then he should clearly be notified about that.

Example of notifying a user about invalid input parameters

input uint MAFastPeriod = 10;
input uint MASlowPeriod = 200;

int OnInit()
  {
//---
   if(MAFastPeriod>=MASlowPeriod)
     {
      Alert("A period of fast moving average has to be less than a period of slow moving average!");
      return INIT_PARAMETERS_INCORRECT;
     }
//---
   return(INIT_SUCCEEDED);
  }

There are certainly other methods of error handling, the list provided only reveals the ones that are most commonly used.


General recommendations for error handling

Choose an adequate level of error handling. Completely different error handling requirements are applied to different programs. It is OK to skip error handling in a small script that will be used only a few times for checking insignificant ideas and won't be shared with a third party. On the contrary, it would be appropriate to process all possible errors, if a project involves hundreds and thousands of potential users. Always try to have a good understanding of the error processing level required in each specific case.

Choose an adequate user interaction level. Explicit user interaction is required only for certain errors: the program can proceed operating on its own without any user notifications. It is important to find a middle way: users shouldn't get bombarded with error warnings or, on the contrary, get zero notifications in critical situations. The following approach may offer a good solution - users should be explicitly notified for any critical errors or situations that require involvement, and for all other cases log files should be kept.

Verify results of all functions and methods that return them. If any function or method can return values, among which there are those indicating errors, then it is best to check them. The opportunity for program's quality improvement shouldn't be neglected.

Check the conditions before performing certain operations, if possible. For example, check the following before attempting to open a trade:

  1. Are trading robots allowed on the terminal: TerminalInfoInteger(TERMINAL_TRADE_ALLOWED).
  2. Are trading robots allowed on the account: AccountInfoInteger(ACCOUNT_TRADE_EXPERT).
  3. Is there a connection to a trading server: TerminalInfoInteger(TERMINAL_CONNECTED).
  4. Are the parameters of a trading operation correct: OrderCheck().

Follow an adequate performance of various program parts. A common code example is a trailing stop loss that doesn't take into account the frequency of queries to the trading server. This function's calling is normally implemented at every tick. If there is a continuous one-way movement, or some errors occur when attempting to modify transactions, then this function can send trade modification requests almost at every tick (or multiple requests for multiple trades).

When quotes are not frequently received, this feature will not cause any problems. Otherwise, there may be serious issues — extremely frequent trade modification requests may cause a broker's disconnection from the automated trading for a particular account and unpleasant conversations with customer support. The easiest solution is to limit the frequency of attempts for implementing trade modification requests: remember the time of a previous request and don't attempt to perform it again, unless 20 seconds have passed.

Example of performing a trailing stop loss function less than every 30 seconds

const int TRAILING_STOP_LOSS_SECONDS_INTERVAL=30;

void TrailingStopLoss()
  {
   static datetime prevModificationTime=0;

   if((int)TimeCurrent() -(int)prevModificationTime<=TRAILING_STOP_LOSS_SECONDS_INTERVAL)
     {
      return;
     }

//--- Stop Loss modification
     {
      ...
      ...
      ...
      prevModificationTime=TimeCurrent();
     }
  }

The same problem may occur, when you attempt to place too many pending orders within a short period of time, as already experienced by the author.


Aim for an adequate relation of stability and correctness. A compromise between code's stability and correctness should be determined when writing a program. Stability implies that the program will continue working with errors, even if it leads to slightly inaccurate results. Correctness doesn't allow returning inaccurate results or performing wrong actions. They have to be either accurate, or to be absent completely, which means it is better to stop the program than to return inaccurate results or do something else wrong.

For example, if an indicator can't calculate something, it's better to have no signal than shut down completely. On the contrary, for a trading robot it is best to stop its work than to open a trade with an excessive volume. Furthermore, a trading robot can notify users with a PUSH notification before stopping, allowing users to learn about issues and handle them promptly.


Display useful information about errors. Try to make error messages informative. It is insufficient, when the program responds with an error — "unable to open a deal" — without further explanation. It is advisable to have a more specific message like "unable to open a deal: incorrect volume of the opened position (0.9999)". It doesn't matter, if the program displays an error information in a pop-up window or in a log file. In any case it should be sufficient for a user or a programmer (especially in the log file analysis) to understand the cause of an error and fix it, if possible. However, users shouldn't be overloaded with information: it is not necessary to display an error's code in a pop-up window, since a user is unable to do much with it.


Logging with MQL5 tools

Log files are normally created by the program specifically for programmers to facilitate the search of failure/error reasons and to evaluate the system's condition at a specific moment in time etc. In addition to that, logging can be used for software profiling.


Levels of logging

Messages received in the log files often carry different criticality and require different levels of attention. Logging levels are applied to separate messages with various criticality from each other and to have the ability to customize the criticality degree of displayed messages. Several logging levels are normally implemented:

  • Debug: debug messages. This level of logging is included in the development, debugging and commissioning stages.
  • Info: informative messages. They carry information about various system activities (e.g. start/end of operation, opening/closing of orders etc). This level messages usually don't require any reaction, but can significantly assist in studying the chains of events that led to operation errors.
  • Warning: warning messages. This level of messages may include a description of situations that led to errors that don't require user intervention. For example, if the calculated trade amount is less than the minimum, and the program has automatically corrected it, then it can be reported with a «Warning» level in the log file.
  • Error: error messages that require intervention. This logging level is typically used with the occurrence of errors linked to issues with saving a certain file, opening or modifying deals etc. In other words, this level includes errors that the program is unable to overcome itself and, therefore, requires a user's or programmer's intervention.
  • Fatal: critical error messages that disable further program operation. Such messages need to be treated instantly, and often a user's or programmer's notification via email, SMS, etc. is provided at this level. Soon we are going to show you, how PUSH notifications are used in MQL5.


Maintaining log files

The easiest way of maintaining log files with MQL5 tools is via the standard functions Print or PrintFormat. As a result, all messages will be sent to the Expert Advisor, indicator and terminal script log.

Example of displaying messages in a common Experts log with Print() function

double VolumeCalculation()
  {
   double result=...;
   if(result<SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN))
     {
      Print("volume of a deal (",DoubleToString(result,2),") appeared to be less than acceptable and has been adjusted to "+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN),2));
      result=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
     }
   return result;
  }

This approach has several disadvantages:

  1. Messages from multiple programs can get mixed up in the total "bunch" and complicate the analysis.
  2. Due to a log file's easy availability it can be accidentally or deliberately deleted by a user.
  3. It is difficult to implement and configure logging levels.
  4. It is impossible to redirect log messages to another source (external file, database, e-mail, etc.).
  5. It is impossible to implement a compulsory rotation of log files (file replacement by data and time or upon reaching a certain size).

Advantage of this approach:

  1. It is sufficient to use the same function without having to invent anything.
  2. In many cases the log file can be viewed directly in the terminal and it doesn't have to be searched separately.

The implementation of a personal logging mechanism can eliminate all disadvantages of using Print() and PrintFormat(). However, if necessary to re-use the code, a transfer to a new project and a logging mechanism (or refusal to use it in the code) will be required.

As an example of a logging mechanism in MQL5 the following scenario can be considered.

Example of a custom logging mechanism implementation in MQL5

//+------------------------------------------------------------------+
//|                                                       logger.mqh |
//|                                   Copyright 2015, Sergey Eryomin |
//|                                             http://www.ensed.org |
//+------------------------------------------------------------------+
#property copyright "Sergey Eryomin"
#property link      "http://www.ensed.org"

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")
//--- maximum number of files for operation of "a new log file for each new 1 Mb"
#define MAX_LOG_FILE_COUNTER (100000) 
//--- number of bytes in a megabyte
#define BYTES_IN_MEGABYTE (1048576)
//--- maximum length of a log file's name
#define MAX_LOG_FILE_NAME_LENGTH (255)
//--- logging levels
enum ENUM_LOG_LEVEL
  {
   LOG_LEVEL_DEBUG,
   LOG_LEVEL_INFO,
   LOG_LEVEL_WARNING,
   LOG_LEVEL_ERROR,
   LOG_LEVEL_FATAL
  };
//--- logging methods
enum ENUM_LOGGING_METHOD
  {
   LOGGING_OUTPUT_METHOD_EXTERN_FILE,// external file
   LOGGING_OUTPUT_METHOD_PRINT // Print function
  };
//--- notification methods
enum ENUM_NOTIFICATION_METHOD
  {
   NOTIFICATION_METHOD_NONE,// disabled
   NOTIFICATION_METHOD_ALERT,// Alert function
   NOTIFICATION_METHOD_MAIL, // SendMail function
   NOTIFICATION_METHOD_PUSH // SendNotification function
  };
//--- log files restriction types
enum ENUM_LOG_FILE_LIMIT_TYPE
  {
   LOG_FILE_LIMIT_TYPE_ONE_DAY,// new log file for every new day
   LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE // new log file for every new 1Mb
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CLogger
  {
public:
   //--- add a message to the log
   //--- Note:
   //--- if output mode to external file is on but it can't be executed,
   //--- then message output is done via Print()
   static void Add(const ENUM_LOG_LEVEL level,const string message)
     {
      if(level>=m_logLevel)
        {
         Write(level,message);
        }

      if(level>=m_notifyLevel)
        {
         Notify(level,message);
        }
     }
   //--- set logging levels
   static void SetLevels(const ENUM_LOG_LEVEL logLevel,const ENUM_LOG_LEVEL notifyLevel)
     {
      m_logLevel=logLevel;
      //--- a level of message output via notifications shouldn't be below a level of writing messages in a log file
      m_notifyLevel=fmax(notifyLevel,m_logLevel);
     }
   //--- set a logging method
   static void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod)
     {
      m_loggingMethod=loggingMethod;
     }
   //--- set a notification method
   static void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod)
     {
      m_notificationMethod=notificationMethod;
     }
   //--- set a name for a log file
   static void SetLogFileName(const string logFileName)
     {
      m_logFileName=logFileName;
     }
   //--- set a type of restriction for a log file
   static void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType)
     {
      m_logFileLimitType=logFileLimitType;
     }

private:
   //--- messages with this and higher logging level will be stored in a log file/journal
   static ENUM_LOG_LEVEL m_logLevel;
   //--- messages with this and higher logging level will be written as notifications
   static ENUM_LOG_LEVEL m_notifyLevel;
   //--- logging method
   static ENUM_LOGGING_METHOD m_loggingMethod;
   //--- notification method
   static ENUM_NOTIFICATION_METHOD m_notificationMethod;
   //--- name of log file
   static string     m_logFileName;
   //--- type of restriction for a log file
   static ENUM_LOG_FILE_LIMIT_TYPE m_logFileLimitType;
   //--- a result of getting a file name for a log           
   struct GettingFileLogNameResult
     {
                        GettingFileLogNameResult(void)
        {
         succes=false;
         ArrayInitialize(value,0);
        }
      bool              succes;
      char              value[MAX_LOG_FILE_NAME_LENGTH];
     };
   //--- a result for checking the size of existing log file
   enum ENUM_LOG_FILE_SIZE_CHECKING_RESULT
     {
      IS_LOG_FILE_LESS_ONE_MEGABYTE,
      IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE,
      LOG_FILE_SIZE_CHECKING_ERROR
     };
   //--- write in a log file
   static void Write(const ENUM_LOG_LEVEL level,const string message)
     {
      switch(m_loggingMethod)
        {
         case LOGGING_OUTPUT_METHOD_EXTERN_FILE:
           {
            GettingFileLogNameResult getLogFileNameResult=GetLogFileName();
            //---
            if(getLogFileNameResult.succes)
              {
               string fileName=CharArrayToString(getLogFileNameResult.value);
               //---
               if(WriteToFile(fileName,GetDebugLevelStr(level)+": "+message))
                 {
                  break;
                 }
              }
           }
         case LOGGING_OUTPUT_METHOD_PRINT:
            default:
              {
               Print(GetDebugLevelStr(level)+": "+message);
               break;
              }
        }
     }
   //--- execute a notification
   static void Notify(const ENUM_LOG_LEVEL level,const string message)
     {
      if(m_notificationMethod==NOTIFICATION_METHOD_NONE)
        {
         return;
        }
      string fullMessage=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+message;
      //---
      switch(m_notificationMethod)
        {
         case NOTIFICATION_METHOD_MAIL:
           {
            if(TerminalInfoInteger(TERMINAL_EMAIL_ENABLED))
              {
               if(SendMail("Logger",fullMessage))
                 {
                  return;
                 }
              }
           }
         case NOTIFICATION_METHOD_PUSH:
           {
            if(TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
              {
               if(SendNotification(fullMessage))
                 {
                  return;
                 }
              }
           }
        }
      //---
      Alert(GetDebugLevelStr(level)+": "+message);
     }
   //--- obtain a log file name for writing
   static GettingFileLogNameResult GetLogFileName()
     {
      if(m_logFileName=="")
        {
         InitializeDefaultLogFileName();
        }
      //---
      switch(m_logFileLimitType)
        {
         case LOG_FILE_LIMIT_TYPE_ONE_DAY:
           {
            return GetLogFileNameOnOneDayLimit();
           }
         case LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE:
           {
            return GetLogFileNameOnOneMegabyteLimit();
           }
         default:
           {
            GettingFileLogNameResult failResult;
            failResult.succes=false;
            return failResult;
           }
        }
     }
   //--- get a log file name in case of restriction with "new log file for every new day"
   static GettingFileLogNameResult GetLogFileNameOnOneDayLimit()
     {
      GettingFileLogNameResult result;
      string fileName=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+TimeToString(TimeLocal(),TIME_DATE);
      StringReplace(fileName,".","_");
      fileName=fileName+".log";
      result.succes=(StringToCharArray(fileName,result.value)==StringLen(fileName)+1);
      return result;
     }
   //--- get a log file name in case of restriction with "new log file for each new 1 Mb"
   static GettingFileLogNameResult GetLogFileNameOnOneMegabyteLimit()
     {
      GettingFileLogNameResult result;
      //---
      for(int i=0; i<MAX_LOG_FILE_COUNTER; i++)
        {
         ResetLastError();
         string fileNameToCheck=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+(string)i;
         StringReplace(fileNameToCheck,".","_");
         fileNameToCheck=fileNameToCheck+".log";
         ResetLastError();
         bool isExists=FileIsExist(fileNameToCheck);
         //---
         if(!isExists)
           {
            if(GetLastError()==5018)
              {
               continue;
              }
           }
         //---
         if(!isExists)
           {
            result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

            break;
           }
         else
           {
            ENUM_LOG_FILE_SIZE_CHECKING_RESULT checkLogFileSize=CheckLogFileSize(fileNameToCheck);

            if(checkLogFileSize==IS_LOG_FILE_LESS_ONE_MEGABYTE)
              {
               result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

               break;
              }
            else if(checkLogFileSize!=IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE)
              {
               break;
              }
           }
        }
      //---
      return result;
     }
   //---
   static ENUM_LOG_FILE_SIZE_CHECKING_RESULT CheckLogFileSize(const string fileNameToCheck)
     {
      int fileHandle=FileOpen(fileNameToCheck,FILE_TXT|FILE_READ);
      //---
      if(fileHandle==INVALID_HANDLE)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      ResetLastError();
      ulong fileSize=FileSize(fileHandle);
      FileClose(fileHandle);
      //---
      if(GetLastError()!=0)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      if(fileSize<BYTES_IN_MEGABYTE)
        {
         return IS_LOG_FILE_LESS_ONE_MEGABYTE;
        }
      else
        {
         return IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE;
        }
     }
   //--- perform a log file name initialization by default
   static void InitializeDefaultLogFileName()
     {
      m_logFileName=MQLInfoString(MQL_PROGRAM_NAME);
      //---
#ifdef __MQL4__
      StringReplace(m_logFileName,".ex4","");
#endif

#ifdef __MQL5__
      StringReplace(m_logFileName,".ex5","");
#endif
     }
   //--- write a message in a file
   static bool WriteToFile(const string fileName,
                           const string text)
     {
      ResetLastError();
      string fullText=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+text;
      int fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);
      bool result=true;
      //---
      if(fileHandle!=INVALID_HANDLE)
        {
         //--- attempt to place a file pointer in the end of a file            
         if(!FileSeek(fileHandle,0,SEEK_END))
           {
            Print("Logger: FileSeek() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
            result=false;
           }
         //--- attempt to write a text in a file
         if(result)
           {
            if(FileWrite(fileHandle,fullText)==0)
              {
               Print("Logger: FileWrite() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
               result=false;
              }
           }
         //---
         FileClose(fileHandle);
        }
      else
        {
         Print("Logger: FileOpen() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
         result=false;
        }
      //---
      return result;
     }
   //--- get a current period as a line
   static string GetPeriodStr()
     {
      ResetLastError();
      string periodStr=EnumToString(Period());
      if(GetLastError()!=0)
        {
         periodStr=(string)Period();
        }
      StringReplace(periodStr,"PERIOD_","");
      //---
      return periodStr;
     }
   //---
   static string GetDebugLevelStr(const ENUM_LOG_LEVEL level)
     {
      ResetLastError();
      string levelStr=EnumToString(level);
      //---
      if(GetLastError()!=0)
        {
         levelStr=(string)level;
        }
      StringReplace(levelStr,"LOG_LEVEL_","");
      //---
      return levelStr;
     }
  };
ENUM_LOG_LEVEL CLogger::m_logLevel=LOG_LEVEL_INFO;
ENUM_LOG_LEVEL CLogger::m_notifyLevel=LOG_LEVEL_FATAL;
ENUM_LOGGING_METHOD CLogger::m_loggingMethod=LOGGING_OUTPUT_METHOD_EXTERN_FILE;
ENUM_NOTIFICATION_METHOD CLogger::m_notificationMethod=NOTIFICATION_METHOD_ALERT;
string CLogger::m_logFileName="";
ENUM_LOG_FILE_LIMIT_TYPE CLogger::m_logFileLimitType=LOG_FILE_LIMIT_TYPE_ONE_DAY;
//+------------------------------------------------------------------+

This code can be placed as a separate included file, for instance Logger.mqh, and saved in <data_folder>/MQL5/Include (this file is attached to the article). The operation with the CLogger class will look approximately the following way:

Example of implementation of a personal logging mechanism

#include <Logger.mqh>

//--- initialize a logger
void InitLogger()
  {
//--- set logging levels: 
//--- DEBUG level for writing messages in a log file
//--- ERROR-level for notification
   CLogger::SetLevels(LOG_LEVEL_DEBUG,LOG_LEVEL_ERROR);
//--- set a notification type as PUSH notification
   CLogger::SetNotificationMethod(NOTIFICATION_METHOD_PUSH);
//--- set logging method as an external file writing
   CLogger::SetLoggingMethod(LOGGING_OUTPUT_METHOD_EXTERN_FILE);
//--- set a name for log files
   CLogger::SetLogFileName("my_log");
//--- set log file restrictions as "new log file for every new day"
   CLogger::SetLogFileLimitType(LOG_FILE_LIMIT_TYPE_ONE_DAY);
  }

int OnInit()
  {
//---
   InitLogger();
//---
   CLogger::Add(LOG_LEVEL_INFO,"");
   CLogger::Add(LOG_LEVEL_INFO,"---------- OnInit() -----------");
   LOG(LOG_LEVEL_DEBUG,"Example of debug message");
   LOG(LOG_LEVEL_INFO,"Example of info message");
   LOG(LOG_LEVEL_WARNING,"Example of warning message");
   LOG(LOG_LEVEL_ERROR,"Example of error message");
   LOG(LOG_LEVEL_FATAL,"Example of fatal message");
//---
   return(INIT_SUCCEEDED);
  }

First, the InitLogger() function has an initialization of all possible logger parameters, and then messages are written in a log file. The result of this code will be recorded in a log file with a name of a type «my_log_USDCAD_D1_2015_09_23.log» inside <data_folder>/MQL5/Files of the following text:

2015.09.23 09:02:10, USDCAD (D1), INFO: 
2015.09.23 09:02:10, USDCAD (D1), INFO: ---------- OnInit() -----------
2015.09.23 09:02:10, USDCAD (D1), DEBUG: Example of debug message (LoggerTest.mq5; int OnInit(); Line: 36)
2015.09.23 09:02:10, USDCAD (D1), INFO: Example of info message (LoggerTest.mq5; int OnInit(); Line: 38)
2015.09.23 09:02:10, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:02:10, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:02:10, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

In addition to that, messages of levels ERROR and FATAL will be sent through PUSH-notifications.

When setting a message level for writing in a log file as Warning (CLogger::SetLevels(LOG_LEVEL_WARNING,LOG_LEVEL_ERROR)), the output will be the following:

2015.09.23 09:34:00, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:34:00, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:34:00, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

It means, that messages below the WARNING level will not be saved.


Public methods of CLogger class and LOG macro

Let's analyze the CLogger class and LOG macro public methods.


void SetLevels(const ENUM_LOG_LEVEL logLevel, const ENUM_LOG_LEVEL notifyLevel). Sets logging levels.

const ENUM_LOG_LEVEL logLevel — messages with this and higher logging level will be stored in a log file/journal. By default = LOG_LEVEL_INFO.

const ENUM_LOG_LEVEL notifyLevel — messages with this and higher logging level will be displayed as notifications. By default = LOG_LEVEL_FATAL.

Possible values for both:

  • LOG_LEVEL_DEBUG,
  • LOG_LEVEL_INFO,
  • LOG_LEVEL_WARNING,
  • LOG_LEVEL_ERROR,
  • LOG_LEVEL_FATAL.


void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod). Sets a logging method.

const ENUM_LOGGING_METHOD loggingMethod — logging method. By default = LOGGING_OUTPUT_METHOD_EXTERN_FILE.

Possible values:

  • LOGGING_OUTPUT_METHOD_EXTERN_FILE — external file,
  • LOGGING_OUTPUT_METHOD_PRINT — Print function.


void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod). Sets a notification method.

const ENUM_NOTIFICATION_METHOD notificationMethod — notification method. By default = NOTIFICATION_METHOD_ALERT.

Possible values:

  • NOTIFICATION_METHOD_NONE — disabled,
  • NOTIFICATION_METHOD_ALERT — Alert function,
  • NOTIFICATION_METHOD_MAIL — SendMail function,
  • NOTIFICATION_METHOD_PUSH — SendNotification function.


void SetLogFileName(const string logFileName). Sets a name of a log file.

const string logFileName — a name of a log file. The default value will be the program's name, where a logger is used (see InitializeDefaultLogFileName() private method).


void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType). Sets a restriction type on a log file.

const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType — restriction type on a log file. Default value: LOG_FILE_LIMIT_TYPE_ONE_DAY.

Possible values:

  • LOG_FILE_LIMIT_TYPE_ONE_DAY — a new log file for every new day. Files with the following names will be created: my_log_USDCAD_D1_2015_09_21.log, my_log_USDCAD_D1_2015_09_22.log , my_log_USDCAD_D1_2015_09_23 .log etc.
  • LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE — a new log file for every new 1 Mb. Files with the following names will be created: my_log_USDCAD_D1_0.log, my_log_USDCAD_D1_1.log, my_log_USDCAD_D1_2.log etc. Switch to a next file once of the previous file reaches 1Mb.


void Add(const ENUM_LOG_LEVEL level,const string message). Adds a message to a log.

const ENUM_LOG_LEVEL level — message level. Possible values:

  • LOG_LEVEL_DEBUG
  • LOG_LEVEL_INFO
  • LOG_LEVEL_WARNING
  • LOG_LEVEL_ERROR
  • LOG_LEVEL_FATAL

const string message — a text of a message.


In addition to the Add method, the LOG macro is also implemented and it adds to the message a file name, function's signature and line number where writing in the log file is performed:

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")

This macro can be particularly useful at debugging.

Thus, the example shows the logging mechanism which allows:

  1. To configure logging levels (DEBUG..FATAL).
  2. To set a level of messages when users should be notified.
  3. To set where a log should be written — in the Expert log via Print() or to an external file.
  4. For external file output — to indicate a file name and set restrictions to log files: a file for every single date, or a file for every logging megabyte.
  5. To specify a notification type (Alert(), SendMail(), SendNotify()).

The proposed option is certainly just an example, and modification will be required for certain tasks (including the removal of unnecessary functionality). For example, in addition to writing to an external file and the common log, writing to a database can be implemented as another logging method.


Conclusion

In this article we have considered matters of error handling and logging with MQL5 tools. Correct error handling and relevant logging can considerably increase the quality of a developed software and greatly simplify the future support.

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/2041

Attached files |
logger.mqh (24.94 KB)
loggertest.mq5 (4.67 KB)

Other articles by this author

Last comments | Go to discussion (2)
Alex
Alex | 31 Oct 2016 at 21:05

Hi,

thank you for your article!

Am I allowed to use your CLogger class in my own projects?

BTW: What kind of license do articles/source code from mql5.com belong to?

Best regards,

Alex 

Zarik
Zarik | 4 Jul 2022 at 16:26

What a treasure! Thank you for your contribution!

Cheers, Zarik

Using Assertions in MQL5 Programs Using Assertions in MQL5 Programs
This article covers the use of assertions in MQL5 language. It provides two examples of the assertion mechanism and some general guidance for implementing assertions.
Evaluation and selection of variables for machine learning models Evaluation and selection of variables for machine learning models
This article focuses on specifics of choice, preconditioning and evaluation of the input variables (predictors) for use in machine learning models. New approaches and opportunities of deep predictor analysis and their influence on possible overfitting of models will be considered. The overall result of using models largely depends on the result of this stage. We will analyze two packages offering new and original approaches to the selection of predictors.
MQL5 for beginners: Anti-vandal protection of graphic objects MQL5 for beginners: Anti-vandal protection of graphic objects
What should your program do, if graphic control panels have been removed or modified by someone else? In this article we will show you how not to have "ownerless" objects on the chart, and how not to lose control over them in cases of renaming or deleting programmatically created objects after the application is deleted.
Handling ZIP Archives in Pure MQL5 Handling ZIP Archives in Pure MQL5
The MQL5 language keeps evolving, and its new features for working with data are constantly being added. Due to innovation it has recently become possible to operate with ZIP archives using regular MQL5 tools without getting third party DLL libraries involved. This article focuses on how this is done and provides the CZip class, which is a universal tool for reading, creating and modifying ZIP archives, as an example.