English Русский 中文 Deutsch 日本語
preview
Dominando los registros (Parte 5): Optimizar el controlador con caché y rotación

Dominando los registros (Parte 5): Optimizar el controlador con caché y rotación

MetaTrader 5Ejemplos |
148 0
joaopedrodev
joaopedrodev

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:

  1. Estructura robusta que utiliza el patrón Singleton, lo que garantiza la coherencia entre los componentes del código.
  2. Persistencia avanzada para almacenar registros en bases de datos, proporcionando un historial rastreable para auditorías y análisis en profundidad.
  3. 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.
  4. Clasificación por niveles de registro, diferenciando mensajes informativos de alertas críticas y errores.
  5. 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:

  1. 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.
  2. 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

Archivos adjuntos |
Logify2Part5c.zip (17.06 KB)
Pruebas de robustez en asesores expertos Pruebas de robustez en asesores expertos
En el desarrollo de una estrategia hay muchos detalles complejos a tener en cuenta, muchos de los cuales no se destacan para los traders principiantes. Como resultado, muchos comerciantes, incluido yo mismo, hemos tenido que aprender estas lecciones a las duras penas. Este artículo se basa en mis observaciones de errores comunes que la mayoría de los traders principiantes encuentran al desarrollar estrategias en MQL5. Ofrecerá una variedad de consejos, trucos y ejemplos para ayudar a identificar la descalificación de un EA y probar la solidez de nuestros propios EA de una manera fácil de implementar. El objetivo es educar a los lectores, ayudándolos a evitar futuras estafas al comprar EA, así como a prevenir errores en el desarrollo de su propia estrategia.
Creación de un Panel de administración de operaciones en MQL5 (Parte IX): Organización del código (I) Creación de un Panel de administración de operaciones en MQL5 (Parte IX): Organización del código (I)
Este debate profundiza en los retos que se plantean al trabajar con grandes bases de código. Exploraremos las mejores prácticas para la organización del código en MQL5 e implementaremos un enfoque práctico para mejorar la legibilidad y la escalabilidad del código fuente de nuestro Panel de administración de operaciones. Además, nuestro objetivo es desarrollar componentes de código reutilizables que puedan beneficiar a otros desarrolladores en el desarrollo de sus algoritmos. Sigue leyendo y únete a la conversación.
Introducción a MQL5 (Parte 12): Guía para principiantes sobre cómo crear indicadores personalizados Introducción a MQL5 (Parte 12): Guía para principiantes sobre cómo crear indicadores personalizados
Aprenda a crear un indicador personalizado en MQL5. Con un enfoque basado en proyectos. Esta guía para principiantes cubre los buffers de indicadores, las propiedades y la visualización de tendencias, permitiéndole aprender paso a paso.
Ingeniería de características con Python y MQL5 (Parte III): El ángulo del precio (2) Coordenadas polares Ingeniería de características con Python y MQL5 (Parte III): El ángulo del precio (2) Coordenadas polares
En este artículo, hacemos nuestro segundo intento de convertir los cambios en los niveles de precios de cualquier mercado en un cambio correspondiente en el ángulo. En esta ocasión, seleccionamos un enfoque matemáticamente más sofisticado que el que elegimos en nuestro primer intento, y los resultados obtenidos sugieren que nuestro cambio de enfoque puede haber sido la decisión correcta. Únase a nosotros hoy para debatir cómo podemos utilizar las coordenadas polares para calcular el ángulo formado por los cambios en los niveles de precios, de una manera significativa, independientemente del mercado que esté analizando.