Dominando los registros (Parte 5): Optimizar el controlador con caché y rotación
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 log personalizada para el desarrollo de Experts Advisors (EAs). 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 brindar una solución robusta, personalizable y poderosa 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, permitiendo almacenar o visualizar los logs cómodamente, ya sea en la consola, en archivos, en la terminal o en una base de datos.
- Clasificación por niveles de registro, diferenciando mensajes informativos de alertas críticas y errores.
- Personalización del formato de salida, para satisfacer las necesidades únicas 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.
Hasta ahora, hemos explorado los conceptos básicos de los registros, hemos aprendido a darles formato y hemos comprendido cómo los controladores controlan el destino de los mensajes. En el último artículo, aprendimos cómo guardar registros en un archivo (.txt, .log o .json). Ahora, en este quinto artículo, optimizaremos el proceso de guardar registros en archivos implementando el almacenamiento en caché y la rotación de archivos. ¡Comencemos!
Agregar un formateador a cada controlador
Hasta ahora, nuestra biblioteca de registro administra el formato de los mensajes a través de una única instancia de la clase CFormatter, que está centralizada en la base de la biblioteca (CLogify). Este enfoque funciona bien para escenarios simples, pero limita la flexibilidad de los controladores.
El problema es que al utilizar un único formateador global, todos los controladores comparten el mismo formato, lo que puede no ser ideal cuando diferentes objetivos requieren un formato diferente. Por ejemplo, mientras que un controlador que escribe registros en JSON puede necesitar una estructura específica, un controlador que imprime registros en la consola puede requerir un formato más legible para humanos. La solución es trasladar la responsabilidad del formateador a la clase base del controlador (CLogifyHandler). De esta manera, cada controlador puede tener su propio formateador independiente, lo que permite un mayor control sobre el formato de los mensajes de registro. Implementemos este cambio y veamos cómo mejora la flexibilidad de la biblioteca.
Yendo directo al código, comenzamos agregando una instancia de CFormatter dentro de CLogifyHandler, como es una tarea sencilla para ti que has leído los artículos anteriores, solo agregaré el código final resaltando lo agregado:
//+------------------------------------------------------------------+ //| LogifyHandler.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" #include "../Formatter/LogifyFormatter.mqh" //+------------------------------------------------------------------+ //| class : CLogifyHandler | //| | //| [PROPERTY] | //| Name : CLogifyHandler | //| Heritage : No heritage | //| Description : Base class for all log handlers. | //| | //+------------------------------------------------------------------+ class CLogifyHandler { protected: string m_name; ENUM_LOG_LEVEL m_level; CLogifyFormatter *m_formatter; public: CLogifyHandler(void); ~CLogifyHandler(void); //--- Handler methods virtual void Emit(MqlLogifyModel &data); // Processes a log message and sends it to the specified destination virtual void Flush(void); // Clears or completes any pending operations virtual void Close(void); // Closes the handler and releases any resources //--- Set/Get void SetLevel(ENUM_LOG_LEVEL level); void SetFormatter(CLogifyFormatter *format); string GetName(void); ENUM_LOG_LEVEL GetLevel(void); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandler::CLogifyHandler(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandler::~CLogifyHandler(void) { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter ; } } //+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandler::Emit(MqlLogifyModel &data) { } //+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandler::Flush(void) { } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandler::Close(void) { } //+------------------------------------------------------------------+ //| Set level | //+------------------------------------------------------------------+ void CLogifyHandler::SetLevel(ENUM_LOG_LEVEL level) { m_level = level; } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogifyHandler::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get name | //+------------------------------------------------------------------+ string CLogifyHandler::GetName(void) { return(m_name); } //+------------------------------------------------------------------+ //| Get level | //+------------------------------------------------------------------+ ENUM_LOG_LEVEL CLogifyHandler::GetLevel(void) { return(m_level); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogifyHandler::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
Continuando con los cambios más simples, eliminamos la instancia CFormatter en CLogify, las partes que se eliminaron de la clase están resaltadas en rojo y las que se agregaron están resaltadas en verde:
//+------------------------------------------------------------------+ //| Logify.mqh | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" #property version "1.00" #include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh" #include "Handlers/LogifyHandler.mqh" #include "Handlers/LogifyHandlerConsole.mqh" #include "Handlers/LogifyHandlerDatabase.mqh" #include "Handlers/LogifyHandlerFile.mqh" //+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifyFormatter *m_formatter; CLogifyHandler *m_handlers[]; public: CLogify(); ~CLogify(); //--- Handler void AddHandler(CLogifyHandler *handler); bool HasHandler(string name); CLogifyHandler *GetHandler(string name); CLogifyHandler *GetHandler(int index); int SizeHandlers(void); //--- Generic method for adding logs bool Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Specific methods for each log level bool Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Get/Set object formatter void SetFormatter(CLogifyFormatter *format); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogify::CLogify() { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogify::~CLogify() { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter; } //--- Delete handlers int size_handlers = ArraySize(m_handlers); for(int i=0;i<size_handlers;i++) { delete m_handlers[i]; } } //+------------------------------------------------------------------+ //| Add handler to handlers array | //+------------------------------------------------------------------+ void CLogify::AddHandler(CLogifyHandler *handler) { int size = ArraySize(m_handlers); ArrayResize(m_handlers,size+1); m_handlers[size] = GetPointer(handler); } //+------------------------------------------------------------------+ //| Checks if handler is already in the array by name | //+------------------------------------------------------------------+ bool CLogify::HasHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(true); } } return(false); } //+------------------------------------------------------------------+ //| Get handler by name | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(m_handlers[i]); } } return(NULL); } //+------------------------------------------------------------------+ //| Get handler by index | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(int index) { return(m_handlers[index]); } //+------------------------------------------------------------------+ //| Gets the total size of the handlers array | //+------------------------------------------------------------------+ int CLogify::SizeHandlers(void) { return(ArraySize(m_handlers)); } //+------------------------------------------------------------------+ //| 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); data.formated = m_formatter.FormatLog(data); //--- Call handlers int size = this.SizeHandlers(); for(int i=0;i<size;i++) { data.formated = m_handlers[i].GetFormatter().FormatLog(data); m_handlers[i].Emit(data); } return(true); } //+------------------------------------------------------------------+ //| 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)); } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogify::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogify::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
La única parte que se agregó fue al formatear el mensaje. Antes, usábamos el formateador dentro de la propia clase. Con los cambios, en cada manejador usamos el formateador proporcionado por el manejador. Al asociar un formateador directamente con cada controlador, eliminamos la restricción de un solo formato y hacemos que la biblioteca sea más adaptable a diferentes necesidades. Ahora, cada destino puede tener un estilo de registro específico, lo que garantiza que la salida sea más apropiada para el contexto en el que se utilizará. En el siguiente tema veremos cómo gestionar la ejecución de logs en ciclos programados con la clase CIntervalWatcher, que será una clase auxiliar para la rotación de archivos.
Creación de la clase CIntervalWatcher
El objetivo principal de CIntervalWatcher es comprobar si ha transcurrido un determinado intervalo de tiempo desde la última llamada. Esto es esencial para generar registros que deben verificarse en intervalos de tiempo específicos. Ya sea para evitar la sobrecarga de escritura o para estructurar mejor los registros, es esencial un mecanismo de control de ciclo, evitando procesamientos innecesarios en cada tick. Permite configurar:
- El intervalo de tiempo que se va a monitorizar (en segundos).
- La fuente de tiempo (hora actual, GMT, servidor local o comercial).
- Si debe devolver verdadero en la primera ejecución.
De esta manera, la clase es útil para verificar cuándo ejecutar una acción periódica dentro de la biblioteca. Creemos una nueva carpeta llamada Utils, que contendrá este archivo. Al final, el explorador de archivos debería verse así:

Pasando a la construcción de la clase, primero creamos una enumeración para admitir diferentes fuentes de tiempo, la llamamos ENUM_TIME_ORIGIN
//+------------------------------------------------------------------+ //| Enum for different time sources | //+------------------------------------------------------------------+ enum ENUM_TIME_ORIGIN { TIME_ORIGIN_CURRENT = 0, // [0] Current Time TIME_ORIGIN_GMT, // [1] GMT Time TIME_ORIGIN_LOCAL, // [2] Local Time TIME_ORIGIN_TRADE_SERVER // [3] Server Time }; //+------------------------------------------------------------------+
Agregamos variables privadas a la clase para almacenar el último instante registrado (m_last_time), el intervalo de tiempo deseado (m_interval), el origen del tiempo (m_time_origin) y una bandera (m_first_return) para controlar el primer retorno. Como consecuencia, creamos un Set y un Get para cada atributo privado. Para facilitar la configuración de intervalos, origen del tiempo y primer retorno, decidí crear algunos constructores adicionales para la clase, ayudando al desarrollador. A continuación se muestra el código con constructores y métodos para acceder y obtener datos privados.
//+------------------------------------------------------------------+ //| class : CIntervalWatcher | //| | //| [PROPERTY] | //| Name : CIntervalWatcher | //| Type : Report | //| Heritage : No heredirary. | //| Description : Monitoring new time periods | //| | //+------------------------------------------------------------------+ class CIntervalWatcher { private: //--- Auxiliary attributes ulong m_last_time; ulong m_interval; ENUM_TIME_ORIGIN m_time_origin; bool m_first_return; public: CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(void); ~CIntervalWatcher(void); //--- Setters void SetInterval(ENUM_TIMEFRAMES interval); void SetInterval(ulong interval); void SetTimeOrigin(ENUM_TIME_ORIGIN time_origin); void SetFirstReturn(bool first_return); //--- Getters ulong GetInterval(void); ENUM_TIME_ORIGIN GetTimeOrigin(void); bool GetFirstReturn(void); ulong GetLastTime(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = PeriodSeconds(interval); m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = interval; m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(void) { m_interval = 10; // 10 seconds m_time_origin = TIME_ORIGIN_CURRENT; m_first_return = true; m_last_time = 0; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CIntervalWatcher::~CIntervalWatcher(void) { } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ENUM_TIMEFRAMES interval) { m_interval = PeriodSeconds(interval); } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ulong interval) { m_interval = interval; } //+------------------------------------------------------------------+ //| Set time origin | //+------------------------------------------------------------------+ void CIntervalWatcher::SetTimeOrigin(ENUM_TIME_ORIGIN time_origin) { m_time_origin = time_origin; } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ void CIntervalWatcher::SetFirstReturn(bool first_return) { m_first_return=first_return; } //+------------------------------------------------------------------+ //| Get interval | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetInterval(void) { return(m_interval); } //+------------------------------------------------------------------+ //| Get time origin | //+------------------------------------------------------------------+ ENUM_TIME_ORIGIN CIntervalWatcher::GetTimeOrigin(void) { return(m_time_origin); } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ bool CIntervalWatcher::GetFirstReturn(void) { return(m_first_return); } //+------------------------------------------------------------------+ //| Set last time | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetLastTime(void) { return(m_last_time); } //+------------------------------------------------------------------+
Para ayudar con el método principal, creemos la función GetTime que devuelve la hora en función del origen definido:
//+------------------------------------------------------------------+ //| Get time in miliseconds | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetTime(ENUM_TIME_ORIGIN time_origin) { switch(time_origin) { case(TIME_ORIGIN_CURRENT): return(TimeCurrent()); case(TIME_ORIGIN_GMT): return(TimeGMT()); case(TIME_ORIGIN_LOCAL): return(TimeLocal()); case(TIME_ORIGIN_TRADE_SERVER): return(TimeTradeServer()); } return(0); } //+------------------------------------------------------------------+
El método más importante de la clase es Inspect(), que verifica si se ha alcanzado el intervalo definido. La lógica es la siguiente: en la primera llamada, verifica si m_last_time es cero (clase recién instanciada), la función almacena la hora actual y devuelve m_first_return. Si la marca de tiempo almacenada es diferente de la marca de tiempo actual más el intervalo, significa que se ha alcanzado el intervalo, por lo que m_last_time se actualiza y la función devuelve verdadero. Si la marca de tiempo es la misma, significa que aún no se ha alcanzado, por lo que la función devuelve falso.
//+------------------------------------------------------------------+ //| Check if there was an update | //+------------------------------------------------------------------+ bool CIntervalWatcher::Inspect(void) { //--- Get time ulong time_current = this.GetTime(m_time_origin); //--- First call, initial return if(m_last_time == 0) { m_last_time = time_current; return(m_first_return); } //--- Check interval if(time_current >= m_last_time + m_interval) { m_last_time = time_current; return(true); } return(false); } //+------------------------------------------------------------------+
Con CIntervalWatcher, tenemos un control más preciso sobre la generación de registros, lo que permite ciclos programables y una mayor eficiencia de procesamiento. Este tipo de enfoque será esencial para una biblioteca de registro que requiera la ejecución periódica de tareas. Ahora, con la ejecución periódica de acciones de registro configurada, podemos centrarnos en optimizar el proceso de grabación y mantener el rendimiento del sistema.
Optimización del almacenamiento de registros: almacenamiento en caché y rotación de archivos
Aunque la grabación directa de registros en archivos que implementamos en el último artículo es una solución funcional, puede volverse ineficiente a medida que crece el volumen de registros. Para evitar el impacto negativo en el rendimiento, es necesario optimizar este proceso. En este tema, exploraremos cómo implementar un sistema de almacenamiento en caché y rotación de archivos para garantizar que los registros se escriban de manera eficiente, sin sobrecargar el almacenamiento y manteniendo la integridad de los datos.
En el último artículo analizamos con más detalle cómo funcionan estas mejoras y cuáles son sus ventajas:
“Imagínese este escenario: un Asesor Experto funcionando durante semanas o meses, registrando cada evento, error o notificación en el mismo archivo. Pronto, ese registro comienza a alcanzar tamaños considerables, lo que hace que la lectura y la interpretación de la información sean bastante complejas. Aquí es donde entra en juego la rotación. Nos permite dividir esta información en trozos más pequeños y organizados, haciendo todo mucho más fácil de leer y analizar.
Las dos formas más comunes de hacerlo son:
- Por tamaño: Establece un límite de tamaño, generalmente en megabytes (MB), para el archivo de registro. Cuando se alcanza este límite, se crea automáticamente un nuevo archivo y el ciclo comienza de nuevo. Este enfoque es muy práctico cuando el objetivo es controlar el crecimiento del tronco, sin tener que ceñirse a un calendario. Tan pronto como el archivo actual alcanza el límite de tamaño (en megabytes), se produce el siguiente flujo: se cambia el nombre del archivo de registro actual y se obtiene un índice, como "log1.log". Los archivos existentes en el directorio también se renumeran, por ejemplo, "log1.log" pasa a ser "log2.log". Si el número de archivos alcanza el máximo permitido, se eliminan los archivos más antiguos. Este enfoque es útil para limitar tanto el espacio ocupado por los registros como la cantidad de archivos guardados.
- Por fecha: En este caso, se crea un nuevo archivo de registro cada día. Cada uno tiene en su nombre la fecha en que fue creado, por ejemplo log_2025-01-19.log, lo que ya soluciona gran parte del dolor de cabeza de organizar logs. Este enfoque es perfecto cuando necesitas echar un vistazo específico a un día en particular, sin perderte en un único archivo gigantesco. Este es el método que más uso al guardar mis registros de Expert Advisors, todo es más limpio, más directo y más fácil de navegar.
Además, también puedes limitar el número de archivos de registro almacenados. Este control es muy importante para evitar la acumulación innecesaria de registros antiguos. Imagina que lo configuras para mantener los 30 archivos más recientes. Cuando aparece el día 31, el sistema descarta automáticamente el más antiguo, lo que evita que se acumulen registros muy antiguos en el disco y se conservan los más recientes.
Otro detalle crucial es el uso de la cache. En lugar de escribir cada mensaje directamente en el archivo tan pronto como llega, los mensajes se almacenan temporalmente en la memoria caché. Cuando la memoria caché alcanza un límite definido, vuelca todo el contenido del archivo a la vez. “Esto da como resultado menos operaciones de lectura y escritura en el disco, mayor rendimiento y una vida útil más larga para sus dispositivos de almacenamiento”.
Para implementar la rotación de archivos de registro, primero necesitamos un método auxiliar llamado SearchForFilesInDirectory(). Este método es responsable de buscar todos los archivos presentes en un directorio específico y devolver sus nombres en una matriz. Se utiliza la función FileFindFirst() para iniciar la búsqueda y, a medida que encuentra archivos, sus nombres se agregan a esta matriz. Una vez completado el proceso, el método cierra el controlador de búsqueda utilizando FileFindClose().
Pero, ¿por qué es tan importante este método? ¡Es simple! Nos permite listar los archivos de registro existentes, garantizando que la clase que administra los registros elimine los archivos más antiguos cuando sea necesario.
class CLogifyHandlerFile : public CLogifyHandler { private: bool SearchForFilesInDirectory(string directory, string &file_names[]); }; //+------------------------------------------------------------------+ //| Returns an array with the names of all files in the directory | //+------------------------------------------------------------------+ bool CLogifyHandlerFile::SearchForFilesInDirectory(string directory,string &file_names[]) { //--- Search for all log files in the specified directory with the given file extension string file_name; long search_handle = FileFindFirst(directory,file_name); ArrayFree(file_names); bool is_found = false; if(search_handle != INVALID_HANDLE) { do { //--- Add each file name found to the array of file names int size_file = ArraySize(file_names); ArrayResize(file_names,size_file+1); file_names[size_file] = file_name; is_found = true; } while(FileFindNext(search_handle,file_name)); FileFindClose(search_handle); } return(is_found); } //+------------------------------------------------------------------+
Ahora que tenemos la función para obtener los archivos, podemos incorporarla al método principal responsable de emitir los registros, Emit(). Dependiendo de la configuración de rotación elegida, se ajustará la lógica.
Si la rotación de registros está configurada para que se realice en función del tamaño del archivo, la función:
- Comprueba si el tamaño del archivo ha excedido el límite configurado (m_config.max_file_size_mb).
- Busca todos los archivos de registro en el directorio.
- Elimina archivos antiguos que exceden el número máximo permitido (m_config.max_file_count).
- Renombra archivos antiguos, incrementando numéricamente sus índices (log1.txt, log2.txt, etc.).
- Cambia el nombre del archivo de registro actual a "log1" para mantener la secuencia.
Si la rotación se basa en la fecha, la función:
- Busca todos los archivos de registro en el directorio.
- Elimina los archivos más antiguos que exceden el número máximo permitido (m_config.max_file_count).
Ahora, veamos la implementación del método Emit() con ambas lógicas de rotación:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerFile::Emit(MqlLogifyModel &data) { //--- Checks if the configured level allows if(data.level >= this.GetLevel()) { //--- Get the full path of the file string log_path = this.LogPath(); //--- Open file ResetLastError(); int handle_file = m_file.Open(log_path, FILE_READ | FILE_WRITE | FILE_ANSI); if(handle_file == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Asegúrese de que el directorio exista y se pueda escribir en él. (Code: "+IntegerToString(GetLastError())+")"); return; } //--- Write m_file.Seek(0, SEEK_END); m_file.WriteString(data.formated + "\n"); //--- Size in megabytes ulong size_mb = m_file.Size() / (1024 * 1024); //--- Close file m_file.Close(); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } } //+------------------------------------------------------------------+
Guardar por bloques para un mejor rendimiento
Pasando a otra mejora, vamos a crear la lógica que considero más interesante del artículo, guardar registros por bloques. La idea central es implementar un caché (memoria temporal), donde se almacenarán los registros hasta que alcancen un límite definido. Cuando se alcanza este límite, todos los registros de la caché se guardan en el archivo de registro a la vez.
Implementaremos esta lógica en pasos. Primero, crearemos la estructura de caché en la clase CLogifyHandlerFile. En la sección privada de la clase, agregaremos una matriz de tipo MqlLogifyModel para almacenar temporalmente los registros de registro. También incluimos una variable para controlar el índice actual del último valor guardado en la caché. Cada vez que se agrega un nuevo registro, este índice se incrementará. También creamos una instancia de la clase CIntervalWatcher y establecemos un intervalo de un día en el constructor. Mira cómo se ve:
class CLogifyHandlerFile : public CLogifyHandler { private: //--- Update utilities CIntervalWatcher m_interval_watcher; //--- Cache data MqlLogifyModel m_cache[]; int m_index_cache; }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::CLogifyHandlerFile(void) { m_interval_watcher.SetInterval(PERIOD_D1); ArrayFree(m_cache); m_index_cache = 0; } //+------------------------------------------------------------------+
Con la estructura de caché y actualización creada, pasamos al siguiente paso: modificar el método Emit() para utilizar el caché.
El método Emit() es responsable de procesar un mensaje de registro y enviarlo al destino configurado (en este caso, un archivo). Lo adaptaremos para que, en lugar de guardar los datos directamente en el archivo, los almacene temporalmente en la caché. Cuando el caché alcanza su límite configurado, o el intervalo definido (un día), el método llama a la función Flush(), que guarda los registros acumulados en el archivo. Este intervalo es útil porque si los datos han estado almacenados en caché durante más de un día, este mecanismo garantiza que los datos se guarden diariamente y también permite que la rutina de rotación se ejecute todos los días.
Aquí está el código modificado:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerFile::Emit(MqlLogifyModel &data) { //--- Checks if the configured level allows if(data.level >= this.GetLevel()) { //--- Resize cache if necessary int size = ArraySize(m_cache); if(size != m_config.messages_per_flush) { ArrayResize(m_cache, m_config.messages_per_flush); size = m_config.messages_per_flush; } //--- Add log to cache m_cache[m_index_cache++] = data; //--- Flush if cache limit is reached or update condition is met if(m_index_cache >= m_config.messages_per_flush || m_interval_watcher.Inspect()) { //--- Save cache Flush(); //--- Reset cache m_index_cache = 0; for(int i=0;i<size;i++) { m_cache[i].Reset(); } } } } //+------------------------------------------------------------------+
La función Flush() es responsable de guardar los datos de caché en el archivo. Este proceso implica abrir el archivo, posicionar el puntero al final y escribir todos los registros almacenados en la caché.
//+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Flush(void) { //--- Get the full path of the file string log_path = this.LogPath(); //--- Open file ResetLastError(); int handle_file = FileOpen(log_path, FILE_READ|FILE_WRITE|FILE_ANSI, '\t', m_config.codepage); if(handle_file == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Asegúrese de que el directorio exista y se pueda escribir en él. (Code: "+IntegerToString(GetLastError())+")"); return; } //--- Loop through all cached messages int size = ArraySize(m_cache); for(int i=0;i<size;i++) { if(m_cache[i].timestamp > 0) { //--- Point to the end of the file and write the message FileSeek(handle_file, 0, SEEK_END); FileWrite(handle_file, m_cache[i].formated); } } //--- Size in megabytes ulong size_mb = FileSize(handle_file) / (1024 * 1024); //--- Close file FileClose(handle_file); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } //+------------------------------------------------------------------+
Con esta implementación, hemos creado una solución de registro eficiente y escalable, capaz de manejar grandes volúmenes de datos sin comprometer el rendimiento de su experto. Por último, debemos asegurarnos de que cuando se cierre el programa, todos los datos almacenados en caché se guarden en el archivo. Para hacer esto, simplemente llame al método Flush() en el método Close(), que ya se llama en el destructor de la clase base CLogify.
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::~CLogifyHandlerFile(void) { this.Close(); } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Close(void) { //--- Save cache Flush(); } //+------------------------------------------------------------------+
Al implementar el almacenamiento en caché y la rotación de archivos, reducimos la cantidad de operaciones de escritura en el disco y garantizamos que nuestros registros se almacenen de manera más eficiente. Esto le da a nuestra biblioteca rendimiento y escalabilidad, haciéndola más robusta para aplicaciones reales. Pero ¿realmente estas optimizaciones marcan la diferencia? Vamos a probarlo.
Pruebas de rendimiento: medición de la eficiencia de las mejoras
Ahora que hemos implementado las optimizaciones, necesitamos medir su impacto real. Las pruebas de rendimiento nos ayudarán a comprender si el caché está reduciendo la carga de escritura y si la rotación de archivos está funcionando como se espera. Para ello, ejecutaremos la misma prueba realizada en el artículo anterior, comparando la versión original de la librería con la versión optimizada.
Para ejecutar la prueba, utilizaremos el mismo archivo, con algunas modificaciones en el formateador, ya que ahora cada controlador tiene su propio formateador. Los cambios se destacan a continuación:
- Verde: Añadidos al código
- Rojo: Eliminaciónes
- Amarillo: El parámetro que define el tamaño de la caché. Cuanto mayor sea la caché, más rápido será el procesamiento.
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,10); //--- Handler File CLogifyHandlerFile *handler_file = new CLogifyHandlerFile(); handler_file.SetConfig(m_config); handler_file.SetLevel(LOG_LEVEL_DEBUG); handler_file.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Add handler in base class logify.AddHandler(handler_file); logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Comencemos una prueba en el Probador de estrategias usando los mismos parámetros de fecha y símbolo.

Utilizando el modelo "OHLC por 1 minuto" en el símbolo EURUSD y un marco de tiempo de 7 días, el tiempo de ejecución fue de 26 segundos. Vale la pena señalar que con cada tic, se genera un nuevo registro y el caché está configurado para almacenar 10 mensajes. Ahora, aumentemos el caché a 100 mensajes y observemos la diferencia en el rendimiento:

Con este cambio, pudimos reducir el tiempo de prueba en 2 segundos, manteniendo la misma configuración de modelado, fecha y símbolos. Si lo comparamos con la primera prueba realizada en el artículo anterior, que tardó 5 minutos y 11 segundos, ¡la mejora es impresionante!
Los resultados demuestran que pequeñas optimizaciones pueden generar ganancias significativas en eficiencia. La combinación de almacenamiento en caché y rotación de archivos hace que la gestión de registros sea más ágil y confiable, validando las elecciones realizadas hasta el momento. Pero ¿cómo se pueden aplicar estas mejoras en la práctica? Exploremos algunos ejemplos de uso.
Ejemplos de uso de la biblioteca de registros
Ahora que hemos mejorado nuestra biblioteca de registros, ¡es hora de ponerla en acción! Exploremos ejemplos prácticos de cómo usarlo para crear diferentes tipos de archivos de registro, cada uno con su propio formato y nivel de gravedad.
Ejemplo 1: Separar los registros en archivos .log y .json
En el primer escenario, configuramos dos archivos de registro: uno en formato .log y el otro en formato .json. Cada uno tiene un formato específico y un nivel de gravedad diferente, lo que facilita la gestión y el análisis de los registros.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ALERT); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Aquí, utilizamos la misma variable de configuración m_config, cambiando solo los valores necesarios para definir ambos formatos de registro. Esto hace que la configuración sea más sencilla y reutilizable.
Ejemplo 2: Almacenar solo errores en un archivo JSON
Ahora, vayamos un paso más allá y configuremos un registro específico para almacenar solo mensajes de error. Para ello, creamos una carpeta aparte donde se guardará este archivo .json. Además, agregamos un controlador de consola para mostrar los registros directamente en la terminal.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs\\error",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ERROR); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Handler Console CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole(); handler_console.SetLevel(LOG_LEVEL_DEBUG); handler_console.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname} | {origin}] {msg}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); logify.AddHandler(handler_console); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
En este ejemplo, utilizamos tres controladores de registro:
- Archivo .log → Almacena registros en formato tradicional.
- Archivo .json → Almacena solo mensajes de error dentro de una carpeta separada.
- Consola → Muestra los registros de una manera más legible para el usuario.
El uso de un formateador más "humano" en la consola ayuda a que la salida sea más comprensible, mientras que el JSON de errores hace que sea más fácil analizarlo más tarde.
Con estos ejemplos queda claro cómo se puede aplicar nuestra biblioteca de registro en proyectos reales. La flexibilidad para crear diferentes formatos y niveles de severidad permite una buena gestión, ayudando a identificar y solucionar problemas más fácilmente. Además, la estructura modular facilita la ampliación del sistema de registro según sea necesario.
¡Ahora todo lo que tienes que hacer es adaptar esta implementación a tus necesidades y asegurarte de que tus registros estén siempre bien organizados y accesibles!
Conclusión
En este artículo, hemos evolucionado nuestra biblioteca de registro, haciéndola más eficiente, escalable y adaptable. Hemos refinado el formato, permitiendo que cada controlador tenga su propio formateador, haciendo que los mensajes sean más organizados y flexibles para diferentes necesidades, como la depuración y auditoría local.
Hemos implementado la clase CIntervalWatcher, que controla los ciclos de ejecución, garantizando que los registros se escriban y roten en intervalos bien definidos. También hemos optimizado la escritura con almacenamiento en caché, reduciendo las operaciones de disco y gestionando mejor el crecimiento de los archivos. Hemos validado estas mejoras con pruebas de rendimiento, perfeccionando aún más la solución para soportar cargas elevadas. Además, hemos presentado ejemplos prácticos para facilitar la adopción de la biblioteca.
Si hay una lección principal que se puede extraer de este artículo, es la importancia de tratar el registro como un aspecto esencial del desarrollo de software. Un sistema de registro bien diseñado no solo facilita la depuración y la auditoría posterior, sino que también ayuda en la seguridad, trazabilidad y confiabilidad de un Asesor Experto. Implementar buenas prácticas de registro en las primeras etapas del desarrollo puede ahorrarle dolores de cabeza, haciendo que el mantenimiento sea más fácil y la resolución de problemas más eficiente. En el próximo artículo, exploraremos cómo almacenar registros en una base de datos para realizar análisis avanzados. ¡Nos vemos allí!
| 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/Handlers/LogifyHandler.mqh | Clase base para administrar controladores de registros, incluida la configuración de niveles y el envío de registros. |
| Include/Logify/Handlers/LogifyHandlerConsole.mqh | Controlador de registros que envía registros formateados directamente a la consola del terminal en MetaTrader. |
| Include/Logify/Handlers/LogifyHandlerDatabase.mqh | Manejador de registros que envía registros formateados a una base de datos (actualmente solo contiene una impresión, pero pronto la guardaremos en una base de datos sqlite real). |
| Include/Logify/Handlers/LogifyHandlerFile.mqh | Controlador de registros que envía registros formateados a un archivo. |
| Include/Logify/Utils/IntervalWatcher.mqh | Comprueba si ha transcurrido un intervalo de tiempo, lo que permite crear rutinas dentro de la biblioteca. |
| 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/17137
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.
Pruebas de robustez en asesores expertos
Creación de un Panel de administración de operaciones en MQL5 (Parte IX): Organización del código (I)
Introducción a MQL5 (Parte 12): Guía para principiantes sobre cómo crear indicadores personalizados
Ingeniería de características con Python y MQL5 (Parte III): El ángulo del precio (2) Coordenadas polares
- 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