
Mastering Log Records (Part 2): Formatting Logs
Introduction
In the first article of this series, Mastering Log Records (Part 1): Fundamental Concepts and First Steps in MQL5, we embarked on the creation of a custom log library for Expert Advisor (EA) development. In it, we explored the motivation behind creating such an essential tool: to overcome the limitations of MetaTrader 5’s native logs and bring a robust, customizable, and powerful solution to the MQL5 universe.
To recap the main points covered, we laid the foundation for our library by establishing the following fundamental requirements:
- Robust structure using the Singleton pattern, ensuring consistency between code components.
- Advanced persistence for storing logs in databases, providing traceable history for in-depth audits and analysis.
- Flexibility in outputs, allowing logs to be stored or displayed conveniently, whether in the console, in files, in the terminal or in a database.
- Classification by log levels, differentiating informative messages from critical alerts and errors.
- Customization of the output format, to meet the unique needs of each developer or project.
With this well-established foundation, it became clear that the logging framework we are developing will be much more than a simple event log; it will be a strategic tool for understanding, monitoring and optimizing the behavior of EAs in real time.
Now, in this second article, we will get to the heart of one of the most relevant features of this library: log formatting. After all, an efficient log is not only about what is being recorded, but about how the information is presented. Imagine receiving confusing or poorly formatted messages in the middle of an important EA test. This can not only make the analysis unnecessarily complex, but also lead to the loss of valuable information. We want each log to carry the necessary clarity and detail, while still being customizable according to the developer's needs.
What is a Log Format?
A log format is the structure that organizes the information recorded during the execution of a program in a clear and understandable way. It is a standard that defines how each record will be displayed, gathering key data such as the severity level of the event, the date and time it occurred, the source of the log, and the associated descriptive message.
This organization is essential to making logs readable and useful. Without a format, logs can appear confusing and lack context, making analysis difficult. For example, imagine an unstructured log that simply says:
DEBUG: Order sent successfully, server responded in 32msNow, compare this to a log that follows a structured format, which not only presents the same information but also provides context:
[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms
This small difference can make a big difference in diagnosing problems, especially in complex systems. The format transforms raw data into a cohesive narrative, guiding the developer in understanding the behavior of the system.
Flexibility is also a key aspect of log formats. Each project has specific needs, and the ability to adapt the format allows for useful customizations, such as highlighting critical events, tracking sources, or enriching messages with contextual metadata.
Basic Structure of a Formatter
The basic structure of a formatter is based on the concept of "placeholders", elements that act as substitution points in a template. These placeholders define where and how information from a log will be inserted into a final message.
Think of a formatter as a machine that transforms raw data from a log event into a readable, customizable format. The input can be a data model containing values such as severity level, message, time, and other details. The formatter then applies these values to the template provided by the user, resulting in a formatted log.
For example, consider a template like this:
{date_time} {levelname}: {msg}When the values are replaced with the corresponding placeholders, the output looks something like this:
12/04/2025 00:25:45 DEBUG: IFR indicator successfully inserted into the chart!
The strength of a formatter lies in its flexibility. Here are some examples of placeholders we will add to the library:
- {levelname} : Represents the log level (e.g., DEBUG, ERROR, FATAL) in a human-friendly way.
- {msg} : The message describing the logged event.
- {args} : Additional data that contextualizes the message, often used to capture dynamic information.
- {timestamp} : The timestamp in milliseconds, useful for accuracy analysis.
- {date_time} : The human-readable version of the timestamp, displaying the date and time in human-standard format.
- {origin} : Indicates the origin of the event, such as the module or class where the log was emitted.
- {filename} , {function} and {line} : Identify the exact location in the code where the log was generated, making debugging more efficient.
The logic behind a formatter is simple but powerful. It allows you to create a custom frame for each log message, giving the developer a way to present the most relevant information for each situation. This approach is especially useful in projects where the volume of data can be high and fast analysis is essential.
With customizable templates and comprehensive placeholders, the library will provide a highly modular tool. This modularity will ensure that you can create logs tailored to the specific needs of your application, increasing the efficiency in interpreting and using the logged data.
Implementing Formatters in MQL5
Now that we understand what formats and placeholders are, let's get straight to the code to understand how this will be implemented in the current stage of the library. First, let's create a new folder inside <Include/Logify> called “Formatter”. Inside this folder, create a file called LogifyFormatter.mqh. In the end, the path will be <Include/Logify/Formatter/LogifyFormatter.mqh>. Remember that I have attached the final files used in the article at the end of the article, just download and use it. This is what the file explorer should look like:
//+------------------------------------------------------------------+ //| LogifyFormatter.mqh | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../LogifyModel.mqh" //+------------------------------------------------------------------+ //| class : CLogifyFormatter | //| | //| [PROPERTY] | //| Name : CLogifyFormatter | //| Heritage : No heritage | //| Description : Class responsible for formatting the log into a | //| string, replacing placeholders with their respective values. | //| | //+------------------------------------------------------------------+ class CLogifyFormatter { public: CLogifyFormatter(void); ~CLogifyFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::CLogifyFormatter(void) { } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::~CLogifyFormatter(void) { } //+------------------------------------------------------------------+
We declare the private attributes that store the date and log formats
class CLogifyFormatter { private: //--- Stores the formats string m_date_format; string m_log_format; public: //--- Format query methods string GetDateFormat(void); string GetLogFormat(void); }; //+------------------------------------------------------------------+ //| Get date format | //+------------------------------------------------------------------+ string CLogifyFormatter::GetDateFormat(void) { return(m_date_format); } //+------------------------------------------------------------------+ //| Get the log format | //+------------------------------------------------------------------+ string CLogifyFormatter::GetLogFormat(void) { return(m_log_format); } //+------------------------------------------------------------------+
Here we define:
- m_date_format : Defines how dates will be formatted (e.g. "yyyy/MM/dd hh:mm:ss")
- m_log_format : Defines the log standard (e.g. "{timestamp} - {msg}")
- And two other methods to access the private attributes
The constructor initializes the date and log formats, validating the log format with the CheckLogFormat method that we will see in a moment. To do this, I added two parameters to the constructor, thus facilitating the definition of the format right when creating an instance of the class.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::CLogifyFormatter(string date_formate,string log_format) { m_date_format = date_formate; if(CheckLogFormat(log_format)) { m_log_format = log_format; } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyFormatter::~CLogifyFormatter(void) { } //+------------------------------------------------------------------+
The destructor ( ~CLogifyFormatter ) does not perform any specific action, but it is a good practice to declare it, as it may be useful in future situations.
Regarding the CheckLogFormat method, we will implement it now. It plays an essential role in validating the templates used to format the logs. Its purpose is to ensure that all placeholders within the template are properly structured and closed before being processed. This type of validation is essential to avoid unexpected errors and ensure the reliability of the log outputs. Let's explore some examples of invalid formats and understand the reasons:
- Unclosed placeholders: An example is {timestamp} - {{msg} . Here, we have an additional { character that was not properly closed. This type of error indicates an incomplete structure, which can lead to failures in the log processing. - Non-opening placeholders: In cases such as {timestamp} - {msg}} , there is an extra } character that does not match any { . As in the previous example, this causes inconsistencies in the log structure.
- Empty placeholders: The template {timestamp} - {msg} {} contains an empty placeholder {} that does not have an associated key. Each placeholder must be filled with a valid reference that will be dynamically replaced, and empty templates break this expectation.
- Empty format: The method also considers invalid cases in which the provided string is completely empty. To be valid, the string must contain at least one character, which will serve as the basis for the formatted log.
When the method identifies any of these irregularities, it returns false and prints detailed error messages to the developer. These messages help to quickly locate and correct problems in the provided template. On the other hand, if the template is properly structured and follows all the rules, the method will return true , signaling that it is ready to be used. In addition to ensuring accuracy, the method promotes good practices in the design of log templates, encouraging developers to create clear and consistent structures. This approach not only improves the readability of logs, but also makes them easier to maintain and analyze over time.
//+------------------------------------------------------------------+ //| Validate format | //+------------------------------------------------------------------+ bool CLogifyFormatter::CheckLogFormat(string log_format) { //--- Variables to track the most recent '{' opening index and the number of '{' brace openings int openIndex = -1; // Index of last '{' found int openBraces = 0; // '{' counter int len = StringLen(log_format); // String length //--- Checks if string is empty if(len == 0) { //--- Prints error message and returns false Print("Erro de formatação: sequência inesperada encontrada. Verifique o padrão de placeholders usado."); return(false); } //--- Iterate through each character of the string for(int i=0;i<len;i++) { //--- Gets the current character ushort character = StringGetCharacter(log_format,i); //--- Checks if the character is an opening '{' if(character == '{') { openBraces++; // Increments the opening counter '{' openIndex = i; // Updates the index of the last opening } //--- Checks if the character is a closing '}' else if(character == '}') { //--- If there is no matching '{' if(openBraces == 0) { //--- Prints error message and returns false Print("Erro de formatação: o caractere '}' na posição ",i," não possui um '{' correspondente."); return(false); } //--- Decreases the open count because a matching '{' was found openBraces--; //--- Extracts the contents of the placeholder (between '{' and '}') string placeHolder = StringSubstr(log_format, openIndex + 1, i - openIndex - 1); //--- Checks if placeholder is empty if(StringLen(placeHolder) == 0) { //--- Prints error message and returns false Print("Erro de formatação: placeholder vazio detectado na posição ",i,". Um nome é esperado dentro de '{...}'."); return(false); } } } //--- After traversing the entire string, check if there are still any unmatched '{'}' if(openBraces > 0) { //--- Prints error message indicating the index of the last opened '{' and returns false Print("Erro de formatação: o placeholder '{' na posição ",openIndex," não possui um '}' correspondente."); return(false); } //--- Format is correct return(true); } //+------------------------------------------------------------------+
Now let's move on to the two main methods of the class that are responsible for formatting logs:
- FormatDate() : For manipulating dates
- FormatLog() : For creating the log format itself
Both of these play central roles in customizing the data logged by the library. To make these methods extensible and flexible, they are declared as virtual. Since the methods are declared as virtual, FormatDate can be easily overridden in derived classes to suit specific scenarios. For example, a custom implementation could adapt the date format to include the time zone or other additional information. This flexible architecture allows the library to evolve with the demands of projects, ensuring its suitability in various contexts.
The FormatDate method is responsible for transforming a datetime object into a formatted string, adapted to the standard defined in m_date_format . This pattern uses a system of placeholders that are dynamically replaced with the corresponding elements of a date, such as year, month, day, time, and so on.
This approach is incredibly flexible, allowing for highly customized formats that can suit a variety of scenarios. For example, you can choose to display just the day and month, or include complete information such as the day of the week and time. Below are the available placeholders:
- Year
- yy→ 2-digit year (e.g. "25").
- yyyy → 4-digit year (e.g. "2025").
- Month
- M → Month without leading zero (e.g. "1" for January).
- MM → 2-digit month (e.g. "01" for January).
- MMM → Month abbreviation (e.g. "Jan").
- MMMM → Full month name (e.g. "January").
- Day
- d → Day without leading zero (e.g. "1").
- dd → 2-digit day (e.g. "01").
- Day of the year
- D → Day of the year without leading zero (e.g. "32" for February 1st). - DDD → Three-digit day of the year (e.g. "032").
- Day of the week
- E → Abbreviated name of the day of the week (e.g. "Mon").
- EEEE → Full name of the day of the week (e.g. "Monday").
- Hours in 24-hour format
- H → Hour without leading zero (e.g. "9").
- HH → Two-digit hour (e.g. "09").
- Hours in 12-hour format
- h → Hour without leading zero (e.g. "9").
- hh → Two-digit hour (e.g. "09").
- Minute
- m → Minute without leading zero (e.g. "5").
- mm → Two-digit minute (e.g. "05").
- Second
- s → Second without leading zero (e.g. "9").
- ss → Second with two digits (e.g. "09").
- AM/PM
- tt → Returns in lowercase letters (am/pm)
- TT → Returns in uppercase letters (AM/PM)
This logic makes it extremely convenient to customize the date display according to your needs. For example, using the "yyyy-MM-dd HH:mm:ss" format, the output would be something like "2025-01-02 14:30:00" . Using "EEEE, MMM dd, yyyy" , the output would be "Tuesday, Jul 30, 2019" . This adaptability is essential to provide logs that are both informative and visually clear.
The logic behind FormatLog is based on the MQL5 native function StringReplace(). This function performs direct string replacements, where all occurrences of a specific substring are replaced by another one. In the context of the FormatLog method, placeholders such as {timestamp} or {message} are replaced by real values from the log model. This transforms instances of a model such as MqlLogifyModel into organized data ready to be visualized.
Here is the code for the implementation in the class, I added several comments to make the code as didactic as possible:
//+------------------------------------------------------------------+ //| class : CLogifyFormatter | //| | //| [PROPERTY] | //| Name : CLogifyFormatter | //| Heritage : No heritage | //| Description : Class responsible for formatting the log into a | //| string, replacing placeholders with their respective values. | //| | //+------------------------------------------------------------------+ class CLogifyFormatter { //--- Date and log formatting methods virtual string FormatDate(datetime date); virtual string FormatLog(MqlLogifyModel &data); }; //+------------------------------------------------------------------+ //| Formats dates | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatDate(datetime date) { string formated = m_date_format; //--- Date and time structure MqlDateTime time; TimeToStruct(date, time); //--- Array with months and days of the week in string string months_abbr[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; string months_full[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; string day_of_week_abbr[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; string day_of_week_full[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; //--- Replace year StringReplace(formated, "yyyy", IntegerToString(time.year)); StringReplace(formated, "yy", IntegerToString(time.year % 100, 2, '0')); //--- Replace month if(StringFind(formated,"MMM") < 0 && StringFind(formated,"MMMM") < 0) { StringReplace(formated, "MM", IntegerToString(time.mon, 2, '0')); StringReplace(formated, "M", IntegerToString(time.mon)); } //--- Replace day StringReplace(formated, "dd", IntegerToString(time.day, 2, '0')); StringReplace(formated, "d", IntegerToString(time.day)); //--- Replace day of year StringReplace(formated, "DDD", IntegerToString(time.day_of_year, 3, '0')); StringReplace(formated, "D", IntegerToString(time.day_of_year)); //--- Replace Replace hours (24h and 12h) StringReplace(formated, "HH", IntegerToString(time.hour, 2, '0')); StringReplace(formated, "H", IntegerToString(time.hour)); int hour_12 = time.hour % 12; if (hour_12 == 0) hour_12 = 12; StringReplace(formated, "hh", IntegerToString(hour_12, 2, '0')); StringReplace(formated, "h", IntegerToString(hour_12)); //--- Replace minutes and seconds StringReplace(formated, "mm", IntegerToString(time.min, 2, '0')); StringReplace(formated, "m", IntegerToString(time.min)); StringReplace(formated, "ss", IntegerToString(time.sec, 2, '0')); StringReplace(formated, "s", IntegerToString(time.sec)); //--- Replace AM/PM bool is_am = (time.hour < 12); StringReplace(formated, "tt", is_am? "am" : "pm"); StringReplace(formated, "TT", is_am? "AM" : "PM"); //--- Replace month StringReplace(formated, "MMMM", months_full[time.mon - 1]); StringReplace(formated, "MMM", months_abbr[time.mon - 1]); //--- Replace day of week StringReplace(formated, "EEEE", day_of_week_full[time.day_of_week]); StringReplace(formated, "E", day_of_week_abbr[time.day_of_week]); return(formated); } //+------------------------------------------------------------------+ //| Format logs | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatLog(MqlLogifyModel &data) { string formated = m_log_format; //--- Replace placeholders StringReplace(formated,"{timestamp}",IntegerToString(data.timestamp)); StringReplace(formated,"{level}",IntegerToString(data.level)); StringReplace(formated,"{origin}",data.origin); StringReplace(formated,"{message}",data.message); StringReplace(formated,"{metadata}",data.metadata); return(formated); } //+------------------------------------------------------------------+
This concludes the construction of the class, now let's make some updates to MqlLogifyModel.
Adding more data to MqlLogifyModel
The MqlLogifyModel is one of the core elements of our logging library, representing the base structure for storing and manipulating the data related to each log event. In its current state, the structure is defined as follows:struct MqlLogifyModel { ulong timestamp; // Date and time of the event ENUM_LOG_LEVEL level; // Severity level string origin; // Log source string message; // Log message string metadata; // Additional information in JSON or text MqlLogifyModel::MqlLogifyModel(void) { timestamp = 0; level = LOG_LEVEL_DEBUG; origin = ""; message = ""; metadata = ""; } MqlLogifyModel::MqlLogifyModel(ulong _timestamp,ENUM_LOG_LEVEL _level,string _origin,string _message,string _metadata) { timestamp = _timestamp; level = _level; origin = _origin; message = _message; metadata = _metadata; } };
This initial version works well in simple scenarios, but we can significantly improve it by adding more information and refining the property names to make it easier to use and align the model with best practices. Below, we will discuss the planned improvements.
Property Name Simplification- The message field will be renamed to msg . This change, although subtle, reduces the number of characters when accessing the property, making it easier to write and read the code.
- metadata will be replaced by args , as the new name more accurately reflects the function of the property: to store the context data associated with the log at the time it is generated.
New Fields Added
To enrich the logs and enable more detailed analysis, the following fields will be included:
- formatted : Log message formatted according to the specified format. It will be used to store the final version of the log, already replacing placeholders with real values. This property will be read-only.
- levelname : Textual name of the log level (e.g., "DEBUG", "INFO"). This is useful when the format requires a description of the severity level.
- date_time : Represents the date and time of the event in datetime format, an alternative to timestamp .
- filename : The name of the file where the log was generated, essential for tracking the exact source.
- function : Name of the function where the log was called, providing more context about the origin of the event.
- line : Line number in the source file where the log was generated. This can be particularly useful in debugging scenarios.
These new fields make the MqlLogifyModel more robust and prepared to meet different logging requirements, such as detailed debugging or integration with external monitoring tools. After the changes, the code looks like this:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ struct MqlLogifyModel { string formated; // The log message formatted according to the specified format. string levelname; // Textual name of the log level (e.g., "DEBUG", "INFO") string msg; // Main content of the log message string args; // Additional arguments associated with the log message ulong timestamp; // Timestamp of the log event, represented in seconds since the start of the Unix epoch datetime date_time; // Date and time of the log event, in datetime format ENUM_LOG_LEVEL level; // Enumeration representing the severity level of the log string origin; // Source or context of the log message (e.g., class or module) string filename; // Name of the source file where the log message was generated string function; // Name of the function where the log was called ulong line; // Line number in the source file where the log was generated MqlLogifyModel::MqlLogifyModel(void) { formated = ""; levelname = ""; msg = ""; args = ""; timestamp = 0; date_time = 0; level = LOG_LEVEL_DEBUG; origin = ""; filename = ""; function = ""; line = 0; } MqlLogifyModel::MqlLogifyModel(string _formated,string _levelname,string _msg,string _args,ulong _timestamp,datetime _date_time,ENUM_LOG_LEVEL _level,string _origin,string _filename,string _function,ulong _line) { formated = _formated; levelname = _levelname; msg = _msg; args = _args; timestamp = _timestamp; date_time = _date_time; level = _level; origin = _origin; filename = _filename; function = _function; line = _line; } }; //+------------------------------------------------------------------+As a next step, we will update the FormatLog method of the CLogifyFormatter class to include support for the placeholders corresponding to the new properties. Below is the updated version of the method, now compatible with all the new model properties:
//+------------------------------------------------------------------+ //| Format logs | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatLog(MqlLogifyModel &data) { string formated = m_log_format; StringReplace(formated,"{levelname}",data.levelname); StringReplace(formated,"{msg}",data.msg); StringReplace(formated,"{args}",data.args); StringReplace(formated,"{timestamp}",IntegerToString(data.timestamp)); StringReplace(formated,"{date_time}",this.FormatDate(data.date_time)); StringReplace(formated,"{level}",IntegerToString(data.level)); StringReplace(formated,"{origin}",data.origin); StringReplace(formated,"{filename}",data.filename); StringReplace(formated,"{function}",data.function); StringReplace(formated,"{line}",IntegerToString(data.line)); return(formated); } //+------------------------------------------------------------------+
In the FormatLog method, the value of {date_time} is replaced by the formatted return of FormatDate, so this placeholder is replaced by the date format previously passed.
Applying Formatters to Logs
Let's make sure that our CLogify class can use a formatter to format log messages. Let's import the class responsible for this and add an attribute to store it:
#include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh"
The next step is to add the m_formatter attribute to the CLogify class to store the formatter instance and create the methods to configure and access it. This will allow us to reuse the formatter at different points in the system:
//+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifyFormatter *m_formatter; public: //--- Get/Set object formatter void SetFormatter(CLogifyFormatter *format); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogify::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogify::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
The existing logging methods have a limited set of parameters, such as timestamp, log level, message, source, and metadata. We will improve this approach by adding new parameters that describe the context of the log, such as:
- filename : Name of the file where the log occurred.
- function : Name of the function where the log occurred.
- line : Line of code that generated the log.
In addition, to make the method signature more intuitive, we will adjust the parameter names. Here is an example of how the main method ( Append ) was modified:
bool CLogify::Append(ulong timestamp, ENUM_LOG_LEVEL level, string message, string origin = "", string metadata = "");
Adjusted Method
bool CLogify::Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0);
With that, our new implementation is:
//+------------------------------------------------------------------+ //| Generic method for adding logs | //+------------------------------------------------------------------+ bool CLogify::Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { //--- If the formatter is not configured, the log will not be recorded. if(m_formatter == NULL) { return(false); } //--- Textual name of the log level string levelStr = ""; switch(level) { case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break; case LOG_LEVEL_INFOR: levelStr = "INFOR"; break; case LOG_LEVEL_ALERT: levelStr = "ALERT"; break; case LOG_LEVEL_ERROR: levelStr = "ERROR"; break; case LOG_LEVEL_FATAL: levelStr = "FATAL"; break; } //--- Creating a log template with detailed information datetime time_current = TimeCurrent(); MqlLogifyModel data("",levelStr,msg,args,time_current,time_current,level,origin,filename,function,line); //--- Printing the formatted log Print(m_formatter.FormatLog(data)); return(true); } //+------------------------------------------------------------------+
Notice that I am printing the return value of the FormatLog function, returning the data object.
Now that we have the base Append method working with detailed parameters, we can adjust or add other specialized methods for each log level (Debug, Infor, Alert, Error, and Fatal). These methods are "shortcuts" that set the log level automatically, while allowing the use of all the other parameters.
//+------------------------------------------------------------------+ //| Debug level message | //+------------------------------------------------------------------+ bool CLogify::Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_DEBUG,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Infor level message | //+------------------------------------------------------------------+ bool CLogify::Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_INFOR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Alert level message | //+------------------------------------------------------------------+ bool CLogify::Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ALERT,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Error level message | //+------------------------------------------------------------------+ bool CLogify::Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ERROR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Fatal level message | //+------------------------------------------------------------------+ bool CLogify::Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_FATAL,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+
Practical Example
I will demonstrate how to use the CLogify class configured with a custom format for logs. We will start with basic examples and progressively add more complexity to illustrate the flexibility of this solution.
1. Basic Configuration of a Log
For this example, we will use the test file LogifyTest.mq5 , created in the first article. The initial configuration involves creating an instance of CLogify and defining the basic format for the log messages. Here is the code:
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} => {msg}")); //--- Log a simple message logify.Debug("Application initialized successfully."); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
When the code is executed, the format of the log generated will be something like this:
[DEBUG] 07:25:32 => Application initialized successfully.
Note that we use a simplified time format ( hh:mm:ss ) and the message is structured to show the log level and the current time. This is the most basic example of using CLogify .
2. Adding Details with Source Identification
Now, let's expand the example to include information such as the log source and additional parameters. This is useful for more complex systems, where logs need to indicate which part of the system is generating the message.
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} {args}")); //--- Log a simple message logify.Debug("Connection established with the server.", "Network"); logify.Alert("Configuration file not found!", "Config", "Attempt 1 of 3"); return(INIT_SUCCEEDED); }This code will produce the following output:
[DEBUG] 07:26:18 (Network) => Connection established with the server. [ALERT] 07:26:19 (Config) => Configuration file not found! Attempt 1 of 3
Here, we add origin parameters and contextual arguments to enrich the log message.
3. Using Advanced Metadata
In more robust systems, it is often necessary to identify the file, function, and line that generated the log. Let's adjust the example to include this information:
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} (File: {filename}, Line: {line})")); //--- Log a simple message logify.Error("Error accessing database.", "Database", "", __FILE__, __FUNCTION__, __LINE__); return(INIT_SUCCEEDED); }When executing the above code, we have:
[ERROR] 07:27:15 (Database) => Error accessing database. (File: LogifyTest.mq5, Line: 25)
Now we have detailed information, allowing us to track exactly where the error occurred in the code.
4. Customizing Formats and Integration into Larger Systems
As a final example, we will show how to customize formats and generate different types of log messages within a loop that simulates the execution of a larger system:
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("yyyy.MM.dd hh:mm:ss","{date_time} [{levelname}] {msg} - {origin} ({filename}:{line})")); //--- Cycle simulating various system operations for(int i=0; i<3; i++) { logify.Debug("Operation in progress...", "TaskProcessor", "", __FILE__, __FUNCTION__, __LINE__); if(i == 1) { logify.Alert("Possible inconsistency detected.", "TaskValidator", "", __FILE__, __FUNCTION__, __LINE__); } if(i == 2) { logify.Fatal("Critical error, purchase order not executed correctly!", "Core", "", __FILE__, __FUNCTION__, __LINE__); } } return(INIT_SUCCEEDED); }Execution will produce the following output:
2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [ALERT] Possible inconsistency detected. - TaskValidator (LogifyTest.mq5:28) 2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [FATAL] Critical error, purchase order not executed correctly! - Core (LogifyTest.mq5:32)
This example simulates a real-world flow of operations, with messages escalated according to the severity of the situation. The log format is highly detailed and shows the complete time, log level, message, and location in the code.
Conclusion
In this article, we comprehensively explore how to customize and apply log formats in MQL5 using the power of the Logify library. We start with an introduction by conceptualizing what a log format is and the importance of this practice in monitoring and debugging applications.
We then cover the basic structure of a formatter, which is the heart of log customization, and how its application in MQL5 can make logs more meaningful and accessible. Based on this, we show how to implement these formatters, highlighting the possibilities of customization and inclusion of additional data in the MqlLogifyModel model.
As we move forward, we look at the process of adding more contextual data to logs to make them informative, such as identifying the exact source of a message (file, function, and line). We also discuss how to configure and apply these formatters to logs to ensure log output that meets the specific needs of any project.
Finally, we conclude with a practical example, where we demonstrate how to implement different levels of complexity in logs, from basic configurations to robust systems with advanced and detailed logs. This example consolidated the learnings, connecting theory to real practice in systems developed in MQL5.
Below is the diagram with the library in its current stage:
All code used in this article is attached below. Here is a table with the description of each library file:
File Name | Description |
---|---|
Experts/Logify/LogiftTest.mq5 | File where we test the library's features, containing a practical example |
Include/Logify/Formatter/LogifyFormatter.mqh | Class responsible for formatting log records, replacing placeholders with specific values |
Include/Logify/Logify.mqh | Core class for log management, integrating levels, models and formatting |
Include/Logify/LogifyLevel.mqh | File that defines the log levels of the Logify library, allowing for detailed control |
Include/Logify/LogifyModel.mqh | Structure that models log records, including details such as level, message, timestamp, and context |





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use