English 中文 Español Deutsch 日本語
preview
Искусство ведения логов (Часть 5): Оптимизация обработчика с помощью кэширования и ротации

Искусство ведения логов (Часть 5): Оптимизация обработчика с помощью кэширования и ротации

MetaTrader 5Примеры |
305 0
joaopedrodev
joaopedrodev

Введение

В первой статье этой серии "Искусство ведения логов (Часть 1): Основные понятия и первые шаги в MQL5", мы приступили к созданию пользовательской библиотеки журналов для разработки советников. Также мы исследовали мотивацию создания столь важного инструмента: преодолеть ограничения собственных журналов MetaTrader 5 и предоставить надежное, настраиваемое и мощное решение для экосистемы MQL5.

Мы заложили основу нашей библиотеки, установив следующие основные требования:

  1. Создание устойчивой конструкции с помощью паттерна Singleton, обеспечивающего согласованность между компонентами кода.
  2. Расширенные возможности сохранения логов в базах данных для отслеживаемости истории в целях углубленного аудита и анализа.
  3. Гибкость в выводе данных, позволяющая удобно хранить и отображать журналы: в консоли, в файлах, в терминале или в базе данных.
  4. Классификация по уровням, отличающая информационные сообщения от важных оповещений и ошибок.
  5. Настройка выходного формата для удовлетворения уникальных потребностей каждого разработчика или проекта.

Благодаря этой прочной основе стало ясно, что разрабатываемая нами структура логирования будет представлять собой нечто большее, чем просто логи событий; это будет стратегический инструмент для понимания, мониторинга и оптимизации поведения советника в реальном времени.

До сих пор мы изучили основы логирования, научились их форматировать, а также рассмотрели, как обработчики управляют назначением сообщений. В прошлой статье мы узнали, как сохранять записи журнала в файл (.txt, .log, или .json). В пятой статье серии мы оптимизируем сохранение журналов в файлы, реализовав кэширование и ротацию файлов.


Добавление форматтера к каждому обработчику

До сих пор наша библиотека логов управляла форматированием сообщений через один экземпляр класса CFormatter, который централизованно размещен в базе библиотеки (CLogify). Этот подход хорошо работает в простых сценариях, но ограничивает гибкость обработчиков.

Проблема в том, что при использовании одного глобального форматтера все обработчики используют один и тот же формат, что может быть неидеально, когда для разных целей требуется разное форматирование. Например, обработчику, который записывает логи в формате JSON, может потребоваться определенная структура, а обработчику, который выводит логи на консоль, может потребоваться более удобочитаемый формат. Решение состоит в том, чтобы перенести ответственность за форматирование в базовый класс обработчика (CLogifyHandler). Таким образом, каждый обработчик может иметь свой собственный независимый форматтер, что обеспечивает больший контроль над форматированием сообщений журнала. Давайте реализуем это изменение и посмотрим, как оно повысит гибкость библиотеки.

Переходя непосредственно к коду, мы начнем с добавления экземпляра CFormatter внутрь CLogifyHandler. Поскольку это простая задача для тех, кто читал предыдущие статьи, я просто добавлю окончательный код, выделив то, что было добавлено:

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

Продолжая простейшие изменения, мы удалили экземпляр CFormatter в CLogify. Части, которые были удалены из класса, выделены красным, а те, которые были добавлены, выделены зеленым:

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

Единственная часть, которая была добавлена, была при форматировании сообщения. Раньше мы использовали форматтер внутри самого класса. Благодаря изменениям в каждом обработчике мы используем форматтер, предоставляемый обработчиком. Связывая форматтер непосредственно с каждым обработчиком, мы устраняем ограничение одного формата и делаем библиотеку более адаптируемой к различным потребностям. Теперь для каждого пункта назначения можно задать определенный стиль логирования, что гарантирует, что выходные данные будут лучше соответствовать контексту, в котором они будут использоваться. В следующей теме мы рассмотрим, как управлять логированием в запланированных циклах с помощью класса CIntervalWatcher, который будет вспомогательным классом для ротации файлов.


Создание класса CIntervalWatcher

Основная задача CIntervalWatcher — проверить, прошел ли определенный интервал времени с момента последнего вызова. Это необходимо для создания логов, которые нужно проверять через определенные промежутки времени. Независимо от того, нужно ли избегать перегрузки при записи или лучше структурировать записи, необходим механизм управления циклом, позволяющий избежать ненужной обработки на каждом этапе. Класс позволяет настроить:

  • Интервал времени, который необходимо контролировать (в секундах).
  • Источник времени (текущее время, GMT, локальное или торговое время сервера).
  • Нужно ли возвращать true при первом выполнении.

Таким образом, класс полезен для проверки того, когда следует выполнять периодическое действие в библиотеке. Давайте создадим новую папку Utils, которая будет содержать этот файл. В конечном итоге файловый браузер должен выглядеть так:

Переходя к построению класса, сначала создадим перечисление для поддержки различных источников времени. Назовем его 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
  };
//+------------------------------------------------------------------+

Мы добавили в класс частные переменные для хранения последнего зафиксированного момента (m_last_time), желаемого временного интервала (m_interval), начала отсчета времени (m_time_origin) и флага (m_first_return) для управления первым возвратом. В результате мы создали Set и Get для каждого частного атрибута. Чтобы упростить настройку интервалов, начала отсчета времени и первого возврата, я решил создать несколько дополнительных конструкторов для класса в помощь разработчику. Ниже представлен код с конструкторами и методами для доступа и получения приватных данных.

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

Чтобы помочь основному методу, создадим функцию GetTime, которая возвращает время на основе определенного источника:

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

Самый важный метод класса — Inspect(), который проверяет, достигнут ли заданный интервал. Логика следующая: при первом вызове проверяется, равно ли m_last_time нулю (вновь созданный класс), функция сохраняет текущее время и возвращает m_first_return. Если сохраненная временная метка отличается от текущей плюс интервал, это означает, что интервал достигнут, поэтому m_last_time обновляется, и функция возвращает true. Если временная метка та же, это означает, что интервал еще не достигнут, поэтому функция возвращает 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);
  }
//+------------------------------------------------------------------+

Благодаря CIntervalWatcher мы получаем более точный контроль над созданием журналов, что позволяет программировать циклы и повышать эффективность обработки. Такой подход будет необходим для библиотеки журналирования, требующей периодического выполнения задач. Теперь, настроив периодическое выполнение действий логирования, мы можем сосредоточиться на оптимизации записи и поддержании производительности системы.


Оптимизация сохранения логов: кэширование и ротация файлов

Хотя прямая запись логов в файлы, которую мы реализовали в прошлой статье, является функциональным решением, по мере роста объема логов оно может стать неэффективным. Чтобы избежать негативного влияния на производительность, необходимо оптимизировать этот процесс. В этой теме мы рассмотрим, как реализовать систему кэширования и ротации файлов, чтобы гарантировать эффективную запись логов, не перегружая хранилище и сохраняя целостность данных.

В предыдущей статье мы более подробно обсудили, как работают эти улучшения и их преимущества:

"Представьте себе следующий сценарий: советник работает в течение нескольких недель или месяцев, записывая каждое событие, ошибку или уведомление в один и тот же файл. Вскоре этот лог достигает значительных размеров, что значительно усложняет чтение и интерпретацию информации. Именно здесь и приходит на помощь ротация. Она позволяет нам разделить эту информацию на более мелкие и организованные части, что значительно упрощает чтение и анализ.

Два наиболее распространенных способа сделать это:

  1. По размеру: Вы устанавливаете ограничение по размеру, обычно в мегабайтах (МБ), для лог-файла. Когда это ограничение достигается, автоматически создается новый файл, и цикл начинается заново. Этот подход очень практичен, когда основное внимание уделяется контролю роста лога, без необходимости придерживаться календаря. Как только текущий файл достигает ограничения по размеру (в мегабайтах), происходит следующий поток: Текущий лог-файл переименовывается, получая индекс, например "log1.log". Существующие файлы в каталоге также перенумеровываются, например "log1.log" становится "log2.log". Если количество файлов достигает максимально допустимого, самые старые файлы удаляются. Этот подход полезен для ограничения как места, занимаемого логами, так и количества сохраненных файлов.
  2. По дате: в этом случае каждый день создается новый лог-файл. Каждый из них имеет в своем имени дату создания, например log_2025-01-19.log, что решает большую часть проблем с организацией логов. Этот подход идеально подходит для тех случаев, когда вам нужно посмотреть конкретный день, не теряясь в одном гигантском файле. Это метод, который я чаще всего применяю для логов моих советников: он делает данные более понятными, структурированными и удобными для анализа.

Кроме того, вы также можете ограничить количество хранимых файлов логов. Этот контроль очень важен для предотвращения ненужного накопления старых логов. Представьте, что вы настроили сохранение 30 самых последних файлов, когда появляется 31-й, система автоматически удаляет самый старый, что предотвращает накопление очень старых логов на диске, а самые последние сохраняются.

Еще одна важная деталь — использование кэша. Вместо того чтобы записывать каждое сообщение непосредственно в файл сразу после его поступления, сообщения временно хранятся в кэше. Когда кэш достигает установленного предела, он сразу же сбрасывает все содержимое файла. Это приводит к уменьшению количества операций чтения и записи на диск, улучшению производительности и увеличению срока службы ваших устройств хранения данных."

Чтобы реализовать ротацию файлов журнала, нам сначала понадобится вспомогательный метод SearchForFilesInDirectory(). Этот метод отвечает за поиск всех файлов, присутствующих в определенном каталоге, и возврат их имен в массиве. Он использует функцию FileFindFirst() для запуска поиска. По мере нахождения файлов их имена добавляются в этот массив. После завершения процесса метод закрывает обработчик поиска, используя FileFindClose().

Но почему этот метод так важен? Всё просто! Он позволяет нам составить список существующих файлов логов, гарантируя, что класс, управляющий логами, при необходимости удалит старые файлы.

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

Теперь, когда у нас есть функция для извлечения файлов, мы можем включить ее в основной метод, отвечающий за публикацию логов, Emit(). Логика будет корректироваться в зависимости от выбранных настроек ротации.

Если ротация логов настроена на основе размера файла, функция:

  • Проверяет, не превысил ли размер файла установленный лимит (m_config.max_file_size_mb).
  • Выполняет поиск по всем файлам логов в каталоге.
  • Удаляет старые файлы, количество которых превышает максимально допустимое (m_config.max_file_count).
  • Переименовывает старые файлы, увеличивая их числовые индексы (log1.txt, log2.txt, etc.).
  • Переименовывает текущий лог-файл в "log1" для сохранения последовательности.

Если ротация основана на дате, функция:

  • Выполняет поиск по всем файлам логов в каталоге.
  • Удаляет самые старые файлы, количество которых превышает максимально допустимое (m_config.max_file_count).

Теперь давайте рассмотрим реализацию метода Emit() с обеими логиками ротации:

//+------------------------------------------------------------------+
//| 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+"'. Ensure the directory exists and is writable. (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]);
                 }
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+


Сохранение по блокам для лучшей производительности

Введем логику, которую я считаю наиболее интересной в статье, — сохранение записей по блокам. Основная идея заключается в реализации кэша (временной памяти), где записи журнала будут храниться до тех пор, пока не достигнут определенного предела. При достижении этого лимита все записи в кэше одновременно сохраняются в лог-файле.

Реализуем эту логику поэтапно. Сначала мы создадим структуру кэша в классе CLogifyHandlerFile. В закрытом разделе класса мы добавим массив типа MqlLogifyModel для временного хранения логов. Мы также включаем переменную для управления текущим индексом последнего значения, сохраненного в кэше. При каждом добавлении новой записи этот индекс будет увеличиваться. Мы также создаем экземпляр класса CIntervalWatcher и устанавливаем в конструкторе интервал в один день. Вот как это выглядит:

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

Создав кэш и структуру обновления, переходим к следующему шагу: изменению метода Emit() для использования кэша.

Метод Emit() отвечает за обработку сообщения журнала и отправку его в указанное место назначения (в данном случае в файл). Мы адаптируем его таким образом, чтобы вместо сохранения данных напрямую в файл они временно сохранялись в кэше. Когда кэш достигает настроенного предела или заданного интервала (один день), метод вызывает функцию Flush(), которая сохраняет накопленные записи в файле. Этот интервал полезен, поскольку если данные кэшируются более одного дня, этот механизм обеспечивает ежедневное сохранение данных, а также позволяет выполнять процедуру ротации каждый день.

Измененный код:

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

Функция Flush() отвечает за сохранение данных кэша в файл. Этот процесс включает открытие файла, установку указателя в конец и вывод всех записей, хранящихся в кэше.

//+------------------------------------------------------------------+
//| 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+"'. Ensure the directory exists and is writable. (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]);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Благодаря этой реализации мы создали эффективное и масштабируемое решение для ведения логов, способное обрабатывать большие объемы данных, не ставя под угрозу производительность советника. Наконец, нам нужно убедиться, что при закрытии программы все кэшированные данные сохраняются в файле. Для этого просто вызовите метод Flush() в методе Close(), который уже вызывается в деструкторе базового класса CLogify.

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

Внедряя кэширование и ротацию файлов, мы сокращаем количество операций записи на диск и обеспечиваем более эффективное хранение наших логов. Это обеспечивает производительность и масштабируемость нашей библиотеки, делая ее более надежной для реальных приложений. Но действительно ли эти оптимизации что-то меняют? Давайте проверим.


Тесты производительности: измерение эффективности улучшений

Теперь, когда мы внедрили оптимизации, нам необходимо оценить их реальное влияние. Тесты производительности помогут нам понять, снижает ли кэш нагрузку записи и работает ли ротация файлов так, как ожидается. Для этого мы проведем тот же тест, что и в предыдущей статье, сравнив исходную версию библиотеки с оптимизированной версией.

Для запуска теста мы будем использовать тот же файл, но с измененным форматтером, поскольку теперь каждый обработчик имеет собственный форматтер. Изменения выделены следующим образом:

  • Зеленый: дополнения к коду
  • Красный: удаление
  • Желтый: параметр, определяющий размер кэша. Чем больше кэш, тем быстрее обработка.
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Давайте начнем тестирование в тестере стратегий, используя те же параметры даты и символа.

При использовании модели "OHLC на М1" на символе EURUSD и 7-дневном таймфрейме время исполнения составило 26 секунд. Стоит отметить, что каждый тик генерируется новая запись журнала, а кэш настроен на хранение 10 сообщений. Теперь давайте увеличим кэш до 100 сообщений и посмотрим на разницу в производительности:

Благодаря этому изменению нам удалось сократить время тестирования на секунды, сохраняя при этом те же настройки моделирования, даты и символов. Если мы сравним с первым тестом, проведенным в предыдущей статье, который занял 5 итнут и 11 секунд, улучшение впечатляет!

Результаты показывают, что небольшая оптимизация может дать значительный прирост эффективности. Сочетание кэширования и ротации файлов делает управление журналами более гибким и надежным, подтверждая правильность сделанного выбора. Но как можно применить эти улучшения на практике? Давайте рассмотрим некоторые примеры использования.


Примеры использования библиотеки логов

Теперь, когда мы улучшили нашу библиотеку логов, пришло время применить ее на практике! Давайте рассмотрим практические примеры того, как использовать ее для создания различных типов лог-файлов, каждый из которых имеет собственное форматирование и уровень значимости.

Пример 1: Разделение логов на файлы .log и .json

В первом сценарии мы создаем два лог-файла: один в формате .log, другой - в формате .json. Каждый из них имеет определенный формат и разный уровень значимости, что упрощает управление логами и их анализ.

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

Здесь мы используем ту же переменную настройки m_config, изменяя только значения, необходимые для определения обоих форматов логов. Это упрощает настройку и делает ее более пригодной для повторного использования.

Пример 2: Хранение ошибок в файле JSON

Теперь давайте сделаем еще один шаг и настроим специальные логи для хранения только сообщений об ошибках. Для этого создадим отдельную папку, в которой будет сохранен этот .json-файл. Кроме того, мы добавляем обработчик консоли для отображения логов непосредственно в терминале.

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

В этом примере мы используем три обработчика логов:

  • Файл .log → Сохраняет журналы в традиционном формате.
  • Файл .json → Сохраняет только сообщения об ошибках в отдельной папке.
  • Консоль → Отображает логи в более удобном для пользователя виде.

Использование более "человеческого" форматирования в консоли помогает сделать вывод более понятным, а JSON ошибок упрощает их последующий анализ.

С помощью этих примеров становится ясно, как нашу библиотеку логов можно применять в реальных проектах. Гибкость в создании различных форматов и уровней значимости обеспечивает эффективное управление, помогая легче выявлять и устранять проблемы. Кроме того, модульная структура позволяет легко расширять систему логов по мере необходимости.

Теперь все, что вам осталось сделать, — это адаптировать реализацию к своим потребностям и убедиться, что ваши логи всегда хорошо организованы и доступны!


Заключение

В этой статье мы усовершенствовали нашу библиотеку логов, сделав ее более эффективной, масштабируемой и адаптируемой. Мы усовершенствовали форматирование, разрешив каждому обработчику иметь собственный форматтер, что делает сообщения более организованными и гибкими для различных нужд, таких как локальная отладка и аудит.

Мы реализовали класс CIntervalWatcher, который управляет циклами выполнения, гарантируя, что логи записываются и меняются с четко определенными интервалами. Мы также оптимизировали запись с помощью кэширования, сократив дисковые операции и лучше управляя ростом файла. Мы подтвердили эти улучшения с помощью тестов производительности, дополнительно доработав решение для поддержки высоких нагрузок. Кроме того, мы привели практические примеры, облегчающие освоение библиотеки.

Если из этой статьи можно извлечь один главный урок, то он заключается в важности рассмотрения логов как важного аспекта разработки программного обеспечения. Хорошо спроектированная система ведения логов не только облегчает отладку и последующий аудит, но и способствует безопасности, отслеживаемости и надежности советника. Внедрение хороших методов ведения логов на ранних этапах разработки может избавить вас от ряда проблем, связанных с разработкой, упростить обслуживание и повысить эффективность устранения неполадок. В следующей статье мы рассмотрим, как хранить логи в базе данных для расширенной аналитики. До встречи!

Имя файла
Описание
Experts/Logify/LogiftTest.mq5
Файл, в котором мы тестируем функции библиотеки, содержащий практический пример.
Include/Logify/Formatter/LogifyFormatter.mqh
Класс, отвечающий за форматирование сообщений, заменяющий заполнители конкретными значениями.
Include/Logify/Handlers/LogifyHandler.mqh
Базовый класс для управления обработчиками логов, включая настройку уровня и отправку логов.
Include/Logify/Handlers/LogifyHandlerConsole.mqh
Обработчик логов, который отправляет отформатированные логи непосредственно в консоль терминала в MetaTrader.
Include/Logify/Handlers/LogifyHandlerDatabase.mqh
Обработчик логов, который отправляет отформатированные логи в базу данных (в настоящее время он содержит только распечатку, но в скором времени мы будем сохранять их в реальной базе данных sqlite).
Include/Logify/Handlers/LogifyHandlerFile.mqh
Обработчик логов, который отправляет отформатированные логи в файл.
Include/Logify/Utils/IntervalWatcher.mqh Проверка временного интервала для создания процедур в библиотеке
Include/Logify/Logify.mqh
Основной класс для управления логами, интегрирующий уровни, модели и форматирование.
Include/Logify/LogifyLevel.mqh
Файл, который определяет уровни логирования библиотеки Logify, позволяя осуществлять детальный контроль.
Include/Logify/LogifyModel.mqh
Структура, которая моделирует логи, включая такие аспекты, как уровень, сообщение, метка времени и контекст.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17137

Прикрепленные файлы |
Logify2Part5c.zip (17.06 KB)
Математические модели в сеточных стратегиях Математические модели в сеточных стратегиях
В этой статье мы рассмотрим применение математики к сеточным стратегиям. Мы разберем основные принципы работы стратегии, её преимущества и недостатки. Вы узнаете, как построить торговую сетку, задавать оптимальные параметры и эффективно управлять рисками.
Автоматизация торговых стратегий на MQL5 (Часть 17): Освоение стратегии скальпинга Grid-Mart с динамической информационной панелью Автоматизация торговых стратегий на MQL5 (Часть 17): Освоение стратегии скальпинга Grid-Mart с динамической информационной панелью
В настоящей статье мы рассмотрим стратегию скальпинга Grid-Mart, автоматизировав ее на MQL5 с помощью динамической информационной панели для получения информации о торговле в режиме реального времени. Мы подробно описываем логику мартингейла на основе сетки, а также функции управления рисками. Мы также проводим тестирование на истории и развертывание для обеспечения надежной работы.
Разработка инструментария для анализа движения цен (Часть 12): Внешние библиотеки (III) TrendMap Разработка инструментария для анализа движения цен (Часть 12): Внешние библиотеки (III) TrendMap
Движение рынка определяется силами быков и медведей. Существуют определенные уровни, которые рынок соблюдает из-за действующих на них сил. Уровни Фибоначчи и VWAP особенно сильно влияют на поведение рынка. В этой статье мы рассмотрим стратегию, основанную на VWAP и уровнях Фибоначчи для генерации сигналов.
Создание торговой панели администратора на MQL5 (Часть IX): Организация кода (I) Создание торговой панели администратора на MQL5 (Часть IX): Организация кода (I)
В этом обсуждении рассматриваются проблемы, возникающие при работе с большими базами кодов. Мы рассмотрим лучшие практики организации кода в MQL5 и реализуем практический подход для повышения читаемости и масштабируемости исходного кода нашей панели торгового администратора. Кроме того, мы начнем разработку повторно используемых компонентов кода, которые потенциально могут принести пользу другим разработчикам при создании алгоритмов. Присоединяйтесь к обсуждению.