English Русский 中文 Español Deutsch 日本語
preview
Dominando Registros de Log (Parte 5): Otimizando o Handler com Cache e Rotação

Dominando Registros de Log (Parte 5): Otimizando o Handler com Cache e Rotação

MetaTrader 5Exemplos |
25 0
joaopedrodev
joaopedrodev

Introdução

No primeiro artigo desta série, Dominando Registros de Log (Parte 1): Conceitos Fundamentais e Primeiros Passos em MQL5, iniciamos a criação de uma biblioteca de log personalizada para o desenvolvimento de Expert Advisors (EAs). Nele, exploramos a motivação por trás da criação de uma ferramenta tão essencial: superar as limitações dos logs nativos do MetaTrader 5 e trazer uma solução robusta, personalizável e poderosa para o universo MQL5.

Para recapitular os principais pontos abordados, estabelecemos a base da nossa biblioteca definindo os seguintes requisitos fundamentais:

  1. Estrutura robusta utilizando o padrão Singleton, garantindo consistência entre os componentes do código.
  2. Persistência avançada para armazenar logs em bancos de dados, fornecendo histórico rastreável para auditorias e análises aprofundadas.
  3. Flexibilidade nas saídas, permitindo que os logs sejam armazenados ou exibidos de forma conveniente, seja no console, em arquivos, no terminal ou em um banco de dados.
  4. Classificação por níveis de log, diferenciando mensagens informativas de alertas críticos e erros.
  5. Personalização do formato de saída, para atender às necessidades únicas de cada desenvolvedor ou projeto.

Com essa base bem estabelecida, ficou claro que o framework de logging que estamos desenvolvendo será muito mais do que um simples registro de eventos; ele será uma ferramenta estratégica para compreender, monitorar e otimizar o comportamento dos EAs em tempo real.

Até aqui, exploramos os fundamentos dos logs, aprendemos como formatá-los e entendemos como os handlers controlam o destino das mensagens. No último artigo, aprendemos como salvar registros de log em um arquivo (.txt, .log ou .json). Agora, neste quinto artigo, vamos otimizar o processo de salvamento de logs em arquivos implementando cache e rotação de arquivos. Vamos começar então!


Adicionando um formatador a cada handler

Até agora, nossa biblioteca de logging gerencia a formatação das mensagens por meio de uma única instância da classe CFormatter, que é centralizada na base da biblioteca (CLogify). Essa abordagem funciona bem para cenários simples, mas limita a flexibilidade dos handlers.

Essa abordagem funciona bem para cenários simples, mas limita a flexibilidade dos handlers. Por exemplo, enquanto um handler que grava logs em JSON pode precisar de uma estrutura específica, um handler que imprime logs no console pode exigir um formato mais legível para humanos. A solução é mover a responsabilidade do formatador para a classe base do handler (CLogifyHandler). Dessa forma, cada handler pode ter seu próprio formatador independente, permitindo maior controle sobre a formatação das mensagens de log. Vamos implementar essa mudança e ver como ela melhora a flexibilidade da biblioteca.

Indo direto ao código, começamos adicionando uma instância de CFormatter dentro de CLogifyHandler; como esta é uma tarefa simples para você que leu os artigos anteriores, vou apenas adicionar o código final destacando o que foi adicionado:

//+------------------------------------------------------------------+
//|                                                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 com as mudanças mais simples, removemos a instância de CFormatter em CLogify; as partes que foram removidas da classe estão destacadas em vermelho, e as que foram adicionadas estão destacadas em 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);
  }
//+------------------------------------------------------------------+

A única parte que foi adicionada foi no momento de formatar a mensagem. Antes, utilizávamos o formatador dentro da própria classe. Com as alterações, em cada handler utilizamos o formatador fornecido pelo handler. Ao associar um formatador diretamente a cada handler, eliminamos a restrição de um único formato e tornamos a biblioteca mais adaptável a diferentes necessidades. Agora, cada destino pode ter um estilo de log específico, garantindo que a saída seja mais apropriada para o contexto em que será utilizada. No próximo tópico, veremos como gerenciar a execução de logs em ciclos programados com a classe CIntervalWatcher, que será uma classe auxiliar para a rotação de arquivos.


Criando a classe CIntervalWatcher

O principal objetivo da CIntervalWatcher é verificar se um determinado intervalo de tempo passou desde a última chamada. Isso é essencial para gerar logs que precisam ser verificados em intervalos de tempo específicos. Seja para evitar sobrecarga de escrita ou para estruturar melhor os registros, um mecanismo de controle de ciclos é essencial, evitando processamento desnecessário a cada tick. Ela permite configurar:

  • O intervalo de tempo a ser monitorado (em segundos).
  • A origem do tempo (tempo atual, GMT, local ou servidor de negociação).
  • Se deve retornar true na primeira execução.

Dessa forma, a classe é útil para verificar quando executar uma ação periódica dentro da biblioteca. Vamos criar uma nova pasta chamada Utils, que conterá esse arquivo. Ao final, o navegador de arquivos deverá ficar assim:

Prosseguindo para a construção da classe, primeiro criamos um enum para dar suporte a diferentes origens de tempo, chamamos de 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
  };
//+------------------------------------------------------------------+

Adicionamos variáveis privadas à classe para armazenar o último instante registrado (m_last_time), o intervalo de tempo desejado (m_interval), a origem do tempo (m_time_origin) e um sinalizador (m_first_return) para controlar o primeiro retorno. Como consequência, criamos um Set e um Get para cada atributo privado. Para facilitar a configuração de intervalos, origem do tempo e primeiro retorno, decidi criar alguns construtores extras para a classe, auxiliando você, desenvolvedor. Abaixo está o código com os construtores e métodos para acessar e obter os dados 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 auxiliar no método principal, vamos criar a função GetTime, que retorna o tempo com base na origem definida:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

O método mais importante da classe é o Inspect(), que verifica se o intervalo definido foi atingido. A lógica é a seguinte: na primeira chamada, ele verifica se m_last_time é zero (classe recém-instanciada); a função armazena o tempo atual e retorna m_first_return. Se o timestamp armazenado for diferente do timestamp atual somado ao intervalo, isso significa que o intervalo foi atingido, então m_last_time é atualizado e a função retorna true. Se o timestamp for o mesmo, significa que ainda não foi atingido, então a função retorna false.

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Com o CIntervalWatcher, temos um controle mais refinado sobre a geração de logs, permitindo ciclos programáveis e maior eficiência de processamento. Esse tipo de abordagem será essencial para uma biblioteca de logging que exige execução periódica de tarefas. Agora, com a execução periódica das ações de log configurada, podemos focar na otimização do processo de gravação e na manutenção do desempenho do sistema.


Otimizando o Salvamento de Logs: Cache e Rotação de Arquivos

Embora a gravação direta de logs em arquivos que implementamos no último artigo seja uma solução funcional, ela pode se tornar ineficiente à medida que o volume de logs cresce. Para evitar impactos negativos no desempenho, é necessário otimizar esse processo. Neste tópico, vamos explorar como implementar um sistema de cache e rotação de arquivos para garantir que os logs sejam gravados de forma eficiente, sem sobrecarregar o armazenamento e mantendo a integridade dos dados.

No último artigo, discutimos com mais detalhes como essas melhorias funcionam e suas vantagens:

“Imagine este cenário: um Expert Advisor executando por semanas ou meses, registrando cada evento, erro ou notificação no mesmo arquivo. Logo, esse log começa a atingir tamanhos consideráveis, tornando a leitura e interpretação das informações bastante complexas. É aí que entra a rotação. Ela nos permite dividir essas informações em partes menores e organizadas, tornando tudo muito mais fácil de ler e analisar.

As duas formas mais comuns de fazer isso são:

  1. Por Tamanho: você define um limite de tamanho, geralmente em megabytes (MB), para o arquivo de log. Quando esse limite é atingido, um novo arquivo é criado automaticamente, e o ciclo começa novamente. Essa abordagem é muito prática quando o foco está em controlar o crescimento dos logs, sem a necessidade de se prender a um calendário. Assim que o arquivo de log atual atinge o limite de tamanho (em megabytes), ocorre o seguinte fluxo: o arquivo de log atual é renomeado, recebendo um índice, como “log1.log”. Os arquivos existentes no diretório também são renumerados, como “log1.log” passando a se chamar “log2.log”. Se o número de arquivos atingir o máximo permitido, os arquivos mais antigos são excluídos. Essa abordagem é útil para limitar tanto o espaço ocupado pelos logs quanto a quantidade de arquivos salvos.
  2. Por Data: neste caso, um novo arquivo de log é criado todos os dias. Cada um possui em seu nome a data em que foi criado, por exemplo log_2025-01-19.log, o que já resolve grande parte do problema de organização dos logs. Essa abordagem é perfeita quando você precisa analisar um dia específico, sem se perder em um único arquivo gigantesco. Este é o método que mais utilizo ao salvar logs dos meus Expert Advisors, tudo fica mais limpo, mais direto e mais fácil de navegar.

Além disso, você também pode limitar o número de arquivos de log armazenados. Esse controle é muito importante para evitar o acúmulo desnecessário de registros antigos. Imagine que você configure para manter os 30 arquivos mais recentes. Quando o 31º surgir, o sistema descarta automaticamente o mais antigo, o que evita que logs muito antigos se acumulem no disco, mantendo apenas os mais recentes.

Outro detalhe crucial é o uso de um cache. Em vez de gravar cada mensagem diretamente no arquivo assim que ela chega, as mensagens são armazenadas temporariamente no cache. Quando o cache atinge um limite definido, todo o conteúdo é gravado no arquivo de uma só vez. Isso resulta em menos operações de leitura e escrita no disco, maior desempenho e uma vida útil mais longa para seus dispositivos de armazenamento.”

Para implementar a rotação de arquivos de log, primeiro precisamos de um método auxiliar chamado SearchForFilesInDirectory(). Esse método é responsável por pesquisar todos os arquivos presentes em um diretório específico e retornar seus nomes em um array. Ele utiliza a função FileFindFirst() para iniciar a busca e, à medida que encontra arquivos, seus nomes são adicionados a esse array. Após a conclusão do processo, o método fecha o manipulador de busca utilizando FileFindClose().

Mas por que esse método é tão importante? Simples! Ele nos permite listar os arquivos de log existentes, garantindo que a classe que gerencia os logs exclua arquivos mais antigos quando necessário.

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);
  }
//+------------------------------------------------------------------+

Agora que temos a função para buscar os arquivos, podemos incorporá-la ao método principal responsável por emitir os logs, o Emit(). Dependendo da configuração de rotação escolhida, a lógica será ajustada.

Se a rotação de logs estiver configurada para ocorrer com base no tamanho do arquivo, a função:

  • Verifica se o tamanho do arquivo excedeu o limite configurado (m_config.max_file_size_mb).
  • Pesquisa todos os arquivos de log no diretório.
  • Remove arquivos antigos que excedem o número máximo permitido (m_config.max_file_count).
  • Renomeia arquivos antigos, incrementando numericamente seus índices (log1.txt, log2.txt, etc.).
  • Renomeia o arquivo de log atual como "log1" para manter a sequência.

Se a rotação for baseada em data, a função:

  • Pesquisa todos os arquivos de log no diretório.
  • Exclui os arquivos mais antigos que excedem o número máximo permitido (m_config.max_file_count).

Agora, vamos ver a implementação do método Emit() com ambas as lógicas de rotação de logs:

//+------------------------------------------------------------------+
//| 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+"'. Garanta que o diretório exista e seja gravável. (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]);
                 }
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+


Salvando por blocos para melhor desempenho

Prosseguindo para outra melhoria, vamos criar a lógica que considero a mais interessante do artigo: salvar registros por blocos. A ideia central é implementar um cache (memória temporária), onde os registros de log serão armazenados até atingirem um limite definido. Quando esse limite é atingido, todos os registros no cache são salvos no arquivo de log de uma só vez.

Implementaremos essa lógica em etapas. Primeiro, criaremos a estrutura de cache na classe CLogifyHandlerFile. Na seção privada da classe, adicionaremos um array do tipo MqlLogifyModel para armazenar temporariamente os registros de log. Também incluímos uma variável para controlar o índice atual do último valor salvo no cache. Sempre que um novo registro for adicionado, esse índice será incrementado. Também criamos uma instância da classe CIntervalWatcher e definimos um intervalo de um dia no construtor. Veja como fica:

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;
  }
//+------------------------------------------------------------------+

Com a estrutura de cache e atualização criada, avançamos para o próximo passo: modificar o método Emit() para utilizar o cache.

O método Emit() é responsável por processar uma mensagem de log e enviá-la ao destino configurado (neste caso, um arquivo). Vamos adaptá-lo para que, em vez de salvar os dados diretamente no arquivo, ele os armazene temporariamente no cache. Quando o cache atinge o limite configurado, ou o intervalo definido (um dia), o método chama a função Flush(), que salva os registros acumulados no arquivo. Esse intervalo é útil porque, se os dados permanecerem em cache por mais de um dia, esse mecanismo garante que eles ainda sejam salvos diariamente, além de permitir que a rotina de rotação seja executada todos os dias.

Aqui está o 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();
           }
        }
     }
  }
//+------------------------------------------------------------------+

A função Flush() é responsável por salvar os dados do cache no arquivo. Esse processo envolve abrir o arquivo, posicionar o ponteiro no final e gravar todos os registros armazenados no cache.

//+------------------------------------------------------------------+
//| 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+"'. Garanta que o diretório exista e seja gravável. (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]);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Com essa implementação, criamos uma solução de logging eficiente e escalável, capaz de lidar com grandes volumes de dados sem comprometer o desempenho do seu expert. Por fim, precisamos garantir que, quando o programa for encerrado, todos os dados em cache sejam salvos no arquivo. Para isso, basta chamar o método Flush() no método Close(), que já é chamado no destrutor da classe base CLogify.

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyHandlerFile::~CLogifyHandlerFile(void)
  {
   this.Close();
  }
//+------------------------------------------------------------------+
//| Closes the handler and releases any resources                    |
//+------------------------------------------------------------------+
void CLogifyHandlerFile::Close(void)
  {
   //--- Save cache
   Flush();
  }
//+------------------------------------------------------------------+

Ao implementar cache e rotação de arquivos, reduzimos o número de operações de escrita no disco e garantimos que nossos logs sejam armazenados de forma mais eficiente. Isso confere desempenho e escalabilidade à nossa biblioteca, tornando-a mais robusta para aplicações reais. Mas será que essas otimizações realmente fazem diferença? Vamos testar.


Testes de Desempenho: Medindo a Eficiência das Melhorias

Agora que implementamos as otimizações, precisamos medir seu impacto real. Os testes de desempenho nos ajudarão a entender se o cache está reduzindo a carga de escrita e se a rotação de arquivos está funcionando conforme esperado. Para isso, executaremos o mesmo teste realizado no último artigo, comparando a versão original da biblioteca com a versão otimizada.

Para executar o teste, utilizaremos o mesmo arquivo, com algumas modificações no formatter, já que agora cada handler possui seu próprio formatter. As alterações estão destacadas da seguinte forma:

  • Verde: Adições ao código
  • Vermelho: Remoções
  • Amarelo: O parâmetro que define o tamanho do cache. Quanto maior o cache, mais rápido o processamento.
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Vamos iniciar um teste no testador de estratégias utilizando os mesmos parâmetros de data e símbolo.

Utilizando o modelo "OHLC para 1 minuto" no símbolo EURUSD e um período de 7 dias, o tempo de execução foi de 26 segundos. Vale destacar que, a cada tick, um novo registro de log é gerado, e o cache está configurado para armazenar 10 mensagens. Agora, vamos aumentar o cache para 100 mensagens e observar a diferença de desempenho:

Com essa alteração, conseguimos reduzir o tempo do teste em 2 segundos, mantendo a mesma modelagem, data e configurações de símbolo. Se compararmos com o primeiro teste realizado no artigo anterior, que levou 5 minutos e 11 segundos, a melhoria é impressionante!

Os resultados demonstram que pequenas otimizações podem gerar ganhos significativos de eficiência. A combinação de cache e rotação de arquivos torna o gerenciamento de logs mais ágil e confiável, validando as escolhas feitas até aqui. Mas como essas melhorias podem ser aplicadas na prática? Vamos explorar alguns exemplos de uso.


Exemplos de Uso da Biblioteca de Log

Agora que aprimoramos nossa biblioteca de log, é hora de colocá-la em ação! Vamos explorar exemplos práticos de como utilizá-la para criar diferentes tipos de arquivos de log, cada um com sua própria formatação e nível de severidade.

Exemplo 1: Separar Logs em Arquivos .log e .json

No primeiro cenário, configuramos dois arquivos de log: um no formato .log e outro no formato .json. Cada um possui um formato específico e um nível de severidade diferente, facilitando o gerenciamento e a análise dos logs.

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Aqui, utilizamos a mesma variável de configuração m_config, alterando apenas os valores necessários para definir ambos os formatos de log. Isso torna a configuração mais simples e reutilizável.

Exemplo 2: Armazenando Apenas Erros em um Arquivo JSON

Agora, vamos um passo além e configurar um log específico para armazenar apenas mensagens de erro. Para isso, criamos uma pasta separada onde esse arquivo .json será salvo. Além disso, adicionamos um handler de console para exibir os logs diretamente no 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);
  }
//+------------------------------------------------------------------+

Neste exemplo, utilizamos três handlers de log:

  • Arquivo .log → Armazena logs no formato tradicional.
  • Arquivo .json → Armazena apenas mensagens de erro dentro de uma pasta separada.
  • Console → Exibe logs de forma mais legível para o usuário.

Utilizar um formatador mais “humano” no console ajuda a tornar a saída mais compreensível, enquanto o JSON de erros facilita a análise posterior.

Com esses exemplos, fica claro como nossa biblioteca de logging pode ser aplicada em projetos reais. A flexibilidade para criar diferentes formatos e níveis de severidade permite um bom gerenciamento, ajudando a identificar e solucionar problemas com mais facilidade. Além disso, a estrutura modular facilita a expansão do sistema de logging conforme necessário.

Agora, tudo o que você precisa fazer é adaptar essa implementação às suas necessidades e garantir que seus logs estejam sempre bem organizados e acessíveis!


Conclusão

Neste artigo, evoluímos nossa biblioteca de logging, tornando-a mais eficiente, escalável e adaptável. Refinamos a formatação, permitindo que cada handler tenha seu próprio formatador, tornando as mensagens mais organizadas e flexíveis para diferentes necessidades, como depuração local e auditoria.

Implementamos a classe CIntervalWatcher, que controla os ciclos de execução, garantindo que os logs sejam gravados e rotacionados em intervalos bem definidos. Também otimizamos a escrita com cache, reduzindo operações de disco e gerenciando melhor o crescimento dos arquivos. Validamos essas melhorias com testes de desempenho, refinando ainda mais a solução para suportar alta carga. Além disso, apresentamos exemplos práticos para facilitar a adoção da biblioteca.

Se há uma principal lição a ser extraída deste artigo, é a importância de tratar o logging como um aspecto essencial do desenvolvimento de software. Um sistema de logging bem projetado não apenas facilita a depuração e auditorias posteriores, como também auxilia na segurança, rastreabilidade e confiabilidade de um Expert Advisor. Implementar boas práticas de logging desde o início do desenvolvimento pode poupar dores de cabeça, tornando a manutenção mais simples e a resolução de problemas mais eficiente. No próximo artigo, exploraremos como armazenar logs em um banco de dados para análises avançadas. Até lá!

Nome do Arquivo
Descrição
Experts/Logify/LogiftTest.mq5
Arquivo onde testamos os recursos da biblioteca, contendo um exemplo prático
Include/Logify/Formatter/LogifyFormatter.mqh
Classe responsável por formatar registros de log, substituindo placeholders por valores específicos
Include/Logify/Handlers/LogifyHandler.mqh
Classe base para gerenciar handlers de log, incluindo definição de nível e envio de logs
Include/Logify/Handlers/LogifyHandlerConsole.mqh
Handler de log que envia logs formatados diretamente para o console do terminal no MetaTrader
Include/Logify/Handlers/LogifyHandlerDatabase.mqh
Handler de log que envia logs formatados para um banco de dados (atualmente contém apenas uma impressão, mas em breve salvaremos em um banco sqlite real)
Include/Logify/Handlers/LogifyHandlerFile.mqh
Handler de log que envia logs formatados para um arquivo
Include/Logify/Utils/IntervalWatcher.mqh Verifica se um intervalo de tempo foi atingido, permitindo criar rotinas dentro da biblioteca
Include/Logify/Logify.mqh
Classe principal para gerenciamento de logs, integrando níveis, modelos e formatação
Include/Logify/LogifyLevel.mqh
Arquivo que define os níveis de log da biblioteca Logify, permitindo controle detalhado
Include/Logify/LogifyModel.mqh
Estrutura que modela registros de log, incluindo detalhes como nível, mensagem, timestamp e contexto

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17137

Arquivos anexados |
Logify2Part5c.zip (17.06 KB)
Redes neurais em trading: Extração eficiente de características para classificação precisa (Mantis) Redes neurais em trading: Extração eficiente de características para classificação precisa (Mantis)
Conheça o Mantis, um modelo fundamental leve para classificação de séries temporais baseado em Transformer, com pré-treinamento contrastivo e atenção híbrida, que garantem precisão recorde e escalabilidade.
Criando um Painel de Administração de Trading em MQL5 (Parte IX): Organização de Código (I) Criando um Painel de Administração de Trading em MQL5 (Parte IX): Organização de Código (I)
Esta discussão aprofunda-se nos desafios encontrados ao trabalhar com grandes bases de código. Vamos explorar as melhores práticas para organização de código em MQL5 e implementar uma abordagem prática para aprimorar a legibilidade e a escalabilidade do código-fonte do nosso Painel de Administração de Trading. Além disso, buscamos desenvolver componentes de código reutilizáveis que possam potencialmente beneficiar outros desenvolvedores no desenvolvimento de seus algoritmos. Continue lendo e participe da discussão.
Do básico ao intermediário: Indicadores técnicos (I) Do básico ao intermediário: Indicadores técnicos (I)
Neste artigo veremos o básico sobre como utilizar indicadores técnicos que estão presentes e são mantidos pelo próprio MetaTrader 5. Saber, entender e conhecer este tipo de indicador pode vir a tornar seu trabalho de implementar algo, em uma tarefa muito mais simples, rápida e eficiente. Visto que você não precisa se preocupar com a parte referente aos cálculos a serem efetuados. A própria plataforma se encarrega de fazer isto para nós.
Busca oscilatória determinística — Deterministic Oscillatory Search (DOS) Busca oscilatória determinística — Deterministic Oscillatory Search (DOS)
O algoritmo Deterministic Oscillatory Search (DOS) é um método inovador de otimização global que combina as vantagens dos algoritmos de gradiente e dos algoritmos de enxame sem o uso de números aleatórios. O mecanismo de oscilações e de inclinações de fitness permite ao DOS explorar espaços de busca complexos por meio de um método determinístico.