
Искусство ведения логов (Часть 2): Форматирование логов
Введение
В первой статье данного цикла, Искусство ведения логов (Часть 1): Основные понятия и первые шаги в MQL5 мы начали с создания собственной библиотеки логов для разработки советника. В указанной статье мы определили основные мотивы для создания такого важнейшего инструмента: преодолеть ограничений, присущих нативным логам MetaTrader 5, и привнести надежное, кастомизируемое и производительное решение во вселенную MQL5.
Напомним основные пункты, которые были рассмотрены: мы заложили основу для нашей библиотеки, утвердив следующие базовые требования:
- Надежная структура с применением паттерна Singleton, обеспечивающая согласованность между фрагментами кода.
- Продвинутый режим хранения для сохранения логов в базах данных, обеспечивая отслеживаемую историю для глубинного анализа и аудитов.
- Гибкость вывода, позволяющая сохранять или отображать логи с удобством, будь то в консоли, в файлах, в терминале или в базе данных.
- Классификация по уровням логирования, отличающая информационные сообщения от критических алертов и ошибок.
- Кастомизация формата вывода для удовлетворения уникальных потребностей каждого разработчика или проекта.
Благодаря этому прочному фундаменту стало ясно, что система логирования, которую мы разрабатываем, будет представлять собой гораздо больше чем простой логгер событий: это будет стратегический инструмент для понимания, мониторинга и оптимизации поведения советников в реальном времени.
Теперь, в этой второй статье мы добрались до одной из ключевых возможностей данной библиотеки: форматирование логов. В конце концов, эффективный лог – это не только о том, что мы записываем, но и о том, как информация презентуется. Представьте, что вы получаете запутанное или плохо отформатированное сообщение во время важного теста советника. Это может не только усложнить анализ без необходимости, но также привести к потере важной информации. Мы хотим, чтобы каждый лог обеспечивал необходимую степень ясности и детализации, при этом сохраняя возможность кастомизации в соответствии с нуждами разработчика.
Что такое формат лога?
Формат лога – это структура, согласно которой простым и понятным путем выстраивается информация, регистрируемая в процессе исполнения программы. Это стандарт, который определяет то, как будет отображаться каждая запись, и включает сбор ключевых данных, таких как степень серьезности события, дата и время его наступления, источник лог-записи и связанное с ним сообщение с описанием.
Такая организация лога крайне важна для обеспечения их удобочитаемости и полезности. Без форматирования логи могут выглядеть запутанно и вне контекста, что усложняет анализ. Представьте, например, что видите в неструктурированном логе такую запись:
DEBUG: Order sent successfully, server responded in 32msТеперь сравните это с логом, в котором соблюдается структурированный формат, и в нем не только отображается та же информация, но и указывается контекст:
[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms
Эта небольшая разница может сыграть большую роль при диагностировании проблем, особенно в сложных системах. Форматирование позволяет превратить необработанные данные в целостный нарратив, дающий разработчику понимание поведения системы.
Гибкость также является ключевым аспектом форматов логов. Каждый проект имеет собственные нужды, и возможность адаптировать под них формат лога позволяет создавать удобные кастомизации, такие как выделение критически важных событий, отслеживание источников или дополнение сообщений контекстными метаданными.
Базовая структура программы форматирования
Базовая структура программы форматирования основана на понятии «плейсхолдеров», то есть элементов, выступающих в качестве заменяемых областей в шаблоне. Такие плейсхолдеры определяют то, куда и как информация из лога будет вставлена в выходное сообщение.
Можете представить программу форматирования как машину, которая преобразует необработанные данные от события в логе в удобочитаемый и кастомизируемый формат. На вход может подаваться модель данных, содержащая такие значения как степень серьезности, сообщение, время и другие сведения. Программа форматирования затем вставляет эти значения в шаблон, предоставленный пользователем, и в результате получается отформатированный лог.
В качестве примера возьмем такой шаблон:
{date_time} {levelname}: {msg}После того как значения заменят соответствующие плейсхолдеры, вывод будет выглядеть примерно так:
12/04/2025 00:25:45 DEBUG: IFR indicator successfully inserted into the chart!
Сильная сторона программы форматирования заключается в ее гибкости. Вот несколько примеров плейсхолдеров, которые мы добавим в нашу библиотеку:
- {levelname}: представляет уровень логирования (например, DEBUG, ERROR или FATAL) в человекочитаемом виде;
- {msg}: сообщение, описывающее событие, записанное в лог;
- {args}: дополнительные данные, дополняющие контекст сообщения, часто используются для отображения динамической информации;
- {timestamp}: временная метка в миллисекундах, удобна для анализа точности;
- {date_time}: человекочитаемая версия временной метки, отображающая дату и время в стандартном для человека формате;
- {origin}: отображает источник события, например, модуль или класс, из которого была получен лог-запись;
- {filename} , {function} и {line}: определяют точное место в коде, откуда был сгенерирован лог, что делает процесс отладки более эффективным.
Логика программы форматирования проста, но эффективна. Она позволяет вам создавать вашу собственную рамку для каждого сообщения из лога, давая разработчику возможность показывать наиболее релевантную для каждой ситуации информацию. Данный подход особенно полезен в проектах, в которых объем данных может быть большим, и очень важен быстрый анализ.
Благодаря кастомизируемым шаблонам и исчерпывающим плейсхолдерам библиотека представляет собой многомодульный инструмент. Данная модульность гарантирует, что вы сможете создавать журналы, настроенные специально под нужды вашего приложения, повышая эффективность толкования и использования сохраненных в логе данных.
Реализация программ форматирования на языке MQL5
Теперь, когда мы разобрались, что такое форматы и плейсхолдеры, перейдем непосредственно к коду, чтобы понять, как это будет реализовано в текущей версии нашей библиотеки. Во-первых, создадим новую папку с именем "Formatter" по пути <Include/Logify>. Создайте в этой папке файл LogifyFormatter.mqh. В результате получится путь <Include/Logify/Formatter/LogifyFormatter.mqh>. Напомню, что в конце статьи я прикрепил конечные версии файлов, использованных в этой статье, просто качайте и пользуйтесь. Вот как должен выглядеть файловый менеджер:
//+------------------------------------------------------------------+ //| LogifyFormatter.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" //+------------------------------------------------------------------+ //| class : CLogifyFormatter | //| | //| [PROPERTY] | //| Name : CLogifyFormatter | //| Heritage : No heritage | //| Description : Class responsible for formatting the log into a | //| string, replacing placeholders with their respective values. | //| | //+------------------------------------------------------------------+ class CLogifyFormatter { public: CLogifyFormatter(void); ~CLogifyFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::CLogifyFormatter(void) { } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::~CLogifyFormatter(void) { } //+------------------------------------------------------------------+
Мы объявим приватные атрибуты, хранящие даты и форматы логов:
class CLogifyFormatter { private: //--- Stores the formats string m_date_format; string m_log_format; public: //--- Format query methods string GetDateFormat(void); string GetLogFormat(void); }; //+------------------------------------------------------------------+ //| Get date format | //+------------------------------------------------------------------+ string CLogifyFormatter::GetDateFormat(void) { return(m_date_format); } //+------------------------------------------------------------------+ //| Get the log format | //+------------------------------------------------------------------+ string CLogifyFormatter::GetLogFormat(void) { return(m_log_format); } //+------------------------------------------------------------------+
Здесь мы определяем:
- m_date_format: определяет то, как будут отформатированы даты (например, "yyyy/MM/dd hh:mm:ss").
- m_log_format: определяет стандарт лога (например, "{timestamp} - {msg}").
- И два других метода доступа к приватным атрибутам.
Конструктор инициализирует дату и формат лога, проверяя формат лога методом CheckLogFormat, с которым мы вскоре познакомимся. Чтобы это сделать, я добавил в конструктор два параметра, чтобы упростить определение верного формата при создании экземпляра класса.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::CLogifyFormatter(string date_formate,string log_format) { m_date_format = date_formate; if(CheckLogFormat(log_format)) { m_log_format = log_format; } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyFormatter::~CLogifyFormatter(void) { } //+------------------------------------------------------------------+
Деструктор (~CLogifyFormatter) не выполняет каких-либо определенных действий, но полезно попрактиковаться в его объявлении, поскольку он может оказаться полезным в будущих ситуациях.
Что касается метода CheckLogFormat, мы займемся его реализацией сейчас. Он играет важнейшую роль в проверке шаблонов, используемых для форматирования логов. Его цель заключается в проверке того, чтобы все плейсхолдеры в шаблоне были надлежащим образом структурированы и закрыты перед их обработкой. Этот вид проверки крайне важен, чтобы исключить непредвиденные ошибки и обеспечить надежность вывода логов. Давайте разберем несколько примеров неверных форматов и поймем причины:
- Незакрытые плейсхолдеры: например, {timestamp} - {{msg}. Здесь у нас есть лишний символ {, который не был надлежащим образом закрыт. Этот тип ошибки говорит о неполной структуре, которая приводит к сбою при обработке лога.
- Неоткрытые плейсхолдеры: в таких случаях, как {timestamp} - {msg}}, присутствует лишний символ }, который не соответствует ни одному символу { . Как и в предыдущем примере, это приводит к нарушениям в структуре лога.
- Пустые плейсхолдеры: шаблон {timestamp} - {msg} {} содержит пустой плейсхолдер {}, которые не имеет связанного с ним ключа. Каждый плейсхолдер должен быть заполнен действующей ссылкой, которая будет динамически заменяться, а пустой шаблон не соответствует этому ожиданию.
- Пустой формат: метод также учитывает случаи неверного ввода, при которых переданная строка полностью пустая. Чтобы быть действительной, строка должна содержать по крайней мере один символ, который будет служить основой для отформатированного журнала.
Когда методы выявляет одну из этих проблем, он возвращает false и выводит разработчику подробные сообщения об ошибках. Эти сообщения помогают быстро находить и устранять проблемы в переданном шаблоне. С другой стороны, если шаблон надлежащим образом структурирован и соответствует всем правилам, метод вернет true, что говорит о том, что шаблон готов к использованию. Помимо обеспечения точности, данный метод способствует применению надлежащих практик при проектировании шаблонов логов, поощряя разработчиков в создании четких и согласованных структур. Данный подход не только повышает читаемость логов, но также упрощает их обслуживание и анализ на дистанции.
//+------------------------------------------------------------------+ //| Validate format | //+------------------------------------------------------------------+ bool CLogifyFormatter::CheckLogFormat(string log_format) { //--- Variables to track the most recent '{' opening index and the number of '{' brace openings int openIndex = -1; // Index of last '{' found int openBraces = 0; // '{' counter int len = StringLen(log_format); // String length //--- Checks if string is empty if(len == 0) { //--- Prints error message and returns false Print("Erro de formatação: sequência inesperada encontrada. Verifique o padrão de placeholders usado."); return(false); } //--- Iterate through each character of the string for(int i=0;i<len;i++) { //--- Gets the current character ushort character = StringGetCharacter(log_format,i); //--- Checks if the character is an opening '{' if(character == '{') { openBraces++; // Increments the opening counter '{' openIndex = i; // Updates the index of the last opening } //--- Checks if the character is a closing '}' else if(character == '}') { //--- If there is no matching '{' if(openBraces == 0) { //--- Prints error message and returns false Print("Erro de formatação: o caractere '}' na posição ",i," não possui um '{' correspondente."); return(false); } //--- Decreases the open count because a matching '{' was found openBraces--; //--- Extracts the contents of the placeholder (between '{' and '}') string placeHolder = StringSubstr(log_format, openIndex + 1, i - openIndex - 1); //--- Checks if placeholder is empty if(StringLen(placeHolder) == 0) { //--- Prints error message and returns false Print("Erro de formatação: placeholder vazio detectado na posição ",i,". Um nome é esperado dentro de '{...}'."); return(false); } } } //--- After traversing the entire string, check if there are still any unmatched '{'}' if(openBraces > 0) { //--- Prints error message indicating the index of the last opened '{' and returns false Print("Erro de formatação: o placeholder '{' na posição ",openIndex," não possui um '}' correspondente."); return(false); } //--- Format is correct return(true); } //+------------------------------------------------------------------+
Теперь перейдем к двум главным методам класса, которые отвечают за форматирование логов:
- FormatDate() для манипулирования датами;
- FormatLog() для создания самого формата журнала.
Оба этих метода играют центральные роли в кастомизации данных, которые записываются библиотекой логов. Чтобы повысить гибкость этих методов и сделать их более расширяемыми, они объявлены как виртуальные. Поскольку методы объявляются как виртуальные, FormatDate можно легко переопределять в получаемых классах под нужды конкретного сценария. Например, в пользовательской реализации формат даты может быть адаптирован так, чтобы включать часовой пояс или иную дополнительную информацию. Такая гибкая архитектура позволяет библиотеке эволюционировать вместе с требованиями проектов, что обеспечивает возможность ее применения в разнообразных контекстах.
Метод FormatDate отвечает за преобразование объекта datetime в форматируемую строку, адаптированную в соответствии со стандартом, определенным в m_date_format. В данном паттерне используется система плейсхолдеров, которые динамически заменяются на соответствующие элементы даты, такие как год, месяц, день, время и так далее.
Данный подход невероятно гибок, позволяет широкие возможности по кастомизации форматов, которые подойдут под самые разнообразные сценарии. К примеру, вы можете выбрать отображение только дня и месяца или дополнить эту информацию, например, днем недели или временем. Ниже приведен список доступных плейсхолдеров:
- Год
- yy→ год из двух цифр (например, "25").
- yyyy → год из четырех цифр (например, "2025").
- Месяц
- M → месяц без ведущего нуля (например, "1" для января).
- MM → месяц из двух цифр (например, "01" для января).
- MMM → сокращенное название месяца (например, "Jan").
- MMMM → полное название месяца (например, "January").
- День
- d → день без ведущего нуля (например, "1").
- dd → день из двух цифр (например, "01").
- День года
- D → день года без ведущего нуля (например, "32" для 1 февраля). - DDD → день года из трех цифр (например, "032").
- День недели
- E → сокращенное название дня недели (например, "Mon").
- EEEE → полное название дня недели (например, "Monday").
- Часы в 24-часовом формате
- H → час без ведущего нуля (например, "9").
- HH → час из двух цифр (например, "09").
- Часы в 12-часовом формате
- h → час без ведущего нуля (например, "9").
- hh → час из двух цифр (например, "09").
- Минуты
- m → минута без ведущего нуля (например, "5").
- mm → минута из двух цифр (например, "05").
- Секунды
- s → секунда без ведущего нуля (например, "9").
- ss → секунда из двух цифр (например, "09").
- AM/PM
- tt → возвращает буквами в нижнем регистре (am/pm).
- TT → возвращает буквами в верхнем регистре (AM/PM).
Эта логика обеспечивает значительное удобство кастомизации отображаемой даты в соответствии с вашими нуждами. К примеру, при использовании формата "yyyy-MM-dd HH:mm:ss" получим вывод наподобие "2025-01-02 14:30:00". Если используется формат "EEEE, MMM dd, yyyy", вывод будет выглядеть как "Tuesday, Jul 30, 2019". Такая адаптивность крайне важна для получения логов, которые являются как информативными, так и визуально читаемыми.
Логика FormatLog основана на StringReplace(), нативной функции MQL5. Данная функция выполняет прямую замену строк, когда все вхождения определенной подстроки заменяются на другую. В контексте метода FormatLog плейсхолдеры, такие как {timestamp} или {message} заменяются на реальные значения из модели лога. Это преобразует экземпляры модели, такой как MqlLogifyModel, в организованные данные, готовые к визуализации.
Вот код для реализации в классе, в который я добавил несколько комментариев, чтобы объяснить код настолько подробно, насколько это возможно:
//+------------------------------------------------------------------+ //| class : CLogifyFormatter | //| | //| [PROPERTY] | //| Name : CLogifyFormatter | //| Heritage : No heritage | //| Description : Class responsible for formatting the log into a | //| string, replacing placeholders with their respective values. | //| | //+------------------------------------------------------------------+ class CLogifyFormatter { //--- Date and log formatting methods virtual string FormatDate(datetime date); virtual string FormatLog(MqlLogifyModel &data); }; //+------------------------------------------------------------------+ //| Formats dates | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatDate(datetime date) { string formated = m_date_format; //--- Date and time structure MqlDateTime time; TimeToStruct(date, time); //--- Array with months and days of the week in string string months_abbr[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; string months_full[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; string day_of_week_abbr[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; string day_of_week_full[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; //--- Replace year StringReplace(formated, "yyyy", IntegerToString(time.year)); StringReplace(formated, "yy", IntegerToString(time.year % 100, 2, '0')); //--- Replace month if(StringFind(formated,"MMM") < 0 && StringFind(formated,"MMMM") < 0) { StringReplace(formated, "MM", IntegerToString(time.mon, 2, '0')); StringReplace(formated, "M", IntegerToString(time.mon)); } //--- Replace day StringReplace(formated, "dd", IntegerToString(time.day, 2, '0')); StringReplace(formated, "d", IntegerToString(time.day)); //--- Replace day of year StringReplace(formated, "DDD", IntegerToString(time.day_of_year, 3, '0')); StringReplace(formated, "D", IntegerToString(time.day_of_year)); //--- Replace Replace hours (24h and 12h) StringReplace(formated, "HH", IntegerToString(time.hour, 2, '0')); StringReplace(formated, "H", IntegerToString(time.hour)); int hour_12 = time.hour % 12; if (hour_12 == 0) hour_12 = 12; StringReplace(formated, "hh", IntegerToString(hour_12, 2, '0')); StringReplace(formated, "h", IntegerToString(hour_12)); //--- Replace minutes and seconds StringReplace(formated, "mm", IntegerToString(time.min, 2, '0')); StringReplace(formated, "m", IntegerToString(time.min)); StringReplace(formated, "ss", IntegerToString(time.sec, 2, '0')); StringReplace(formated, "s", IntegerToString(time.sec)); //--- Replace AM/PM bool is_am = (time.hour < 12); StringReplace(formated, "tt", is_am? "am" : "pm"); StringReplace(formated, "TT", is_am? "AM" : "PM"); //--- Replace month StringReplace(formated, "MMMM", months_full[time.mon - 1]); StringReplace(formated, "MMM", months_abbr[time.mon - 1]); //--- Replace day of week StringReplace(formated, "EEEE", day_of_week_full[time.day_of_week]); StringReplace(formated, "E", day_of_week_abbr[time.day_of_week]); return(formated); } //+------------------------------------------------------------------+ //| Format logs | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatLog(MqlLogifyModel &data) { string formated = m_log_format; //--- Replace placeholders StringReplace(formated,"{timestamp}",IntegerToString(data.timestamp)); StringReplace(formated,"{level}",IntegerToString(data.level)); StringReplace(formated,"{origin}",data.origin); StringReplace(formated,"{message}",data.message); StringReplace(formated,"{metadata}",data.metadata); return(formated); } //+------------------------------------------------------------------+
На этом построение класса завершено, теперь приступим к обновлению модели MqlLogifyModel.
Добавление данных в MqlLogifyModel
Модель MqlLogifyModel является одним из ключевых элементов нашей библиотеки логирования, представляющим собой базовую структуру для хранения данных о каждом событии в логе и манипулирования ими. В своем текущем виде структура определена следующим образом:struct MqlLogifyModel { ulong timestamp; // Date and time of the event ENUM_LOG_LEVEL level; // Severity level string origin; // Log source string message; // Log message string metadata; // Additional information in JSON or text MqlLogifyModel::MqlLogifyModel(void) { timestamp = 0; level = LOG_LEVEL_DEBUG; origin = ""; message = ""; metadata = ""; } MqlLogifyModel::MqlLogifyModel(ulong _timestamp,ENUM_LOG_LEVEL _level,string _origin,string _message,string _metadata) { timestamp = _timestamp; level = _level; origin = _origin; message = _message; metadata = _metadata; } };
Эта начальная версия хорошо работает в простых сценариях, но мы можем значительно усовершенствовать ее, добавив больше информации и уточнив имена свойств, чтобы упростить использование модели и приведение ее в соответствие с наилучшей практикой. Ниже мы обсудим планируемые улучшения.
Упрощение имен свойств- Поле сообщения будет переименовано в msg. Это изменение, хоть оно и небольшое, уменьшает количество символов при обращении к свойству, благодаря чему писать и читать код становится проще.
- Метаданные будут переименованы в args, так как новое имя более точно отражает функционал свойства: хранение контекстуальных данных, относящихся к лог-записи на момент ее создания.
Добавлены новые поля
Чтобы обогатить логи и позволить проводить более детальный анализ, будут добавлены следующие поля:
- formatted: приведение сообщения в логе в соответствие с указанным форматом. Поле будет использоваться для хранения финальной версии лога с уже замененными на реальные значения плейсхолдерами. Данное свойство доступно только для чтения.
- levelname: текстовое обозначение уровня логирования (например, "DEBUG" или "INFO"). Это полезно, когда в формате требуется описание степени серьезности.
- date_time: представляет дату и время события в формате datetime, альтернатива временной метке.
- filename: имя файла, в котором была создана лог-запись, важно для точного отслеживания источника.
- function: название функции, которая вызвала лог, дающее больше контекстной информации об источнике события.
- line: номер строки в исходном файле, в котором была сгенерирована лог-запись. Это свойство может быть особенно полезно в отладочных сценариях.
Эти новые поля делают модель MqlLogifyModel более надежной и готовой соответствовать различным требованиям к логированию, таким как подробная отладка или интеграция с внешними инструментами мониторинга. После изменений код выглядит так:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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 MqlLogifyModel::MqlLogifyModel(void) { formated = ""; levelname = ""; msg = ""; args = ""; timestamp = 0; date_time = 0; level = LOG_LEVEL_DEBUG; origin = ""; filename = ""; function = ""; line = 0; } 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) { formated = _formated; levelname = _levelname; msg = _msg; args = _args; timestamp = _timestamp; date_time = _date_time; level = _level; origin = _origin; filename = _filename; function = _function; line = _line; } }; //+------------------------------------------------------------------+В качестве следующего шага мы обновим метод FormatLog класса CLogifyFormatter, добавив в него поддержку плейсхолдеров, соответствующих новым свойствам. Ниже представлена обновленная версия метода, который теперь совместим со всеми новыми свойствами модели:
//+------------------------------------------------------------------+ //| 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)); return(formated); } //+------------------------------------------------------------------+
В методе FormatLog значение {date_time} заменяется на отформатированный вывод FormatDate, т.е. данный плейсхолдер заменяется на дату в ранее переданном формате.
Применение программ форматирования к логам
Давайте убедимся, что наш класс CLogify может использовать программу для форматирования лог-записей. Давайте импортируем класс, ответственный за это, и добавим атрибут для его хранения:
#include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh"
Следующим шагом будет добавление атрибута m_formatter в класс CLogify для хранения экземпляра программы форматирования и создания методов для его настройки и обращения к нему. Это позволит нам повторно использовать программу форматирования в разных точках системы:
//+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifyFormatter *m_formatter; public: //--- Get/Set object formatter void SetFormatter(CLogifyFormatter *format); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogify::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogify::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
Существующие методы логирования имеют ограниченный набор параметров, таких как временная метка, уровень логирования, сообщение, источник и метаданные. Мы усовершенствуем также и этот подход, добавив новые параметры, которые описывают контекст лога, а именно:
- filename: имя файла, в котором был вызван лог.
- function: имя функции, в котором был вызван лог.
- line: строчка кода, которая сгенерировала лог.
Кроме того, чтобы сделать сигнатуру метода более интуитивно понятной, мы изменим имена параметров. Вот пример изменения основного метода (Append):
bool CLogify::Append(ulong timestamp, ENUM_LOG_LEVEL level, string message, string origin = "", string metadata = "");
Измененный метод
bool CLogify::Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0);
Теперь наша новая реализация выглядит так:
//+------------------------------------------------------------------+ //| 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); //--- Printing the formatted log Print(m_formatter.FormatLog(data)); return(true); } //+------------------------------------------------------------------+
Заметьте, что я вывожу возвращаемое функцией FormatLog значение, которое представляет собой объект данных.
Теперь, когда наш основной метод Append работает с подробными параметрами, мы можем скорректировать или добавить другие специальные методы для каждого уровня логирования (Debug, Infor, Alert, Error и Fatal). Эти методы являются быстрым способом автоматической установки уровня логирования и при этом позволяют использовать все остальные параметры.
//+------------------------------------------------------------------+ //| 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)); } //+------------------------------------------------------------------+
Практический пример
Я продемонстрирую, как использовать класс CLogify с настроенным пользовательским форматом для логов. Мы начнем с базовых примеров и постепенно будем их усложнять, чтобы продемонстрировать гибкость решения.
1. Базовая конфигурация лога
Для этого примера мы воспользуемся тестовым файлом LogifyTest.mq5, созданным в первой статье. Первичная настройка предполагает создание экземпляра CLogify и определение базового формата для сообщений лога. Код представлен ниже:
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} => {msg}")); //--- Log a simple message logify.Debug("Application initialized successfully."); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Когда код исполняется, формат генерируемых логов будет примерно таким:
[DEBUG] 07:25:32 => Application initialized successfully.
Отметим, что используется упрощенный формат времени (hh:mm:ss), а структура сообщения отражает уровень логирования и текущее время. Вот наиболее базовый пример использования CLogify.
2. Добавление деталей и определение источника
Теперь расширим наш пример, добавив такую информацию, как источник лог-записи и дополнительные параметры. Это полезно для более сложных систем, в логах которых должно указываться, какая часть системы генерирует сообщение.
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} {args}")); //--- Log a simple message logify.Debug("Connection established with the server.", "Network"); logify.Alert("Configuration file not found!", "Config", "Attempt 1 of 3"); return(INIT_SUCCEEDED); }Данный код даст следующий вывод:
[DEBUG] 07:26:18 (Network) => Connection established with the server. [ALERT] 07:26:19 (Config) => Configuration file not found! Attempt 1 of 3
Здесь мы добавим параметры источника и контекстуальные аргументы, чтобы сделать сообщение лога более информативным.
3. Использование продвинутых метаданных
В более надежных системах зачастую необходимо определять файл, функцию и строку, на которых была сгенерирована лог-запись. Давайте изменим пример, дополнив лог этой информацией:
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} (File: {filename}, Line: {line})")); //--- Log a simple message logify.Error("Error accessing database.", "Database", "", __FILE__, __FUNCTION__, __LINE__); return(INIT_SUCCEEDED); }При исполнении кода выше мы получим:
[ERROR] 07:27:15 (Database) => Error accessing database. (File: LogifyTest.mq5, Line: 25)
Теперь у нас есть подробная информация, которая позволит отследить, где именно в коде возникла ошибка.
4. Кастомизация форматов и интеграция в большие системы
В качестве финального примера мы продемонстрируем, как кастомизировать форматы и генерировать различные типы лог-сообщений в цикле, который симулирует процесс исполнения большей системы:
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("yyyy.MM.dd hh:mm:ss","{date_time} [{levelname}] {msg} - {origin} ({filename}:{line})")); //--- Cycle simulating various system operations for(int i=0; i<3; i++) { logify.Debug("Operation in progress...", "TaskProcessor", "", __FILE__, __FUNCTION__, __LINE__); if(i == 1) { logify.Alert("Possible inconsistency detected.", "TaskValidator", "", __FILE__, __FUNCTION__, __LINE__); } if(i == 2) { logify.Fatal("Critical error, purchase order not executed correctly!", "Core", "", __FILE__, __FUNCTION__, __LINE__); } } return(INIT_SUCCEEDED); }Исполнение даст следующий вывод:
2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [ALERT] Possible inconsistency detected. - TaskValidator (LogifyTest.mq5:28) 2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [FATAL] Critical error, purchase order not executed correctly! - Core (LogifyTest.mq5:32)
В этом примере симулируется реальный поток операций, при этом сообщения эскалируются в соответствии со степенью серьезности ситуации. Формат лога высоко детализирован и отображает полное время, уровень логирования, сообщение и место в коде.
Заключение
В данной статье мы подробно изучили индивидуальную настройку и применение различных форматов логов на языке MQL5 с помощью библиотеки Logify. Мы начали с введения, в котором дали определение формата лога и описали важность данной практики для сценариев мониторинга и отладки.
Затем мы рассмотрели базовую структуру программы форматирования, которая является ключевым инструментом кастомизации логов, и то, как ее реализация на языке MQL5 делает логи более осмысленными и доступными. На основе вышеизложенного мы показали процесс реализации таких программ форматирования, уделив внимание возможностям кастомизации и включения дополнительных данных в модель MqlLogifyModel.
По мере того, как мы двигались дальше, мы рассмотрели процесс добавления дополнительных контекстуальных данных в лог, чтобы сделать его более информативным, например, за счет упоминания точного источника сообщения (файл, функция и строка кода). Мы также обсудили, как настраивать такие программы форматирования и применять их к журналам, обеспечивая, чтобы выводимый лог соответствовал специфическим нуждам любого проекта.
Наконец, мы предложили практический пример, в котором продемонстрировано, как реализовывать различные уровни сложности в логах, от базовых конфигураций до надежных систем с продвинутыми и детализированными логами. В данном примере мы объединили все изученное, соединив теорию с реальной практикой в системах, разработанных на языке MQL5.
Ниже представлена диаграмма, отражающая текущую версию библиотеки:
Весь код, используемый в данной статье, прикреплен ниже. Ниже представлена таблица с описанием каждого из файлов библиотеки:
Имя файла | Описание |
---|---|
Experts/Logify/LogiftTest.mq5 | Файл, в котором мы тестируем возможности библиотеки, содержит практический пример. |
Include/Logify/Formatter/LogifyFormatter.mqh | Класс, отвечающий за форматирование лог-записей, заменяющий плейсхолдеры на конкретные значения. |
Include/Logify/Logify.mqh | Основной класс для управления логами, соединяющий вместе уровни, модели и форматирование. |
Include/Logify/LogifyLevel.mqh | Файл, который определяет уровни логирования библиотеки Logify, обеспечивая более детальный контроль. |
Include/Logify/LogifyModel.mqh | Структура, представляющая собой модель лог-записей, включающая такие подробности, как уровень, сообщение, временная метка и контекст. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16833
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования