English Deutsch 日本語
preview
Искусство работы с логами (Часть 9): Применяем паттерн Builder-класса и настраиваем конфигурации по умолчанию

Искусство работы с логами (Часть 9): Применяем паттерн Builder-класса и настраиваем конфигурации по умолчанию

MetaTrader 5Примеры |
182 7
joaopedrodev
joaopedrodev

Введение

С тех пор как я начал использовать Logify в различных личных и профессиональных проектах, я быстро осознал, что самая большая сложность заключалась не в надёжности или функциональности самой библиотеки, а в её настройке. Logify — это мощный инструмент для управления логированием в советниках (Expert Advisors), предлагающий множество обработчиков, уровней логирования, настраиваемые форматы, языковую поддержку и многое другое. Однако всё это требовало от пользователя ручной настройки каждого обработчика, форматтера и параметра, что может быть приемлемо для небольших проектов, но быстро превращается в повторяющуюся, утомительную и чреватую ошибками задачу по мере роста количества советников и проектов.

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

Это понимание подтолкнуло меня к размышлению: как можно упростить эту настройку, облегчить жизнь пользователю, не жертвуя при этом гибкостью и возможностью кастомизации? Именно тогда и родилась идея создания Builder-класса для Logify — класса, который позволяет собрать всю конфигурацию плавным, цепочным способом, с помощью интуитивно понятных методов, создающих обработчики с разумными шаблонами и допускающих быстрые, точечные корректировки. Цель — превратить десятки строк конфигурации в несколько вызовов методов, как если бы мы писали чёткую сводку того, что нам нужно, вместо того чтобы заниматься всей ручной сборкой.

В этой статье я покажу, как я реализовал эти улучшения. Я представлю Builder, объяснив его дизайн и использование. А затем продемонстрирую, как можно настроить Logify, на практических примерах.


Понимание паттерна Builder: упрощение создания сложных объектов

Прежде чем мы погрузимся в реализацию нашего CLogifyBuilder, важно понять идею, лежащую в основе используемого нами паттерна Builder.

На практике Builder — это паттерн проектирования, имеющий единственную цель: упростить создание сложных объектов, особенно когда эти объекты требуют нескольких этапов настройки или имеют множество возможных опций. Его смысл заключается в том, чтобы отделить процесс конструирования от конечного представления объекта, позволяя одной и той же структуре сборки создавать различные "вариации" готовых к использованию объектов.

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

Именно для такой ситуации паттерн Builder и подходит лучше всего. Он разбивает процесс создания на цепочку методов (так называемый цепочный интерфейс), каждый из которых отвечает за настройку определенной части объекта.

В конце вы вызываете метод .Build() или аналогичный ему и получаете готовый к использованию объект. Этот подход дает три основных преимущества:

  1. Понятный и линейный код: создание объекта выглядит как логичный "сценарий", почти как если бы вы описывали, что хотите получить.
  2. Снижение количества ошибок: поскольку каждый шаг имеет изолированную цель, гораздо легче обнаружить и исправить неправильные настройки.
  3. Гибкость и повторное использование: один и тот же Builder может быть повторно использован для создания вариаций объекта с небольшими изменениями.


Применение паттерна Builder к Logify

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

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

CLogify *logify = logify
   .Create()
   .AddHandlerComment()
      .SetTitle("My Logger")
      .SetSize(5)
   .Done()
   .AddHandlerConsole()
   .Done()
   .Build();

Обратите внимание, насколько это понятно и выразительно. Метод .Create() запускает процесс конструирования, каждый .AddHandlerXXX() открывает настройку определенного обработчика, методы .SetX() регулируют его параметры, метод .Done() завершает настройку обработчика, а .Build() возвращает готовый экземпляр CLogify.

В этом и заключается сила паттерна Builder: он позволяет разработчику описать, что он хочет получить, не беспокоясь о деталях внутренней реализации.

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


Специализированные Builder-классы для каждого обработчика

Мы создали новый файл <Include/Logify/LogifyBuilder.mqh>. Внутри него находится класс CLogifyBuilder, который уже содержит в себе, в качестве приватного поля, экземпляр CLogify. Этот экземпляр будет настраиваться в процессе сборки и в конечном итоге будет возвращен пользователю.

//+------------------------------------------------------------------+
//|                                                LogifyBuilder.mqh |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/en/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/en/users/joaopedrodev"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
#include "Logify.mqh"
//+------------------------------------------------------------------+
//| class : CLogifyBuilder                                           |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyBuilder                                      |
//| Heritage    : No heritage                                        |
//| Description : Build CLogify objects, following the Builder design|
//|               pattern.                                           |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyBuilder
  {
private:
   CLogify           *m_logify;
   
public:
                     CLogifyBuilder(void)
                    ~CLogifyBuilder(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyBuilder::CLogifyBuilder(void)
  {
   m_logify = new CLogify();
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyBuilder::~CLogifyBuilder(void)
  {
  }
//+------------------------------------------------------------------+

Класс CLogifyBuilder является центральным звеном в процессе создания объекта CLogify, но он делегирует настройку каждого типа обработчика специализированным строителям: CLogifyHandlerCommentBuilder, CLogifyHandlerConsoleBuilder, CLogifyHandlerDatabaseBuilder и CLogifyHandlerFileBuilder.

Каждый такой Builder-класс инкапсулирует детали настройки конкретного типа вывода логов. Это позволяет избежать смешивания ответственности и сохраняет ядро процесса сборки чистым и модульным. Давайте посмотрим на их структуру, взяв в качестве примера ConsoleBuilder.


CLogifyHandlerConsoleBuilder

Класс начинается с определения минимальной структуры для поддержки текучего интерфейса (fluent API). В своем конструкторе он получает указатель на основной Builder (CLogifyBuilder*), сохраняя ссылку на контекст сборки. Это позволяет вернуться в этот контекст с помощью метода Done() после настройки обработчика:

//+------------------------------------------------------------------+
//| class : CLogifyHandlerConsoleBuilder                             |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyHandlerConsoleBuilder                        |
//| Heritage    : No heritage                                        |
//| Description : Console handler constructor.                       |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandlerConsoleBuilder
  {
private:

   CLogifyBuilder    *m_parent;

public:
                     CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify);
                    ~CLogifyHandlerConsoleBuilder(void);

   CLogifyBuilder    *Done(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify)
  {
   m_parent = logify;
  };
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::~CLogifyHandlerConsoleBuilder(void)
  {
  }
//+------------------------------------------------------------------+
//| Finalizes the handler configuration.                             |
//+------------------------------------------------------------------+
CLogifyBuilder    *CLogifyHandlerConsoleBuilder::Done(void)
  {
   m_parent.AddHandler(GetPointer(m_handler));
   delete GetPointer(this);
   return(m_parent);
  }
//+------------------------------------------------------------------+

Done() — это точка возврата к главному строителю. Он добавляет обработчик в экземпляр CLogify и уничтожает промежуточный Builder. Это обеспечивает непрерывность цикла сборки и позволяет избежать ненужного удержания памяти в коде.

Все специализированные Builder-классы имеют одинаковую анатомию:

  • CLogifyBuilder *m_parent  ссылка на родительский Builder, используемая для возврата через метод Done().
  • CLogifyFormatter *m_formatter  экземпляр форматтера, который будет связан с обработчиком.
  • CLogifyHandlerX *m_handler  сам настраиваемый обработчик.

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

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


Полный Builder-класс для консоли (Console Builder)

Далее представлен Builder с уже реализованными методами настройки:

//+------------------------------------------------------------------+
//| class : CLogifyHandlerConsoleBuilder                             |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyHandlerConsoleBuilder                        |
//| Heritage    : No heritage                                        |
//| Description : Console handler constructor.                       |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandlerConsoleBuilder
  {
private:

   CLogifyBuilder    *m_parent;
   CLogifyFormatter  *m_formatter;
   CLogifyHandlerConsole *m_handler;

public:
                     CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify);
                    ~CLogifyHandlerConsoleBuilder(void);

   CLogifyHandlerConsoleBuilder *SetLevel(ENUM_LOG_LEVEL level);
   CLogifyHandlerConsoleBuilder *SetFormatter(string format);
   CLogifyHandlerConsoleBuilder *SetFormatter(ENUM_LOG_LEVEL level, string format);
   CLogifyBuilder    *Done(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify)
  {
   m_parent = logify;
   m_formatter = new CLogifyFormatter();
   m_handler = new CLogifyHandlerConsole();

   m_handler.SetFormatter(GetPointer(m_formatter));
  };
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::~CLogifyHandlerConsoleBuilder(void)
  {
  }
//+------------------------------------------------------------------+
//| Sets the log level for the handler.                              |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder *CLogifyHandlerConsoleBuilder::SetLevel(ENUM_LOG_LEVEL level)
  {
   m_handler.SetLevel(level);
   return(GetPointer(this));
  }
//+------------------------------------------------------------------+
//| Sets the default format string for the formatter.                |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder *CLogifyHandlerConsoleBuilder::SetFormatter(string format)
  {
   m_formatter.SetFormat(format);
   m_handler.SetFormatter(GetPointer(m_formatter));
   return(GetPointer(this));
  }
//+------------------------------------------------------------------+
//| Sets a log-level-specific format for the formatter.              |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder *CLogifyHandlerConsoleBuilder::SetFormatter(ENUM_LOG_LEVEL level, string format)
  {
   m_formatter.SetFormat(level,format);
   m_handler.SetFormatter(GetPointer(m_formatter));
   return(GetPointer(this));
  }
//+------------------------------------------------------------------+
//| Finalizes the handler configuration.                             |
//+------------------------------------------------------------------+
CLogifyBuilder    *CLogifyHandlerConsoleBuilder::Done(void)
  {
   m_parent.AddHandler(GetPointer(m_handler));
   delete GetPointer(this);
   return(m_parent);
  }
//+------------------------------------------------------------------+

С этим готово, переходим к остальным специализированным строителям.


Другие специализированные Builder-классы

CLogifyHandlerCommentBuilder

Отвечает за настройку обработчика, который выводит сообщения непосредственно на график (с помощью функции Comment()), используя структуру MqlLogifyHandleCommentConfig.

Позволяет определить:

  • SetSize(int)  количество отображаемых сообщений.
  • SetFrameStyle(ENUM_LOG_FRAME_STYLE)  стиль рамки вокруг области лога.
  • SetDirection(ENUM_LOG_DIRECTION)  вертикальная или горизонтальная ориентация.
  • SetTitle(string)  фиксированный заголовок в верхней части лога.

Также принимает методы SetLevel() и SetFormatter() — глобальные или для конкретного уровня. Настройка завершается вызовом метода Done().

CLogifyHandlerDatabaseBuilder

Настраивает обработчик сохранения данных в базе данных (пока в виде бинарной структуры). Использует MqlLogifyHandleDatabaseConfig. Предлагает:

  • SetDirectory(string)
  • SetBaseFileName(string)
  • SetMessagesPerFlush(int)

Структура идентична другим Builder-классам, что обеспечивает согласованность.

CLogifyHandlerFileBuilder

Самый полный из всех. Настраивает запись в файлы (.log, .txt и т.д.) через MqlLogifyHandleFileConfig.

Доступные опции:

  • SetDirectory(), SetFilename(), SetFileExtension()
  • SetRotationMode(): по дате, размеру или вручную
  • SetMessagesPerFlush()
  • SetCodepage(): например, CP_UTF8
  • SetFileSizeMB(), SetMaxFileCount()

А также три служебных метода с готовыми предустановками (пресетами):

  • ConfigNoRotation() — без ротации
  • ConfigDateRotation() — ротация по дате
  • ConfigSizeRotation() — ротация по размеру

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


Основной класс: CLogifyBuilder

Теперь, когда мы рассмотрели, как работают специализированные Builder-классы, пришло время взглянуть на компонент, который координирует их: класс CLogifyBuilder.

Он отвечает за создание и поддержку главного экземпляра CLogify, в который будут добавлены все обработчики. Но вместо того чтобы настраивать всё напрямую, он делегирует эту ответственность специализированным Builder-классам, каждый из которых заботится о конкретном типе обработчика. Таким образом, CLogifyBuilder действует как своего рода дирижер, управляя модульным построением логгера.

Ниже представлена полная реализация класса:

//+------------------------------------------------------------------+
//| class : CLogifyBuilder                                           |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyBuilder                                      |
//| Heritage    : No heritage                                        |
//| Description : Build CLogify objects, following the Builder design|
//|               pattern.                                           |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyBuilder
  {
private:

   CLogify           *m_logify;

public:
                     CLogifyBuilder(void);
                    ~CLogifyBuilder(void);

   CLogifyBuilder    *UseLanguage(ENUM_LANGUAGE language);

   //--- Starts configuration handlers
   CLogifyHandlerCommentBuilder *AddHandlerComment(void);
   CLogifyHandlerConsoleBuilder *AddHandlerConsole(void);
   CLogifyHandlerDatabaseBuilder *AddHandlerDatabase(void);
   CLogifyHandlerFileBuilder *AddHandlerFile(void);

   void              AddHandler(CLogifyHandler *handler);
   CLogify           *Build(void);
  };
//+------------------------------------------------------------------+

Этот класс концентрирует несколько важных функций:

  • UseLanguage(ENUM_LANGUAGE language)  позволяет установить основной язык системы логирования. Это влияет на внутренние сообщения об ошибках (через CLogifyError) и форматирование, зависящее от локализации.
  • AddHandlerX()  это точки входа для настройки обработчиков. Каждый метод (AddHandlerConsole(), AddHandlerFile() и т.д.) создает экземпляр специализированного строителя, передавая ему указатель на себя (this), чтобы впоследствии можно было вернуться к основному Builder-классу через метод Done() после завершения настройки.
  • AddHandler(CLogifyHandler *handler) — этот метод вызывается внутренне специализированными Builder-классами в конце настройки (в методе Done() ). Он регистрирует готовый обработчик в создаваемом экземпляре CLogify.
  • Build() — завершает процесс сборки, удаляет Builder из памяти с помощью delete GetPointer(this) и возвращает готовый логгер. Это подчеркивает идею о том, что экземпляр Builder-класса существует только в процессе сборки.

Благодаря такой структуре паттерн Builder реализован полностью: он модульный, понятный и расширяемый. Вы можете ознакомиться с полным кодом в прилагаемых файлах.


Элегантная точка входа

Хотя у нас уже есть конструктор CLogifyBuilder, прямой вызов этого конструктора пользователем с помощью new CLogifyBuilder() — не самый выразительный и интуитивно понятный способ начать создание логгера.

Именно поэтому мы добавили статический метод под названием Create() в класс CLogify:

//+------------------------------------------------------------------+
//| Returns an instance of the builder                               |
//+------------------------------------------------------------------+
#include "LogifyBuilder.mqh"
CLogifyBuilder *CLogify::Create(void)
  {
   return(new CLogifyBuilder());
  }
//+------------------------------------------------------------------+

А объявляется этот метод в классе CLogify следующим образом:

class CLogify
  {
public:
   static CLogifyBuilder *Create(void);
  };

Метод Create() является статическим по нескольким причинам:

  1. Он принадлежит классу, а не экземпляруу вас еще нет экземпляра CLogify, когда вы хотите начать его создание.
  2. Он не зависит от какого-либо внутреннего состояния — все, что он делает, это создает и возвращает Builder.
  3. Позволяет избежать прямого связывания с классом строителя — если завтра реализация строителя изменится, вы сможете сохранить тот же статический интерфейс в классе CLogify и обеспечить совместимость с существующим кодом.


Стандартные настройки

По мере того как библиотека Logify обретает форму, нам нужно подумать о распространенном сценарии: пользователь, который хочет быстро начать логирование, ничего не настраивая. Ему нет дела до обработчиков, языков, форматов или директорий; ему просто нужен видимый вывод сообщений во время разработки или тестирования. Чтобы предусмотреть это, мы внедрили метод EnsureDefaultHandler().

Этот метод действует как автоматический запасной вариант: если ни один обработчик не был явно настроен, он добавляет два базовых, функциональных обработчика: один для Консоли (Console), другой для комментариев на графике (Comment). Оба являются нативными для MQL5 и гарантируют немедленную видимость сообщения.

void CLogify::EnsureDefaultHandler()
  {
   //--- Check if there is no handler
   if(this.SizeHandlers() == 0)
     {
      this.AddHandler(new CLogifyHandlerConsole());
      this.AddHandler(new CLogifyHandlerComment());
     }
  }

Вызов происходит внутри метода Append(), а не в конструкторе:

bool CLogify::Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0, int code_error = 0)
  {
   //--- Ensures that there is at least one handler
   this.EnsureDefaultHandler();
   
   // (continues...)
  }

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

Перенеся эту логику в метод Append(), мы оставляем контроль в руках разработчика. Работает это так:

  • Если ни один обработчик не настроен, метод EnsureDefaultHandler() активирует оба обработчика по умолчанию при первом вызове Append().
  • Если хотя бы один обработчик был добавлен вручную, метод не делает ничего.
  • Поведение по умолчанию безопасно и обеспечивает видимость, но не вмешивается, когда есть явная конфигурация.

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

Благодаря этому Logify становится решением типа "включи и работай" (plug-and-play), не жертвуя при этом возможностью кастомизации. Это важный шаг в облегчении его внедрения как новичками, так и командами, требующими более строгих стандартов логирования.


Тестирование

Давайте сравним, каким было использование библиотеки до и после улучшений, реализованных в этой статье.

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

Ниже показаны два сценария рядом:

Старый код Новый код
//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify *logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   MqlLogifyHandleCommentConfig m_config;
   m_config.size = 5;
   m_config.frame_style = LOG_FRAME_STYLE_SINGLE;
   m_config.direction = LOG_DIRECTION_UP;
   m_config.title = "Expert name";

   CLogifyFormatter *formatter = new CLogifyFormatter("{date_time} [{levelname}]: {msg}");
   formatter.SetFormat(LOG_LEVEL_ERROR,"{date_time} [{levelname}]: {msg} [{err_constant} | {err_code} | {err_description}]");

   CLogifyHandlerComment *handler_comment = new CLogifyHandlerComment();
   handler_comment.SetConfig(m_config);
   handler_comment.SetLevel(LOG_LEVEL_DEBUG);
   handler_comment.SetFormatter(formatter);

   CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole();
   handler_console.SetLevel(LOG_LEVEL_DEBUG);
   handler_console.SetFormatter(formatter);

   logify = new CLogify();
   logify.AddHandler(handler_comment);
   logify.AddHandler(handler_console);

   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");

   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   delete logify;
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify *logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   logify = new CLogify();
   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   delete logify;
  }
//+------------------------------------------------------------------+

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

Для случаев, когда нам нужен больший контроль над поведением лога, в игру вступает новый конструктор (Builder). Он предоставляет "текучий" интерфейс (fluent interface) и, что самое важное, является на 100% типизированным. Это означает, что сам редактор кода предлагает доступные методы в реальном времени, сокращая количество ошибок и устраняя необходимость запоминать сигнатуры функций.

Когда вы печатаете logify.Create().AddHandler, редактор уже предлагает все доступные обработчики:

А когда вы продолжаете ввод с .AddHandlerComment(), отображаются только допустимые настройки для этого конкретного типа обработчика:

Вот так выглядит код в итоге, настраивающий обработчик комментариев с определенным форматом для ошибок.

//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify *logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   logify = logify.Create().AddHandlerComment().SetLevel(LOG_LEVEL_DEBUG).SetFormatter(LOG_LEVEL_ERROR,"{date_time} [{levelname}] {msg} ({err_constant} {err_code}: {err_description})").SetTitle("My expert").SetSize(5).Done().Build();
//---
   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   delete logify;
  }
//+------------------------------------------------------------------+

Такой подход снижает неопределенность и упрощает разработку. Код получается чистым, понятным и защищенным от ошибок.


Исправление для MetaTrader 5 build 5100 и выше

С выходом сборки 5100 терминала MetaTrader 5 некоторые внутренние изменения в компиляторе потребовали большей ясности при обработке типов в вызовах таких функций, как DatabaseColumnLong() и DatabaseColumnInteger().

На практике это означает, что больше небезопасно напрямую передавать ссылки на поля структур (например, data[size].timestamp) в эти функции. Чтобы избежать ошибок компиляции, идеально сначала сохранить значение во временной переменной соответствующего типа и только затем передавать его по ссылке в функцию.

Старый код Новый код
//+------------------------------------------------------------------+
//| Get data by sql command                                          |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[])
  {
   //--- The rest of the method code remains the same

   //--- Reads query results line by line
   for(int i=0;DatabaseRead(request);i++)
     {
      int size = ArraySize(data);
      ArrayResize(data,size+1,size);
      
      //--- Maps database data to the MqlLogifyModel model
      DatabaseColumnText(request,1,data[size].formated);
      DatabaseColumnText(request,2,data[size].levelname);
      DatabaseColumnText(request,3,data[size].msg);
      DatabaseColumnText(request,4,data[size].args);
      DatabaseColumnLong(request,5,data[size].timestamp);
      string value;
      DatabaseColumnText(request,6,value);
      data[size].date_time = StringToTime(value);
      DatabaseColumnInteger(request,7,data[size].level);
      DatabaseColumnText(request,8,data[size].origin);
      DatabaseColumnText(request,9,data[size].filename);
      DatabaseColumnText(request,10,data[size].function);
      DatabaseColumnLong(request,11,data[size].line);
     }
   
   //--- The rest of the method code remains the same
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Get data by sql command                                          |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[])
  {
   //--- The rest of the method code remains the same

   //--- Reads query results line by line
   for(int i=0;DatabaseRead(request);i++)
     {
      int size = ArraySize(data);
      ArrayResize(data,size+1,size);
      
      //--- Maps database data to the MqlLogifyModel model
      DatabaseColumnText(request,1,data[size].formated);
      DatabaseColumnText(request,2,data[size].levelname);
      DatabaseColumnText(request,3,data[size].msg);
      DatabaseColumnText(request,4,data[size].args);
      long timestamp = (long)data[size].timestamp;
      DatabaseColumnLong(request,5,timestamp);
      string value;
      DatabaseColumnText(request,6,value);
      data[size].date_time = StringToTime(value);
      int level = data[size].level;
      DatabaseColumnInteger(request,7,level);
      DatabaseColumnText(request,8,data[size].origin);
      DatabaseColumnText(request,9,data[size].filename);
      DatabaseColumnText(request,10,data[size].function);
      long line = (long)data[size].line;
      DatabaseColumnLong(request,11,line);
     }
   
   //--- The rest of the method code remains the same
  }
//+------------------------------------------------------------------+

Остальная часть кода остается без изменений. Это разовая корректировка, но необходимая для поддержания совместимости с последними версиями терминала.

Стоит помнить, что такого рода правки обычны, когда компилятор становится более требовательным к типам, и это обычно делается для предотвращения трудноуловимых проблем во время выполнения. Поэтому, хотя это может показаться простым изменением, это важное обновление, гарантирующее, что Logify продолжит стабильно работать в новых версиях MetaTrader 5.


Заключение

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

В этой части статьи мы решили эту проблему. Мы создали новый способ работы с логгером: простой, понятный и быстрый. В дело вступил паттерн Builder, чтобы сделать всё более естественным: вы печатаете logify.Create(), и сам редактор показывает вам следующие опции. Нужен обработчик для комментариев на графике? Вводите AddHandlerComment(). Хотите изменить заголовок? Появляется метод SetTitle(). Вам не нужно ничего запоминать, не нужно возвращаться к документации. Просто следуйте потоку.

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

Наконец, мы подкорректировали одну важную техническую деталь: с выходом сборки 5100 MetaTrader 5 компилятор стал строже относиться к передаче ссылок в таких функциях, как DatabaseColumnLong() и DatabaseColumnInteger(). Чтобы обеспечить совместимость, мы добавили небольшие исправления в CLogifyHandlerDatabase, используя промежуточные переменные перед передачей данных в эти функции. Для тех, кто использует библиотеку, ничего не меняется, но за кулисами она остается стабильной даже после обновлений терминала.

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

Название файла Описание
Experts/Logify/LogiftTest.mq5
Файл, в котором мы тестируем возможности библиотеки, содержащий практический пример
Include/Logify/Error/Languages/ErrorMessages.XX.mqh Файл с сообщениями об ошибках для каждого языка, где X — код языка
Include/Logify/Error/Error.mqh
Структура данных для хранения ошибок
Include/Logify/Error/LogifyError.mqh
Класс для получения детальной информации об ошибках
Include/Logify/Formatter/LogifyFormatter.mqh
Класс, отвечающий за форматирование записей лога, заменяющий плейсхолдеры на конкретные значения
Include/Logify/Handlers/LogifyHandler.mqh
Базовый класс для управления обработчиками логов, включая установку уровня и отправку логов
Include/Logify/Handlers/LogifyHandlerComment.mqh
Обработчик логов, который отправляет отформатированные логи напрямую в комментарий на графике терминала MetaTrader
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/LogifyBuilder.mqh Класс, отвечающий за создание объекта CLogify, упрощающий его настройку
Include/Logify/LogifyLevel.mqh Файл, определяющий уровни логирования библиотеки Logify, позволяющий осуществлять детальный контроль
Include/Logify/LogifyModel.mqh Структура, моделирующая записи логов, включающая такие детали, как уровень, сообщение, временная метка и контекст

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

Прикрепленные файлы |
Logify.zip (154.39 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (7)
Spoxus Spoxus
Spoxus Spoxus | 2 июл. 2025 в 21:16

Я задаюсь вопросом, хочу ли я выводить только сообщения об ошибках и отладке. А у меня все Info, Alert и т.д. встроены в советник. Может быть, установить значение bool для каждого типа в 'enum ENUM_LOG_LEVEL', чтобы показать, что мы хотим?

В производственном коде, если мы отключим некоторые из журналов, они не должны компилироваться в финальный файл ex5.

joaopedrodev
joaopedrodev | 31 июл. 2025 в 16:58
Spoxus Spoxus производственном коде, если мы отключим некоторые из журналов, они не должны компилироваться в финальный файл ex5.

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

//+------------------------------------------------------------------+
//| Импорт|
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify Logify;
//+------------------------------------------------------------------+
//| Входы|
//+------------------------------------------------------------------+
input ENUM_LOG_LEVEL InpLogLevel = LOG_LEVEL_INFO; // Уровень журнала
//+------------------------------------------------------------------+
//| Функция инициализации эксперта|
//+------------------------------------------------------------------+
int OnInit()
  {
   Logify.EnsureDefaultHandler();
   Logify.GetHandler(0).SetLevel(InpLogLevel);
   
   Logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   Logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   Logify.Error("Failed to send sell order", 10016,"Order Management");
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

В результате будут отображаться только сообщения с уровнем серьезности, большим или равным заданному в обработчике.

joaopedrodev
joaopedrodev | 31 июл. 2025 в 16:59
hini #:

Язык вывода журнала ошибок по умолчанию может быть возвращен на язык терминала пользователя в соответствии с этим кодом

Часть 10 этой статьи находится в очереди на публикацию. В ней рассматривается способ подавления идентичных журналов, а также задается язык по умолчанию для терминала. Спасибо за предложение!

Amy Liu
Amy Liu | 4 янв. 2026 в 13:41
Это лучшая статья, которую я когда-либо видел - я в восторге!
Amy Liu
Amy Liu | 4 янв. 2026 в 13:52
Spoxus Spoxus производственном коде, если мы отключим некоторые из журналов, они не должны компилироваться в финальный файл ex5.
joaopedrodev #:

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

В результате будут отображаться только сообщения с уровнем серьезности, большим или равным заданному в обработчике.

Автор показывает, как изменить уровень во время выполнения программы без модификации кода.

Я думаю, что он просто хочет показать только один уровень журнала. Для этого ему нужно изменить код, как показано ниже:

//--- E.G.
void CLogifyHandlerComment::Emit(MqlLogifyModel &data)
  {
   //--- Проверьте, разрешен ли уровень журнала
   if(data.level != this.GetLevel()) // замените '<' на '!='
     {
      return;
     }

   //--- Журналы смен для сохранения истории
   for(int i = m_config.size-1; i > 0; i--)
     {
      m_cache[i] = m_cache[i-1];
     }
   m_cache[0] = data;

   //--- Постройте полный комментарий
   string comment = BuildHeader();
   comment += FormatLogLines();
   comment += BuildFooter();

   //--- Отображение на графике
   Comment(comment);
  }
Моделирование рынка (Часть 22): Первые шаги на SQL (V) Моделирование рынка (Часть 22): Первые шаги на SQL (V)
Прежде, чем вы сдадитесь и решите отказаться от изучения SQL, позвольте мне напомнить вам, уважаемые читатели, что здесь мы всё ещё используем только самые базовые элементы. Мы ещё не рассмотрели некоторые возможности SQL. Как только вы их усвоите, вы увидите, что SQL гораздо практичнее, чем кажется. Хотя, скорее всего, мы в конечном итоге изменим направление того, что мы создаем, потому, что процесс создания является динамичным. Мы покажем немного больше о создании разных вещей в SQL, ведь это по настоящему важно и нужно вам. Просто думать, что вы более способны, чем целое сообщество программистов и разработчиков, приведет только к потере времени и возможностей. Не переживайте, потому что дальше будет ещё интереснее.
Знакомство с языком MQL5 (Часть 39): Руководство для начинающих по работе с файлами в MQL5 (I) Знакомство с языком MQL5 (Часть 39): Руководство для начинающих по работе с файлами в MQL5 (I)
В этой статье работа с файлами в MQL5 рассматривается на практическом проектном примере. Вы будете использовать FileSelectDialog, чтобы выбрать или создать CSV-файл, открыть его с помощью FileOpen и записать структурированные заголовки с данными счета, такие как имя счета, баланс, логин, диапазон дат и время последнего обновления. В результате вы получите понятную основу для пригодного к повторному использованию торгового журнала и безопасной работы с файлами в MetaTrader 5.
Знакомство с языком MQL5 (Часть 40): Руководство для начинающих по работе с файлами в MQL5 (II) Знакомство с языком MQL5 (Часть 40): Руководство для начинающих по работе с файлами в MQL5 (II)
В этой статье вы создадите торговый журнал в формате CSV с помощью MQL5, считывая историю счета за заданный период и записывая в файл структурированные записи. В статье объясняется, как подсчитывать сделки, получать тикеты, определять символ и тип ордера, а также с помощью динамических массивов собирать данные о входе в сделку (лот, время, цена, SL/TP) и выходе из нее (время, цена, прибыль, результат). В результате получается упорядоченный журнал, который сохраняется между запусками программы и подходит для анализа и отчетности.
Возможности Мастера MQL5, которые вам нужно знать (Часть 71): Использование паттернов MACD и OBV Возможности Мастера MQL5, которые вам нужно знать (Часть 71): Использование паттернов MACD и OBV
Осциллятор схождения-расхождения скользящих средних (Moving-Average-Convergence-Divergence, MACD) и индикатор балансового объема (On-Balance-Volume, OBV) - еще одна пара индикаторов, которые можно использовать совместно в советнике MQL5. Как это принято в данной серии статей, данная комбинация индикаторов дополняет друг друга: MACD подтверждает тренды, а OBV проверяет объем. Как обычно, мы используем Мастер MQL5 для построения паттернов и тестирования потенциала, который может иметь эта пара индикаторов.