English 中文 Deutsch 日本語
preview
Искусство работы с логами (Часть 8): Самопереводящиеся записи об ошибках

Искусство работы с логами (Часть 8): Самопереводящиеся записи об ошибках

MetaTrader 5Примеры |
176 6
joaopedrodev
joaopedrodev

Введение

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

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

logify.Error("Failed to send sell order", "Order Management", "Code: "+IntegerToString(GetLastError()));
// Console:
// 2025.06.06 10:30:00 [ERROR]: (Order Management) Failed to send sell order | Code: 10016

Результат? Расплывчатое сообщение. Мы знаем, где произошла ошибка, но не знаем, почему. И кто хоть раз сталкивался с необходимостью искать десятки кодов MQL5 в документации? Раньше я сам довольно часто делал так: получив код ошибки, мне приходилось лезть в документацию, чтобы выяснить, что же на самом деле произошло. Именно это неудобство и натолкнуло меня на идею: а что, если Logify сможет интерпретировать ошибку за меня? Что, если вместо простого кода он будет выдавать мне его значение в ясной, контекстуализированной форме, готовой для записи в лог?

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

logify.Error("Failed to send sell order", GetLastError(), "Order Management");
// Console:
// 2025.06.06 10:30:00 [ERROR]: (Order Management) Failed to send sell order | 10016: Invalid stops in the request

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

На этом этапе вы узнаете, как:

  1. Обогащать логи ошибок точными описаниями, взятыми непосредственно из документации MQL5.
  2. Отображать сообщения об ошибках на нескольких языках с динамическим выбором наиболее подходящего языка для каждого контекста.
  3. Настраивать форматирование в зависимости от уровня логирования, создавая различные шаблоны для ошибок, предупреждений и информационных сообщений на основе их степени критичности.

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


Создание способа обработки ошибок

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

  • Числовой код (code)
  • Символическая константа
  • Понятное описание (description)

Мы создаем эту структуру в файле <Include/Logify/Error/Error.mqh> file:

//+------------------------------------------------------------------+
//| Data structure for error handling                                |
//+------------------------------------------------------------------+
struct MqlError
  {
   int      code;          // Cod of error
   string   description;   // Description of error
   string   constant;      // Type error
   
   MqlError::MqlError(void)
     {
      code = 0;
      description = "";
      constant = "";
     }
  };
//+------------------------------------------------------------------+

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

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

Стратегия здесь заключается в создании файла .mqh для каждого языка, содержащего функцию, которая инициализирует массив всеми известными ошибками. Название файла следует стандарту ErrorMessages.xx.mqh, где xx обозначает аббревиатуру языка (например, en для английского, de для немецкого, pt для португальского).

Вот как выглядит файл на английском языке:

//+------------------------------------------------------------------+
//|                                             ErrorMessages.en.mqh |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/en/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/en/users/joaopedrodev"
//+------------------------------------------------------------------+
//| Import struct                                                    
//+------------------------------------------------------------------+
#include "../Error.mqh"
void InitializeErrorsEnglish(MqlError &errors[])
  {
   //--- Free and resize
   ArrayFree(errors);
   ArrayResize(errors,274);
   
   //+------------------------------------------------------------------+
   //| Unknown error                                                    |
   //+------------------------------------------------------------------+
   errors[0].code = 0;
   errors[0].description = "No error found";
   errors[0].constant = "ERROR_UNKNOWN";
   //+------------------------------------------------------------------+
   //| Server error                                                     |
   //+------------------------------------------------------------------+
   errors[1].code = 10004;
   errors[1].description = "New quote";
   errors[1].constant = "TRADE_RETCODE_REQUOTE";
   //---
   errors[2].code = 10006;
   errors[2].description = "Request rejected";
   errors[2].constant = "TRADE_RETCODE_REJECT";
   //---
   // Remaining error codes...
   //---
   errors[272].code = 5625;
   errors[272].description = "Parameter binding error, wrong index";
   errors[272].constant = "ERR_DATABASE_RANGE";
   //---
   errors[273].code = 5626;
   errors[273].description = "Open file is not a database file";
   errors[273].constant = "ERR_DATABASE_NOTADB";
  }
//+------------------------------------------------------------------+

Функция InitializeErrorsEnglish() получает массив MqlError и заполняет его всеми известными ошибками на английском языке. Мы повторяем этот шаблон для других языков, таких как немецкий, испанский, французский, итальянский, японский, корейский, португальский, русский, турецкий и китайский. Все эти файлы хранятся в папке <Include/Logify/Error/Languages>. В общей сложности у нас есть 11 языковых файлов и 274 записи в каждом файле. И да, это была кропотливая работа, но теперь вы можете наслаждаться ею простым подключением. Помните, что весь код прилагается в конце статьи.

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

Интерфейс класса прост:

//+------------------------------------------------------------------+
//| class : CLogifyError                                             |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyError                                        |
//| Heritage    : No heritage                                        |
//| Description : class to look up the error code and return details |
//|               of each error code.                                |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyError
  {
private:
   
   ENUM_LANGUAGE     m_language;
   MqlError          m_errors[];
   
public:
                     CLogifyError(void);
                    ~CLogifyError(void);
   
   //--- Set/Get
   void              SetLanguage(ENUM_LANGUAGE language);
   ENUM_LANGUAGE     GetLanguage(void);
   
   //--- Get error
   MqlError          Error(int code);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyError::CLogifyError()
  {
   InitializeErrorsEnglish(m_errors);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyError::~CLogifyError(void)
  {
  }
//+------------------------------------------------------------------+

При создании экземпляра класс загружает ошибки на английском языке по умолчанию. Вы можете изменить язык в любое время с помощью SetLanguage(), и библиотека перезагрузит данные на соответствующем языке.

Метод Error(int code) — это сердце класса. Он просматривает массив ошибок в поисках введенного кода и возвращает соответствующую структуру MqlError. Если код не найден, функция возвращает общую ошибку в качестве запасного варианта. Кроме того, он автоматически определяет, находится ли код ошибки в диапазоне, зарезервированном для пользовательских ошибок (от ERR_USER_ERROR_FIRST до ERR_USER_ERROR_LAST), и возвращает ответ также и для них.

//+------------------------------------------------------------------+
//| Returns error information based on the error code received       |
//+------------------------------------------------------------------+
MqlError CLogifyError::Error(int code)
  {
   int size = ArraySize(m_errors);
   for(int i=0;i<size;i++)
     {
      if(m_errors[i].code == code)
        {
         //--- Return
         return(m_errors[i]);
        }
     }
   
   //--- User error
   if(code >= ERR_USER_ERROR_FIRST && code < ERR_USER_ERROR_LAST)
     {
      MqlError error;
      error.code = code;
      error.constant = "User error";
      error.description = "ERR_USER_ERROR";
      
      //--- Return
      return(m_errors[274]);
     }
   
   //--- Return
   return(m_errors[0]);
  }
//+------------------------------------------------------------------+

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

#include "LogifyLevel.mqh"
#include "Error/Error.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct MqlLogifyModel
  {
   string formated;        // The log message formatted according to the specified format.
   string levelname;       // Textual name of the log level (e.g., "DEBUG", "INFO")
   string msg;             // Main content of the log message
   string args;            // Additional arguments associated with the log message
   ulong timestamp;        // Timestamp of the log event, represented in seconds since the start of the Unix epoch
   datetime date_time;     // Date and time of the log event, in datetime format
   ENUM_LOG_LEVEL level;   // Enumeration representing the severity level of the log
   string origin;          // Source or context of the log message (e.g., class or module)
   string filename;        // Name of the source file where the log message was generated
   string function;        // Name of the function where the log was called
   ulong line;             // Line number in the source file where the log was generated
   MqlError error;         // Error data
   
   void MqlLogifyModel::Reset(void)
     {
      formated = "";
      levelname = "";
      msg = "";
      args = "";
      timestamp = 0;
      date_time = 0;
      level = LOG_LEVEL_DEBUG;
      origin = "";
      filename = "";
      function = "";
      line = 0;
     }
   
   MqlLogifyModel::MqlLogifyModel(void)
     {
      this.Reset();
     }
   MqlLogifyModel::MqlLogifyModel(string _formated,string _levelname,string _msg,string _args,ulong _timestamp,datetime _date_time,ENUM_LOG_LEVEL _level,string _origin,string _filename,string _function,ulong _line,MqlError &_error)
     {
      formated = _formated;
      levelname = _levelname;
      msg = _msg;
      args = _args;
      timestamp = _timestamp;
      date_time = _date_time;
      level = _level;
      origin = _origin;
      filename = _filename;
      function = _function;
      line = _line;
      error = _error;
     }
  };
//+------------------------------------------------------------------+

С этими изменениями структура нашей библиотеки выглядит следующим образом:


Интеграция ошибок в основной класс

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

Пришло время интегрировать систему ошибок в ядро Logify, чтобы каждая создаваемая запись журнала содержала, при необходимости, четкое многоязычное объяснение того, что пошло не так. Для этого мы выполним три шага: создадим экземпляр класса ошибок, настроим метод для добавления записей (Append) и, наконец, настроим форматтер для распознавания новых плейсхолдеров {err_code}, {err_constant} и {err_description}.

1. Добавление интеллекта ошибок в сердце Logify

Класс CLogify отвечает за связь форматирования, обработки и отправки лога, поэтому первый логический шаг — предоставить ему прямой доступ к нашей системе ошибок. Для этого достаточно импортировать файл LogifyError.mqh и создать экземпляр класса CLogifyError в качестве закрытого члена CLogify. Это гарантирует, что он всегда будет доступен внутри, не требуя от пользователя библиотеки беспокоиться об этом. Это небольшое дополнение уже открывает важные возможности. Теперь, когда бы ни произошла ошибка, мы можем быстро получить ее код, описание и константу.

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

#include "LogifyModel.mqh"
#include "Handlers/LogifyHandler.mqh"
#include "Handlers/LogifyHandlerComment.mqh"
#include "Handlers/LogifyHandlerConsole.mqh"
#include "Handlers/LogifyHandlerDatabase.mqh"
#include "Handlers/LogifyHandlerFile.mqh"
#include "Error/LogifyError.mqh"
//+------------------------------------------------------------------+
//| class : CLogify                                                  |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : Logify                                             |
//| Heritage    : No heritage                                        |
//| Description : Core class for log management.                     |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogify
  {
private:
   CLogifyError      m_error;
         
public:
                     CLogify();
                    ~CLogify();
   
   //--- Language
   void              SetLanguage(ENUM_LANGUAGE language);
   ENUM_LANGUAGE     GetLanguage(void);
  };
//+------------------------------------------------------------------+

2. Обогащение метода Append кодом ошибки

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

bool Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0, int code_error = 0);

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

Теперь, имея на руках код ошибки, мы используем метод Error() класса CLogifyError, чтобы получить соответствующую структуру MqlError:

//+------------------------------------------------------------------+
//| 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,int code_error=0)
  {
   //--- Ensures that there is at least one handler
   this.EnsureDefaultHandler();
   
   //--- Textual name of the log level
   string levelStr = "";
   switch(level)
     {
      case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break;
      case LOG_LEVEL_INFO : levelStr = "INFO"; 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,m_error.Error(code_error));
   
   //--- Call handlers
   int size = this.SizeHandlers();
   for(int i=0;i<size;i++)
     {
      data.formated = m_handlers[i].GetFormatter().Format(data);
      m_handlers[i].Emit(data);
     }
   
   return(true);
  }
//+------------------------------------------------------------------+

Теперь объект MqlLogifyModel несет в себе не только информацию лога, но и суть ошибки, которая его вызвала. Теперь осталось добавить перегруженную версию метода для методов Error() и Fatal(), не удаляя существующие, так как это может привести к ошибкам — пользователю библиотеки не всегда нужно добавлять код ошибки. Вот как выглядит код:

class CLogify
  {
public:
   //--- Specific methods for each log level
   bool              Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Info(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              Error(string msg, int code_error, 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);
   bool              Fatal(string msg, int code_error, string origin = "", string args = "",string filename="",string function="",int line=0);
  };
bool CLogify::Error(string msg, int code_error, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   return(this.Append(LOG_LEVEL_ERROR,msg,origin,args,filename,function,line,code_error));
  }
bool CLogify::Fatal(string msg, int code_error, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   return(this.Append(LOG_LEVEL_FATAL,msg,origin,args,filename,function,line,code_error));
  }

3. Форматирование сообщения с использованием плейсхолдеров ошибки

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

//+------------------------------------------------------------------+
//| Format logs                                                      |
//+------------------------------------------------------------------+
string CLogifyFormatter::FormatLog(MqlLogifyModel &data)
  {
   string formated = m_log_format;
   
   StringReplace(formated,"{levelname}",data.levelname);
   StringReplace(formated,"{msg}",data.msg);
   StringReplace(formated,"{args}",data.args);
   StringReplace(formated,"{timestamp}",IntegerToString(data.timestamp));
   StringReplace(formated,"{date_time}",this.FormatDate(data.date_time));
   StringReplace(formated,"{level}",IntegerToString(data.level));
   StringReplace(formated,"{origin}",data.origin);
   StringReplace(formated,"{filename}",data.filename);
   StringReplace(formated,"{function}",data.function);
   StringReplace(formated,"{line}",IntegerToString(data.line));
   
   if(data.level >= LOG_LEVEL_ERROR)
     {
      StringReplace(formated,"{err_code}",IntegerToString(data.error.code));
      StringReplace(formated,"{err_constant}",data.error.constant);
      StringReplace(formated,"{err_description}",data.error.description);
     }
   else
     {
      StringReplace(formated,"{err_code}","");
      StringReplace(formated,"{err_constant}","");
      StringReplace(formated,"{err_description}","");
     }
   
   return(formated);
  }
//+------------------------------------------------------------------+

Итак, мы создали три новых хука для настройки:

  • {err_code} : отображает точный код ошибки (например, 10006 )
  • {err_constant} : соответствующая константа (например, TRADE_RETCODE_REJECT).
  • {err_description} : понятное описание (например, Request rejected )

Представим, что мы определили глобальный формат вот так:

"{date_time} [{levelname}] {msg} ({err_constant} {err_code}: {err_description})"

По идее, это должно давать что-то вроде:

2025.06.09 14:22:15 [ERROR] Failed to send order (10016 TRADE_RETCODE_REJECT: Request rejected)

И действительно, для уровней логирования ERROR и FATAL это работает безупречно. Но что произойдет, если мы используем тот же формат для логов DEBUG, INFO или ALERT? Результат будет примерно таким:

2025.06.09 14:22:15 [INFO] Operation completed successfully (  : )

Или даже хуже: в зависимости от форматирования это может привести к визуальному смещению в записях логов. Несмотря на все наши старания очистить плейсхолдеры ошибок для младших уровней, проблема остается: формат был создан в расчете на то, что {err_code}, {err_constant} и {err_description} будут иметь значения. Когда их нет, лог выглядит... странно и неполноценно. Мы сталкиваемся с нарушением семантики: одна и та же маска форматирования применяется к сообщениям, имеющим совершенно разный контекст.

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

Поэтому вместо CLogifyFormatter с универсальным форматом мы создаем отдельный форматтер для каждого уровня (DEBUG, INFO, ALERT, ERROR, FATAL). Это позволяет настроить, например:

  • DEBUG: показывать функцию, файл и строку
  • INFO: быть минималистичным — только что произошло
  • ALERT: добавлять контекст, например, источник и аргументы
  • ERROR: включать данные об ошибке
  • FATAL: быть максимально полным


Реализация нескольких форматов

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

Старый код Новый код
class CLogifyFormatter
  {
private:
   
   //--- Stores the formats
   string            m_date_format;
   string            m_log_format;
  };
class CLogifyFormatter
  {
private:
   
   //--- Stores the formats
   string            m_date_format;
   string            m_format[5];
  };

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

С разделением форматов по уровням нам нужен удобный способ их настройки. Для этого мы создали два метода SetFormat: один применяет одинаковый формат ко всем уровням, а другой позволяет индивидуально настроить конкретный уровень.

class CLogifyFormatter
  {
public:
   void              SetFormat(string format);
   void              SetFormat(ENUM_LOG_LEVEL level, string format);
  };
//+------------------------------------------------------------------+
//| Sets the format for all levels                                   |
//+------------------------------------------------------------------+
void CLogifyFormatter::SetFormat(string format)
  {
   m_format[LOG_LEVEL_DEBUG] = format;
   m_format[LOG_LEVEL_INFO] = format;
   m_format[LOG_LEVEL_ALERT] = format;
   m_format[LOG_LEVEL_ERROR] = format;
   m_format[LOG_LEVEL_FATAL] = format;
  }
//+------------------------------------------------------------------+
//| Sets the format to a specific level                              |
//+------------------------------------------------------------------+
void CLogifyFormatter::SetFormat(ENUM_LOG_LEVEL level, string format)
  {
   m_format[level] = format;
  }
//+------------------------------------------------------------------+

Такая реализация не только удобна, но и позволяет нам быть одновременно универсальными и точными. Для тех, кому нужна простота, достаточно одного вызова SetFormat(...). Для тех, кто стремится к точности, можно просто вызвать SetFormat(level, format) для каждого нужного уровня.

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

Старый код Новый код
string CLogifyFormatter::FormatLog(MqlLogifyModel &data)
  {
   string formated = m_log_format;
   ...
  }
string CLogifyFormatter::Format(MqlLogifyModel &data)
  {
   string formated = m_format[data.level];
   ...
  }

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


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

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

  • Визуальный обработчик через Comment() с рамкой, заголовком и ограничением на количество строк.
  • Консольный обработчик через Print() для записи сообщений во внутренний лог платформы.
  • Один форматтер, общий для обоих обработчиков.
  • Четыре лог-сообщения разных уровней: два DEBUG, одно INFO и одно ERROR с кодом ошибки.
//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify logify;
//+------------------------------------------------------------------+
//| Expert initialization                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Handler config
   MqlLogifyHandleCommentConfig m_config;
   m_config.size = 5;                                      // Max log lines
   m_config.frame_style = LOG_FRAME_STYLE_SINGLE;          // Frame style
   m_config.direction = LOG_DIRECTION_UP;                  // Log direction (up)
   m_config.title = "Expert name";                         // Log panel title
   
   CLogifyFormatter *formatter = new CLogifyFormatter("{date_time} [{levelname}]: {msg}");
   formatter.SetFormat(LOG_LEVEL_ERROR,"{date_time} [{levelname}]: {msg} [{err_constant} | {err_code} | {err_description}]");
   
   //--- Create and configure handler
   CLogifyHandlerComment *handler_comment = new CLogifyHandlerComment();
   handler_comment.SetConfig(m_config);
   handler_comment.SetLevel(LOG_LEVEL_DEBUG);              // Min log level
   handler_comment.SetFormatter(formatter);
   
   CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole();
   handler_console.SetLevel(LOG_LEVEL_DEBUG);              // Min log level
   handler_console.SetFormatter(formatter);
   
   //--- Add handler to Logify
   logify.AddHandler(handler_comment);
   logify.AddHandler(handler_console);
   
   //--- Test logs
   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);
  }
//+------------------------------------------------------------------+

Когда вы запустите этот код, консоль MetaTrader отобразит следующие сообщения, отформатированные именно так, как мы и ожидали. Визуальный результат наглядно показывает, что младшие уровни (такие как DEBUG и INFO) следуют более лаконичному формату, в то время как ERROR задействует более детальный формат с данными об ошибке.

09/06/2025 14:22:31 [DEBUG]: Initializing Expert Advisor...
09/06/2025 14:22:31 [DEBUG]: RSI indicator value calculated: 72.56
09/06/2025 14:22:31 [INFO]: Buy order sent successfully
09/06/2025 14:22:31 [ERROR]: Failed to send sell order [TRADE_DISABLED | 10016 | Trade operations not allowed]

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


Заключение

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

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

Если вы дошли до этого момента, вы узнали, как:

  • Проектировать масштабируемую и слабосвязанную структуру логирования;
  • Интегрировать многоязычные сообщения об ошибках на основе кодов MetaTrader;
  • Работать с заполнителями и форматированием в зависимости от уровня логирования;
  • Создавать многоразовые обработчики с собственными конфигурациями;

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


Название файла Описание
Experts/Logify/LogifyTest.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/LogifyLevel.mqh Файл, определяющий уровни журналирования библиотеки Logify, что позволяет осуществлять детальный контроль
Include/Logify/LogifyModel.mqh Структура, которая моделирует записи журнала, включая такие детали, как уровень, сообщение, временная метка и контекст

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

Прикрепленные файлы |
Logify.zip (151.36 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (6)
joaopedrodev
joaopedrodev | 20 июн. 2025 в 12:44
Спасибо, я рад помочь. Любые предложения по улучшению, пожалуйста, свяжитесь со мной!
hini
hini | 20 июн. 2025 в 17:42
joaopedrodev #:
Спасибо, я рад помочь. Любые предложения по улучшению, пожалуйста, свяжитесь со мной!
Что делать, если логгирование происходит очень часто, но я хочу избежать повторных отпечатков? Например, когда обнаружен сигнал на открытие сделки, но спред слишком велик и сохраняется в течение десятков секунд - при использовании логгирования он будет выводиться не менее десятков, а то и сотен раз. Как можно разрешить эту ситуацию? Я знаю, что одним из решений является использование переменной для обеспечения отображения журнала только один раз, но было бы лучше, если бы сама библиотека логирования могла справиться с этой задачей. Не могли бы вы дать несколько предложений?
hini
hini | 20 июн. 2025 в 17:54

Я скомпилировал библиотеку регистрации Logify, и после MT5 build 5100 появилось несколько ошибок компиляции, связанных с типами в CLogifyHandlerDatabase::Query. Я полагаю, что вы уже должны были решить эту проблему.
joaopedrodev
joaopedrodev | 23 июн. 2025 в 13:24
Спасибо за предложения, я рассмотрю их в будущих статьях.
Carl Schreiber
Carl Schreiber | 26 сент. 2025 в 20:01
joaopedrodev #:
Спасибо за предложения, я учту их в будущих статьях.

Небольшая идея по поводу языков.

Возможно, расположить оригинальные английские сообщения таким образом, чтобы их можно было легко скопировать и вставить в переводчик (deepl.com или translate.google.com) и поместить результат обратно в программу. Таким образом, каждый может легко настроить программу на свой язык.

Deeple распознает 36 языков, Google - около 130.

Торговые инструменты на языке MQL5 (Часть 9): Мастер первого запуска для советников с прокручиваемым руководством Торговые инструменты на языке MQL5 (Часть 9): Мастер первого запуска для советников с прокручиваемым руководством
В этой статье мы разрабатываем мастер первоначальной пользовательской настройки в MQL5 для советников, включающий прокручиваемое руководство с интерактивной панелью, динамическое форматирование текста и визуальные элементы управления, такие как кнопки и флажки, позволяющие пользователям эффективно перемещаться по инструкциям и настраивать торговые параметры. Пользователи программы получают представление о том, что представляет собой программа и что нужно делать при первом запуске, что больше похоже на ориентирующий сценарий.
Переосмысливаем классические стратегии (Часть 13): Обновление стратегии по пересечению скользящих (Часть 2) Переосмысливаем классические стратегии (Часть 13): Обновление стратегии по пересечению скользящих (Часть 2)
Мы попробуем внедрить дополнительные улучшения в нашу стратегию по пересечению скользящих средних, чтобы постараться снизить задержку и повысить надежность за счет дополнительного анализа данных. Как мы знаем, проецирование данных в многомерное пространство иногда может улучшить производительность моделей машинного обучения. Давайте посмотрим, что это на практике означает для нас, трейдеров. Также увидим, как можно использовать этот эффективный принцип в терминале MetaTrader 5.
Знакомство с языком MQL5 (Часть 38): Освоение API и функции WebRequest в языке MQL5 (XII) Знакомство с языком MQL5 (Часть 38): Освоение API и функции WebRequest в языке MQL5 (XII)
Создайте практический мост между MetaTrader 5 и Binance: получайте 30-минутные свечи с помощью WebRequest, извлекайте из JSON значения OHLC и времени и подтверждайте бычий паттерн поглощения, используя только полностью закрытые свечи. Затем соберите строку запроса, вычислите подпись HMAC-SHA256, добавьте X-MBX-APIKEY и отправьте аутентифицированные ордера. Вы получите четкий сквозной рабочий процесс советника – от получения данных до исполнения ордера.
Нейросети в трейдинге: Единая архитектура взаимодействия рыночных признаков и торгового контекста (Окончание) Нейросети в трейдинге: Единая архитектура взаимодействия рыночных признаков и торгового контекста (Окончание)
В данной статье мы завершаем перенос ключевых компонентов фреймворка OneTrans в среду MQL5 и показываем их интеграцию в единый вычислительный граф. Основное внимание уделено организации обучения моделей на исторических финансовых данных с использованием Актера и Критика, а также оценке действий через псевдо идеальные сценарии. Результаты тестирования демонстрируют практическую ценность реализованных решений для построения адаптивных стратегий и анализа рыночной динамики.