Dominando los registros (Parte 2): Formateo de registros
Introducción
En el primer artículo de esta serie, Dominando los registros (Parte 1): Conceptos fundamentales y primeros pasos en MQL5, nos embarcamos en la creación de una biblioteca de registros personalizada para el desarrollo de un Asesor experto (Expert Advisor, EA). En él, exploramos la motivación detrás de la creación de una herramienta tan esencial: superar las limitaciones de los registros nativos de MetaTrader 5 y aportar una solución robusta, personalizable y potente al universo MQL5.
Para recapitular los puntos principales tratados, sentamos las bases de nuestra biblioteca estableciendo los siguientes requisitos fundamentales:
- Estructura robusta que utiliza el patrón Singleton, lo que garantiza la coherencia entre los componentes del código.
- Persistencia avanzada para almacenar registros en bases de datos, proporcionando un historial rastreable para auditorías y análisis en profundidad.
- Flexibilidad en las salidas, lo que permite almacenar o mostrar los registros de forma cómoda, ya sea en la consola, en archivos, en el terminal o en una base de datos.
- Clasificación por niveles de registro, diferenciando los mensajes informativos de las alertas críticas y los errores.
- Personalización del formato de salida, para satisfacer las necesidades específicas de cada desarrollador o proyecto.
Con esta base bien establecida, quedó claro que el marco de registro que estamos desarrollando será mucho más que un simple registro de eventos; será una herramienta estratégica para comprender, monitorear y optimizar el comportamiento de los EA en tiempo real.
Ahora, en este segundo artículo, abordaremos una de las características más relevantes de esta biblioteca: el formateo de registros. Después de todo, un registro eficiente no solo se refiere a lo que se registra, sino también a cómo se presenta la información. Imagínese recibir mensajes confusos o mal formateados en medio de una prueba de EA importante. Esto no sólo puede hacer que el análisis sea innecesariamente complejo, sino que también puede provocar la pérdida de información valiosa. Queremos que cada registro tenga la claridad y el detalle necesarios, y que al mismo tiempo sea personalizable según las necesidades del desarrollador.
¿Qué es un formato de registro?
Un formato de registro es la estructura que organiza la información registrada durante la ejecución de un programa de forma clara y comprensible. Es un estándar que define cómo se mostrará cada registro, reuniendo datos clave como el nivel de gravedad del evento, la fecha y hora en que ocurrió, la fuente del registro y el mensaje descriptivo asociado.
Esta organización es esencial para que los registros sean legibles y útiles. Sin un formato, los registros pueden parecer confusos y carecer de contexto, lo que dificulta el análisis. Por ejemplo, imagine un registro no estructurado que simplemente diga:
DEBUG: Pedido enviado correctamente, el servidor respondió en 32 ms.Ahora, compárelo con un registro que sigue un formato estructurado, que no solo presenta la misma información, sino que también proporciona contexto:
[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms
Esta pequeña diferencia puede hacer una gran diferencia en el diagnóstico de problemas, especialmente en sistemas complejos. El formato transforma los datos sin procesar en una narrativa cohesiva, guiando al desarrollador en la comprensión del comportamiento del sistema.
La flexibilidad también es un aspecto clave de los formatos de registro. Cada proyecto tiene necesidades específicas, y la capacidad de adaptar el formato permite personalizaciones útiles, como resaltar eventos críticos, rastrear fuentes o enriquecer mensajes con metadatos contextuales.
Estructura básica de un formateador
La estructura básica de un formateador se basa en el concepto de "marcadores de posición", elementos que actúan como puntos de sustitución en una plantilla. Estos marcadores de posición definen dónde y cómo se insertará la información de un registro en un mensaje final.
Piense en un formateador como una máquina que transforma datos sin procesar de un evento de registro en un formato legible y personalizable. La entrada puede ser un modelo de datos que contenga valores como el nivel de gravedad, el mensaje, la hora y otros detalles. Luego, el formateador aplica estos valores a la plantilla proporcionada por el usuario, lo que da como resultado un registro formateado.
Por ejemplo, considere una plantilla como ésta:
{date_time} {levelname}: {msg}Cuando los valores se reemplazan con los marcadores de posición correspondientes, la salida se parece a esto: 12/04/2025 00:25:45 DEBUG: IFR indicator successfully inserted into the chart!
La fortaleza de un formateador reside en su flexibilidad. A continuación se muestran algunos ejemplos de marcadores de posición que agregaremos a la biblioteca:
- {levelname}: Representa el nivel de registro (por ejemplo, DEBUG, ERROR, FATAL) de una forma fácil de entender.
- {msg}: El mensaje que describe el evento registrado.
- {args}: Datos adicionales que contextualizan el mensaje, a menudo utilizados para capturar información dinámica.
- {timestamp}: La marca de tiempo en milisegundos, útil para el análisis de precisión.
- {date_time}: La versión legible por humanos de la marca de tiempo, que muestra la fecha y la hora en un formato estándar para humanos.
- {origin}: Indica el origen del evento, como el módulo o la clase donde se emitió el registro.
- {filename}, {function} and {line}: Identify the exact location in the code where the log was generated, making debugging more efficient.
La lógica detrás de un formateador es simple pero poderosa. Permite crear un marco personalizado para cada mensaje de registro, brindando al desarrollador una forma de presentar la información más relevante para cada situación. Este enfoque es especialmente útil en proyectos donde el volumen de datos puede ser alto y es esencial un análisis rápido.
Con plantillas personalizables y marcadores de posición completos, la biblioteca proporcionará una herramienta altamente modular. Esta modularidad garantizará que pueda crear registros adaptados a las necesidades específicas de su aplicación, aumentando la eficiencia en la interpretación y el uso de los datos registrados.
Implementación de formateadores en MQL5
Ahora que entendemos qué son los formatos y marcadores de posición, vayamos directamente al código para comprender cómo se implementará esto en la etapa actual de la biblioteca. Primero, creemos una nueva carpeta dentro de <Include/Logify> llamada «Formatter». Dentro de esta carpeta, cree un archivo llamado LogifyFormatter.mqh. Al final, la ruta será <Include/Logify/Formatter/LogifyFormatter.mqh>. Recuerda que he adjuntado los archivos finales utilizados en el artículo al final del mismo, solo descárgalos y úsalos. Así es como debería verse el explorador de archivos: 
//+------------------------------------------------------------------+ //| 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) { } //+------------------------------------------------------------------+
Declaramos los atributos privados que almacenan los formatos de fecha y registro.
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); } //+------------------------------------------------------------------+
Aquí definimos:
- 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}")
- Y otros dos métodos para acceder a los atributos privados
El constructor inicializa los formatos de fecha y registro, validando el formato de registro con el método CheckLogFormat que veremos en un momento. Para ello, agregué dos parámetros al constructor, facilitando así la definición del formato justo al crear una instancia de la clase.
//+------------------------------------------------------------------+ //| 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) { } //+------------------------------------------------------------------+
El destructor ( ~CLogifyFormatter ) no realiza ninguna acción específica, pero es una buena práctica declararlo, ya que puede ser útil en situaciones futuras.
Respecto al método CheckLogFormat, lo implementaremos ahora. Desempeña un papel esencial en la validación de las plantillas utilizadas para formatear los registros. Su propósito es garantizar que todos los marcadores de posición dentro de la plantilla estén correctamente estructurados y cerrados antes de ser procesados. Este tipo de validación es esencial para evitar errores inesperados y garantizar la confiabilidad de las salidas del registro. Exploremos algunos ejemplos de formatos no válidos y comprendamos las razones:
- Marcadores de posición sin cerrar: Un ejemplo es {timestamp} - {{msg} . Aquí tenemos un carácter { adicional que no se cerró correctamente. Este tipo de error indica una estructura incompleta, lo que puede provocar fallos en el procesamiento del registro. - Marcadores de posición que no se abren: En casos como {timestamp} - {msg}} , hay un carácter } adicional que no coincide con ningún { . Al igual que en el ejemplo anterior, esto provoca inconsistencias en la estructura del registro.
- Marcadores de posición vacíos: La plantilla {timestamp} - {msg} {} contiene un marcador de posición vacío {} que no tiene una clave asociada. Cada marcador de posición debe rellenarse con una referencia válida que se sustituirá dinámicamente, y las plantillas vacías incumplen esta expectativa.
- Formato vacío: El método también tiene en cuenta los casos no válidos en los que la cadena proporcionada está completamente vacía. Para que sea válida, la cadena debe contener al menos un carácter, que servirá como base para el registro formateado.
Cuando el método identifica cualquiera de estas irregularidades, devuelve falso e imprime mensajes de error detallados para el desarrollador. Estos mensajes ayudan a localizar y corregir rápidamente problemas en la plantilla proporcionada. Por otro lado, si la plantilla está correctamente estructurada y sigue todas las reglas, el método devolverá true, lo que indica que está listo para ser utilizado. Además de garantizar la precisión, el método promueve buenas prácticas en el diseño de plantillas de registro, animando a los desarrolladores a crear estructuras claras y consistentes. Este enfoque no solo mejora la legibilidad de los registros, sino que también hace que sea más fácil mantenerlos y analizarlos a lo largo del tiempo.
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Ahora pasemos a los dos métodos principales de la clase que son responsables de formatear los registros:
- FormatDate(): Para manipular fechas.
- FormatLog(): Para crear el formato del registro en sí mismo.
Ambos desempeñan un papel fundamental en la personalización de los datos registrados por la biblioteca. Para que estos métodos sean extensibles y flexibles, se declaran como virtuales. Dado que los métodos se declaran como virtuales, FormatDate se puede sobrescribir fácilmente en clases derivadas para adaptarse a escenarios específicos. Por ejemplo, una implementación personalizada podría adaptar el formato de fecha para incluir la zona horaria u otra información adicional. Esta arquitectura flexible permite que la biblioteca evolucione con las demandas de los proyectos, garantizando su idoneidad en diversos contextos.
El método FormatDate es responsable de transformar un objeto datetime en una cadena formateada, adaptada al estándar definido en m_date_format . Este patrón utiliza un sistema de marcadores de posición que se reemplazan dinámicamente con los elementos correspondientes de una fecha, como año, mes, día, hora, etc.
Este enfoque es increíblemente flexible y permite formatos altamente personalizados que pueden adaptarse a una variedad de escenarios. Por ejemplo, puede elegir mostrar solo el día y el mes, o incluir información completa como el día de la semana y la hora. A continuación se muestran los marcadores de posición disponibles:
- Año
- yy→ Año de 2 dígitos (por ejemplo, «25»).
- yyyy → Año de 4 dígitos (por ejemplo, «2025»).
- Mes
- M → Mes sin el cero inicial (por ejemplo, «1» para enero).
- MM → Mes de 2 dígitos (por ejemplo, «01» para enero).
- MMM → Abreviatura del mes (por ejemplo, «Ene»).
- MMMM → Nombre completo del mes (por ejemplo, «Enero»).
- Día
- d → Día sin el cero inicial (por ejemplo, «1»).
- dd → Día de 2 dígitos (por ejemplo, «01»).
- Día del año
- D → Día del año sin el cero inicial (por ejemplo, «32» para el 1 de febrero). - DDD → Día del año de tres dígitos (por ejemplo, «032»).
- Día de la semana
- E → Nombre abreviado del día de la semana (por ejemplo, «Lun»).
- EEEE → Nombre completo del día de la semana (por ejemplo, «Lunes»).
- Horas en formato de 24 horas
- H → Hora sin el cero inicial (por ejemplo, «9»).
- HH → Hora de dos dígitos (por ejemplo, «09»).
- Horas en formato de 12 horas
- h → Hora sin el cero inicial (por ejemplo, «9»).
- hh → Hora de dos dígitos (por ejemplo, «09»).
- Minuto
- m → Minuto sin cero inicial (por ejemplo, «5»).
- mm → Minutos de dos dígitos (por ejemplo, «05»).
- Segundo
- s → Segundo sin cero inicial (por ejemplo, «9»).
- ss → Segundo con dos dígitos (por ejemplo, «09»).
- AM/PM
- tt → Devuelve en minúsculas (am/pm)
- TT → Devuelve en mayúsculas (AM/PM)
Esta lógica hace que sea extremadamente conveniente personalizar la visualización de la fecha según sus necesidades. Por ejemplo, utilizando el formato «yyyy-MM-dd HH:mm:ss», el resultado sería algo así como «2025-01-02 14:30:00». Si utilizamos «EEEE, MMM dd, yyyy», el resultado sería «Tuesday, Jul 30, 2019». Esta adaptabilidad es esencial para proporcionar registros que sean informativos y visualmente claros.
La lógica detrás de FormatLog se basa en la función nativa de MQL5 StringReplace(). Esta función realiza sustituciones directas de cadenas, en las que todas las apariciones de una subcadena específica se sustituyen por otra. En el contexto del método FormatLog, los marcadores de posición como {timestamp} o {message} se sustituyen por valores reales del modelo de registro. Esto transforma instancias de un modelo como MqlLogifyModel en datos organizados listos para ser visualizados.
Aquí está el código para la implementación en la clase, he añadido varios comentarios para que el código sea lo más didáctico posible:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Con esto concluye la construcción de la clase, ahora vamos a realizar algunas actualizaciones en MqlLogifyModel.
Añadir más datos a MqlLogifyModel
El MqlLogifyModel es uno de los elementos centrales de nuestra biblioteca de registro, y representa la estructura básica para almacenar y manipular los datos relacionados con cada evento de registro. En su estado actual, la estructura se define de la siguiente manera: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; } };
Esta versión inicial funciona bien en escenarios sencillos, pero podemos mejorarla significativamente añadiendo más información y perfeccionando los nombres de las propiedades para facilitar su uso y alinear el modelo con las mejores prácticas. A continuación, analizaremos las mejoras previstas.
Simplificación del nombre de la propiedad- El campo del mensaje pasará a llamarse msg. Este cambio, aunque sutil, reduce el número de caracteres al acceder a la propiedad, lo que facilita la escritura y lectura del código.
- Los metadatos serán sustituidos por args, ya que el nuevo nombre refleja con mayor precisión la función de la propiedad: almacenar los datos de contexto asociados al registro en el momento en que se genera.
Nuevos campos añadidos
Para enriquecer los registros y permitir un análisis más detallado, se incluirán los siguientes campos:
- formatted: Mensaje de registro formateado según el formato especificado. Se utilizará para almacenar la versión final del registro, sustituyendo los marcadores de posición por valores reales. Esta propiedad será de solo lectura.
- levelname: Nombre textual del nivel de registro (por ejemplo, «DEBUG», «INFO»). Esto resulta útil cuando el formato requiere una descripción del nivel de gravedad.
- date_time: Representa la fecha y hora del evento en formato de fecha y hora, una alternativa a la marca de tiempo.
- filename: El nombre del archivo donde se generó el registro, esencial para rastrear la fuente exacta.
- function: Nombre de la función en la que se llamó al registro, lo que proporciona más contexto sobre el origen del evento.
- line: Número de línea en el archivo fuente donde se generó el registro. Esto puede resultar especialmente útil en situaciones de depuración.
Estos nuevos campos hacen que MqlLogifyModel sea más robusto y esté preparado para satisfacer diferentes requisitos de registro, como la depuración detallada o la integración con herramientas de supervisión externas. Después de los cambios, el código queda así:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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; } }; //+------------------------------------------------------------------+Como siguiente paso, actualizaremos el método FormatLog de la clase CLogifyFormatter para incluir compatibilidad con los marcadores de posición correspondientes a las nuevas propiedades. A continuación se muestra la versión actualizada del método, ahora compatible con todas las nuevas propiedades del modelo:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
En el método FormatLog, el valor de {date_time} se sustituye por el resultado formateado de FormatDate, por lo que este marcador de posición se sustituye por el formato de fecha pasado anteriormente.
Aplicación de formateadores a los registros
Asegurémonos de que nuestra clase CLogify pueda utilizar un formateador para dar formato a los mensajes de registro. Importemos la clase responsable de esto y añadamos un atributo para almacenarlo:
#include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh"
El siguiente paso es añadir el atributo m_formatter a la clase CLogify para almacenar la instancia del formateador y crear los métodos para configurarlo y acceder a él. Esto nos permitirá reutilizar el formateador en diferentes puntos del sistema:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Los métodos de registro existentes tienen un conjunto limitado de parámetros, como la marca de tiempo, el nivel de registro, el mensaje, la fuente y los metadatos. Mejoraremos este enfoque añadiendo nuevos parámetros que describan el contexto del registro, tales como:
- filename: Nombre del archivo donde se produjo el registro.
- function: Nombre de la función en la que se produjo el registro.
- line: Línea de código que generó el registro.
Además, para que la firma del método sea más intuitiva, ajustaremos los nombres de los parámetros. A continuación se muestra un ejemplo de cómo se modificó el método principal (Append):
bool CLogify::Append(ulong timestamp, ENUM_LOG_LEVEL level, string message, string origin = "", string metadata = "");
Método ajustado:
bool CLogify::Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0);
Con eso, nuestra nueva implementación es:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Observe que estoy imprimiendo el valor de retorno de la función FormatLog, que devuelve el objeto de datos.
Ahora que tenemos el método Append básico funcionando con parámetros detallados, podemos ajustar o añadir otros métodos especializados para cada nivel de registro (Debug, Infor, Alert, Error y Fatal). Estos métodos son «atajos» que establecen automáticamente el nivel de registro, al tiempo que permiten el uso de todos los demás parámetros.
//+------------------------------------------------------------------+ //| 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)); } //+------------------------------------------------------------------+
Ejemplo práctico
Demostraré cómo utilizar la clase CLogify configurada con un formato personalizado para los registros. Comenzaremos con ejemplos básicos y añadiremos progresivamente más complejidad para ilustrar la flexibilidad de esta solución.
1. Configuración básica de un registro
Para este ejemplo, utilizaremos el archivo de prueba LogifyTest.mq5, creado en el primer artículo. La configuración inicial consiste en crear una instancia de CLogify y definir el formato básico de los mensajes de registro. Aquí está el código:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Cuando se ejecute el código, el formato del registro generado será similar al siguiente:
[DEBUG] 07:25:32 => Application initialized successfully.
Tenga en cuenta que utilizamos un formato de hora simplificado ( hh:mm:ss ) y que el mensaje está estructurado para mostrar el nivel de registro y la hora actual. Este es el ejemplo más básico del uso de CLogify.
2. Añadir detalles con identificación de la fuente
Ahora, ampliemos el ejemplo para incluir información como la fuente del registro y parámetros adicionales. Esto resulta útil para sistemas más complejos, en los que los registros deben indicar qué parte del sistema está generando el mensaje.
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); }Este código producirá el siguiente resultado:
[DEBUG] 07:26:18 (Network) => Connection established with the server. [ALERT] 07:26:19 (Config) => Configuration file not found! Attempt 1 of 3
Aquí, añadimos parámetros de origen y argumentos contextuales para enriquecer el mensaje de registro.
3. Uso de metadatos avanzados
En sistemas más robustos, a menudo es necesario identificar el archivo, la función y la línea que generaron el registro. Ajustemos el ejemplo para incluir esta información:
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); }Al ejecutar el código anterior, obtenemos:
[ERROR] 07:27:15 (Database) => Error accessing database. (File: LogifyTest.mq5, Line: 25)
Ahora disponemos de información detallada que nos permite rastrear exactamente dónde se produjo el error en el código.
4. Personalización de formatos e integración en sistemas más grandes
Como último ejemplo, mostraremos cómo personalizar formatos y generar diferentes tipos de mensajes de registro dentro de un bucle que simula la ejecución de un sistema más grande:
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); }La ejecución producirá el siguiente resultado:
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)
Este ejemplo simula un flujo de operaciones del mundo real, con mensajes escalados según la gravedad de la situación. El formato del registro es muy detallado y muestra la hora completa, el nivel del registro, el mensaje y la ubicación en el código.
Conclusión
En este artículo, exploramos exhaustivamente cómo personalizar y aplicar formatos de registro en MQL5 utilizando el poder de la biblioteca Logify. Comenzamos con una introducción conceptualizando qué es un formato de registro y la importancia de esta práctica en el monitoreo y depuración de aplicaciones.
A continuación, abordamos la estructura básica de un formateador, que es el núcleo de la personalización de registros, y cómo su aplicación en MQL5 puede hacer que los registros sean más significativos y accesibles. Basándonos en esto, mostramos cómo implementar estos formateadores, destacando las posibilidades de personalización e inclusión de datos adicionales en el modelo MqlLogifyModel.
A medida que avanzamos, analizamos el proceso de añadir más datos contextuales a los registros para que sean más informativos, como identificar la fuente exacta de un mensaje (archivo, función y línea). También analizamos cómo configurar y aplicar estos formateadores a los registros para garantizar que la salida de registros satisfaga las necesidades específicas de cualquier proyecto.
Por último, concluimos con un ejemplo práctico en el que mostramos cómo implementar diferentes niveles de complejidad en los registros, desde configuraciones básicas hasta sistemas robustos con registros avanzados y detallados. Este ejemplo consolidó los conocimientos adquiridos, conectando la teoría con la práctica real en los sistemas desarrollados en MQL5.
A continuación se muestra el diagrama con la biblioteca en su estado actual:

Todo el código utilizado en este artículo se adjunta a continuación. Aquí hay una tabla con la descripción de cada archivo de biblioteca:
| Nombre del archivo | Descripción |
|---|---|
| Experts/Logify/LogiftTest.mq5 | Archivo donde probamos las funcionalidades de la librería, conteniendo un ejemplo práctico. |
| Include/Logify/Formatter/LogifyFormatter.mqh | Clase responsable de formatear los registros de registro, reemplazando marcadores de posición con valores específicos. |
| Include/Logify/Logify.mqh | Clase principal para la gestión de registros, integrando niveles, modelos y formato. |
| Include/Logify/LogifyLevel.mqh | Archivo que define los niveles de registro de la biblioteca Logify, lo que permite un control detallado. |
| Include/Logify/LogifyModel.mqh | Estructura que modela los registros de registro, incluidos detalles como el nivel, el mensaje, la marca de tiempo y el contexto. |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/16833
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Cambiando a MQL5 Algo Forge (Parte 4): Trabajamos con versiones y lanzamientos
Métodos de conjunto para mejorar las tareas de clasificación en MQL5
Características del Wizard MQL5 que debe conocer (Parte 52): Accelerator Oscillator (AC)
Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 3): Estrategias dinámicas de seguimiento de tendencias y reversión a la media
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso