Искусство ведения логов (Часть 4): Сохранение логов в файлах
Введение
В первой статье данного цикла, Искусство ведения логов (Часть 1): Основные понятия и первые шаги в MQL5, мы начали с создания собственной библиотеки логов для разработки советника. В указанной статье мы определили основные мотивы для создания такого важнейшего инструмента: преодолеть ограничения, присущие нативным логам MetaTrader 5, и привнести надежное, кастомизируемое и производительное решение во вселенную MQL5.
Напомним основные пункты, которые были рассмотрены: мы заложили основу для нашей библиотеки, утвердив следующие базовые требования:
- Надежная структура с применением паттерна Singleton, обеспечивающая согласованность между фрагментами кода.
- Продвинутый режим хранения для сохранения логов в базах данных, обеспечивая отслеживаемую историю для глубинного анализа и аудитов.
- Гибкость вывода, позволяющая сохранять или отображать логи с удобством, будь то в консоли, в файлах, в терминале или в базе данных.
- Классификация по уровням логирования, отличающая информационные сообщения от критических алертов и ошибок.
- Кастомизация формата вывода для удовлетворения уникальных потребностей каждого разработчика или проекта.
Благодаря этому прочному фундаменту стало ясно, что система логирования, которую мы разрабатываем, будет представлять собой гораздо больше чем простой логгер событий: это будет стратегический инструмент для понимания, мониторинга и оптимизации поведения советников в реальном времени.
До сих пор мы изучали основы логов, научились их форматировать и поняли, как обработчики контролируют назначение сообщений. Но где хранить эти логи для дальнейшего использования? В этой четвертой статье мы подробнее рассмотрим процесс сохранения логов в файлах. Приступим!
Зачем сохранять логи в файлах?
Сохранение логов в файлах — обязательная практика для любой системы, которая ценит надежность и эффективное обслуживание. Представьте себе следующий сценарий: ваш советник работает уже несколько дней, и вдруг происходит неожиданная ошибка. Как понять, что произошло? Без постоянных записей это будет похоже на попытку решить головоломку, не имея всех деталей.
Лог-файлы — это не просто способ хранения сообщений. Они представляют собой память системы. Вот основные причины для их использования:
-
Устойчивость и история
Логи, сохраненные в файлах, остаются доступными даже после завершения работы программы. Это позволяет выполнять исторические запросы для проверки производительности, понимания прошлого поведения и выявления закономерностей во времени.
-
Аудит и прозрачность
В критически важных проектах, таких как финансовый рынок, ведение подробной истории имеет важное значение для аудита или обоснования автоматизированных решений. Хорошо сохраненный лог может стать вашей лучшей защитой в случае возникновения вопросов.
-
Диагностика и отладка
С помощью лог-файлов вы можете отслеживать конкретные ошибки, контролировать критические события и анализировать каждый шаг выполнения системы.
-
Гибкость доступа
В отличие от логов, отображаемых на консоли или терминале, файлы можно просматривать удаленно или делиться ими с командами, создавая общий и подробный обзор важных событий.
-
Автоматизация и интеграция
Файлы могут анализироваться автоматизированными инструментами, которые отправляют оповещения о критических проблемах или создают подробные отчеты о производительности.
Сохраняя логи в файлах, вы превращаете простой ресурс в инструмент для управления, отслеживания и улучшения. Мне не нужно здесь подробно объяснять важность сохранения этих данных в файле; давайте перейдем к следующей теме и разберемся, как эффективно реализовать этот ресурс в нашей библиотеке.
Прежде чем перейти непосредственно к коду, важно определить функции, которые должен обеспечивать обработчик файлов. Ниже я подробно описал каждое из необходимых требований:
-
Настройка каталога, имени и типа файла
Пользователи могут настраивать:
- Каталог, в котором будут храниться логи.
- Имя лог-файла, обеспечивающее больший контроль и упорядоченность.
- Формат выходного файла с поддержкой .log, .txt и .json.
-
Настройка кодировки
Поддержка различных типов кодирования для лог-файлов, таких как:
- UTF-8 (рекомендуемый стандарт).
- UTF-7, ANSI Code Page (ACP) или другие, по мере необходимости.
-
Отчеты об ошибках библиотеки
Библиотека должна включать систему для выявления и сообщения об ошибках в своем собственном выполнении:
- Сообщения об ошибках, отображаемые непосредственно в консоли терминала.
- Четкая информация для облегчения диагностики и решения проблем.
Работа с файлами в MQL5
В MQL5 для работы с файлами необходимо базовое понимание того, как язык обрабатывает эти операции. Если вы хотите действительно углубиться в сложные операции чтения, записи и использования флагов, я не могу не порекомендовать статью "Основы программирования на MQL5: Файлы" Дмитрия Федосеева. В ней дается полный и подробный обзор темы, который превращает сложность в нечто понятное, не теряя при этом глубины.
Но здесь нам нужно что-то более прямое и объективное. Мы не будем углубляться в мельчайшие детали, потому что моя задача — научить вас основам: простому и практичному открытию, манипулированию и закрытию файлов.
-
Понимание файловых каталогов в MQL5. В MQL5 все файлы, обрабатываемые стандартными функциями, автоматически сохраняются в папке MQL5/Files, которая находится в каталоге установки терминала. Это означает, что при работе с файлами в MQL5 вам нужно указывать только относительный путь от этой базовой папки, без необходимости указывать полный путь. Например, при сохранении в logs/expert.log полный путь будет следующим:
<terminal folder>/MQL5/Files/logs/expert.log
-
Создание и открытие файлов. Функция для открытия или создания файлов — FileOpen. Она требует в качестве обязательного аргумента путь к файлу (после MQL5/Files) и некоторые флаги, которые определяют, как будет обрабатываться файл. Мы будем использовать следующие флаги:
- FILE_READ позволяет открыть файл для чтения.
- FILE_WRITE позволяет открыть файл для записи.
- FILE_ANSI указывает, что содержимое будет записано с использованием строк в формате ANSI (каждый символ занимает один байт).
Полезная особенность MQL5 заключается в том, что при сочетании FILE_READ и FILE_WRITE файл автоматически создается, если он не существует. Это избавляет от необходимости вручную проверять наличие файла.
-
Закрытие файла. Наконец, когда вы закончите работу с файлом, используйте функцию FileClose(), чтобы закрыть обработку файла.
Вот практический пример того, как открыть (или создать) и закрыть файл в MQL5:/p>
int OnInit() { //--- Open the file and store the handler int handle_file = FileOpen("logs\\expert.log", FILE_READ|FILE_WRITE|FILE_ANSI, '\t', CP_UTF8); //--- If opening fails, display an error in the terminal log if(handle_file == INVALID_HANDLE) { Print("[ERROR] Unable to open log file. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")"); return(INIT_FAILED); } //--- Close file FileClose(handle_file); return(INIT_SUCCEEDED); }
Теперь, когда мы открыли файл, пришло время узнать, как в него записывать.
- Позиционирование указателя записи. Перед записью необходимо определить, где будут вставлены данные. Мы используем функцию FileSeek(), чтобы позиционировать указатель записи в конце файла. Это позволяет избежать перезаписи существующего содержимого.
- Запись данных. Метод FileWrite() записывает строки в файл. Нет необходимости использовать "\n"» для разрыва строки. При использовании этого метода при следующей записи данных они будут автоматически записаны в другую строку, что обеспечит лучшую организацию.
Практический пример:
int OnInit() { //--- Open the file and store the handler int handle_file = FileOpen("logs\\expert.log", FILE_READ|FILE_WRITE|FILE_ANSI, '\t', CP_UTF8); //--- If opening fails, display an error in the terminal log if(handle_file == INVALID_HANDLE) { Print("[ERROR] Unable to open log file. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")"); return(INIT_FAILED); } //--- Move the writing pointer FileSeek(handle_file, 0, SEEK_END); //--- Writes the content inside the file FileWrite(handle_file, "[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms"); //--- Close file FileClose(handle_file); return(INIT_SUCCEEDED); }
После запуска кода вы увидите файл, созданный в папке Files. Полный путь будет выглядеть примерно так:
<Terminal folder>/MQL5/Files/logs/expert.log Если открыть файл, вы увидите именно то, что мы написали:
[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms
Теперь, когда мы научились очень просто работать с файлами в MQL5, давайте добавим эту работу в класс обработчика, отвечающий за сохранение файлов, CLogifyHandlerFile.
Создание конфигураций класса CLogifyHandlerFile
Теперь давайте подробно рассмотрим, как мы можем настроить этот класс для эффективной обработки ротации файлов, о которой я упоминал в разделе требований. Но что именно означает "ротация файлов"? Давайте разберемся подробнее. Ротация — обязательная практика, которая позволяет избежать хаотичной ситуации, когда один файл лога растет бесконечно, и предотвращающая чрезмерное накопление данных в одном лог-файле, что может осложнить анализ, превратив логи в трудночитаемый и медленно обрабатываемый массив данных.
Представьте себе следующий сценарий: советник работает в течение нескольких недель или месяцев, записывая каждое событие, ошибку или уведомление в один и тот же файл. Вскоре этот лог достигает значительных размеров, что значительно усложняет чтение и интерпретацию информации. Именно здесь и приходит на помощь ротация. Она позволяет нам разделить эту информацию на более мелкие и организованные части, что значительно упрощает чтение и анализ.
Два наиболее распространенных способа сделать это:
- По размеру. Вы устанавливаете ограничение по размеру, обычно в мегабайтах (МБ), для лог-файла. Когда это ограничение достигается, автоматически создается новый файл, и цикл начинается заново. Этот подход очень практичен, когда основное внимание уделяется контролю роста лога, без необходимости придерживаться календаря. Как только текущий файл достигает ограничения по размеру (в мегабайтах), происходит следующий поток: Текущий лог-файл переименовывается, получая индекс, например "log1.log". Существующие файлы в каталоге также перенумеровываются, например "log1.log" становится "log2.log". Если количество файлов достигает максимально допустимого, самые старые файлы удаляются. Этот подход полезен для ограничения как места, занимаемого логами, так и количества сохраненных файлов.
- По дате. в этом случае каждый день создается новый лог-файл. Каждый из них имеет в своем имени дату создания, например log_2025-01-19.log, что решает большую часть проблем с организацией логов. Этот подход идеально подходит для тех случаев, когда вам нужно посмотреть конкретный день, не теряясь в одном гигантском файле. Это метод, который я чаще всего применяю для логов моих советников: он делает данные более понятными, структурированными и удобными для анализа.
Кроме того, вы также можете ограничить количество хранимых файлов логов. Этот контроль очень важен для предотвращения ненужного накопления старых логов. Представьте, что вы настроили сохранение 30 самых последних файлов, и когда появляется 31-й, система автоматически удаляет самый старый, что предотвращает накопление очень старых логов на диске, а самые последние сохраняются.
Еще одна важная деталь — использование кэша. Вместо того чтобы записывать каждое сообщение непосредственно в файл сразу после его поступления, сообщения временно хранятся в кэше. Когда кэш достигает установленного предела, он сразу же сбрасывает все содержимое файла. Это приводит к уменьшению количества операций чтения и записи на диск, улучшению производительности и увеличению срока службы ваших устройств хранения данных.
Теперь, когда мы понимаем концепцию ротации файлов, давайте создадим структуру MqlLogifyHandleFileConfig для хранения всех конфигураций класса CLogifyHandlerFile. Эта структура будет отвечать за хранение параметров, определяющих управление логами.
Первая часть структуры будет включать определение перечислений для типов ротации и расширений файлов, которые будут использоваться:
//+------------------------------------------------------------------+ //| ENUMS for log rotation and file extension | //+------------------------------------------------------------------+ enum ENUM_LOG_ROTATION_MODE { LOG_ROTATION_MODE_NONE = 0, // No rotation LOG_ROTATION_MODE_DATE, // Rotate based on date LOG_ROTATION_MODE_SIZE, // Rotate based on file size }; enum ENUM_LOG_FILE_EXTENSION { LOG_FILE_EXTENSION_TXT = 0, // .txt file LOG_FILE_EXTENSION_LOG, // .log file LOG_FILE_EXTENSION_JSON, // .json file };
Сама структура MqlLogifyHandleFileConfig будет содержать следующие параметры:
- directory – каталог, в котором будут храниться лог-файлы;
- base_filename – базовое имя файла без расширения;
- file_extension – тип расширения лог-файла (например, .txt, .log или .json);
- rotation_mode – режим ротации файлов;
- messages_per_flush – количество сообщений, которые необходимо кэшировать перед записью в файл;
- codepage – кодировка, используемая для лог-файлов (например, UTF-8 или ANSI);
- max_file_size_mb – максимальный размер каждого лог-файла, если ротация основана на размере;
- max_file_count – максимальное количество лог-файлов, которые необходимо сохранить перед удалением самых старых.
В дополнение к конструкторам и деструкторам я добавлю в структуру вспомогательные методы для настройки каждого из режимов ротации, призванные сделать процесс настройки более практичным и, прежде всего, надежным. Эти методы нужны не только для элегантности, они гарантируют, что при настройке не будет упущено ни одной важной детали.
Например, если режим ротации установлен по дате (LOG_ROTATION_MODE_DATE), попытка настроить атрибут max_file_size_mb не имеет никакого смысла, ведь этот параметр актуален только в режиме по размеру (LOG_ROTATION_MODE_SIZE). Роль этих методов заключается в том, чтобы избежать подобных несоответствий, защищая систему от нерабочих конфигураций.
Если по какой-то причине важный параметр не указан, система принимает меры. Она может автоматически ввести значение по умолчанию, выдав предупреждение разработчику, тем самым обеспечивая надежность процесса и исключая неприятные сюрпризы.
Вспомогательные методы, которые мы будем реализовывать:
- CreateNoRotationConfig() – конфигурация для ротации без файлов (все записи сохраняются в один и тот же файл без ротации);
- CreateDateRotationConfig() – конфигурация для ротации на основе дат;
- CreateSizeRotationConfig() – конфигурация для ротации на основе размера файла.
- ValidateConfig() – метод, который проверяет, все ли конфигурации верны и готовы к использованию (этот метод будет использоваться автоматически классом, а не разработчиком, который будет использовать библиотеку).
Вот полная реализация структуры:
//+------------------------------------------------------------------+ //| Struct: MqlLogifyHandleFileConfig | //+------------------------------------------------------------------+ struct MqlLogifyHandleFileConfig { string directory; // Directory for log files string base_filename; // Base file name ENUM_LOG_FILE_EXTENSION file_extension; // File extension type ENUM_LOG_ROTATION_MODE rotation_mode; // Rotation mode int messages_per_flush; // Messages before flushing uint codepage; // Encoding (e.g., UTF-8, ANSI) ulong max_file_size_mb; // Max file size in MB for rotation int max_file_count; // Max number of files before deletion //--- Default constructor MqlLogifyHandleFileConfig(void) { directory = "logs"; // Default directory base_filename = "expert"; // Default base name file_extension = LOG_FILE_EXTENSION_LOG;// Default to .log extension rotation_mode = LOG_ROTATION_MODE_SIZE;// Default size-based rotation messages_per_flush = 100; // Default flush threshold codepage = CP_UTF8; // Default UTF-8 encoding max_file_size_mb = 5; // Default max file size in MB max_file_count = 10; // Default max file count } //--- Destructor ~MqlLogifyHandleFileConfig(void) { } //--- Create configuration for no rotation void CreateNoRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, int msg_per_flush=100, uint cp=CP_UTF8) { directory = dir; base_filename = base_name; file_extension = extension; rotation_mode = LOG_ROTATION_MODE_NONE; messages_per_flush = msg_per_flush; codepage = cp; } //--- Create configuration for date-based rotation void CreateDateRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, int max_files=10, int msg_per_flush=100, uint cp=CP_UTF8) { directory = dir; base_filename = base_name; file_extension = extension; rotation_mode = LOG_ROTATION_MODE_DATE; messages_per_flush = msg_per_flush; codepage = cp; max_file_count = max_files; } //--- Create configuration for size-based rotation void CreateSizeRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, ulong max_size=5, int max_files=10, int msg_per_flush=100, uint cp=CP_UTF8) { directory = dir; base_filename = base_name; file_extension = extension; rotation_mode = LOG_ROTATION_MODE_SIZE; messages_per_flush = msg_per_flush; codepage = cp; max_file_size_mb = max_size; max_file_count = max_files; } //--- Validate configuration bool ValidateConfig(string &error_message) { //--- Saves the return value bool is_valid = true; //--- Check if the directory is not empty if(directory == "") { directory = "logs"; error_message = "The directory cannot be empty."; is_valid = false; } //--- Check if the base filename is not empty if(base_filename == "") { base_filename = "expert"; error_message = "The base filename cannot be empty."; is_valid = false; } //--- Check if the number of messages per flush is positive if(messages_per_flush <= 0) { messages_per_flush = 100; error_message = "The number of messages per flush must be greater than zero."; is_valid = false; } //--- Check if the codepage is valid (verify against expected values) if(codepage != CP_ACP && codepage != CP_MACCP && codepage != CP_OEMCP && codepage != CP_SYMBOL && codepage != CP_THREAD_ACP && codepage != CP_UTF7 && codepage != CP_UTF8) { codepage = CP_UTF8; error_message = "The specified codepage is invalid."; is_valid = false; } //--- Validate limits for size-based rotation if(rotation_mode == LOG_ROTATION_MODE_SIZE) { if(max_file_size_mb <= 0) { max_file_size_mb = 5; error_message = "The maximum file size (in MB) must be greater than zero."; is_valid = false; } if(max_file_count <= 0) { max_file_count = 10; error_message = "The maximum number of files must be greater than zero."; is_valid = false; } } //--- Validate limits for date-based rotation if(rotation_mode == LOG_ROTATION_MODE_DATE) { if(max_file_count <= 0) { max_file_count = 10; error_message = "The maximum number of files for date-based rotation must be greater than zero."; is_valid = false; } } //--- No errors found error_message = ""; return(is_valid); } }; //+------------------------------------------------------------------+
Отдельно стоит отметить, как работает функция ValidateConfig(). При анализе этой функции обратите внимание на одну интересную особенность: когда она обнаруживает ошибку в каком-либо значении конфигурации, она не сразу возвращает false, сообщая, что что-то пошло не так. Сначала она принимает меры по исправлению ситуации, чтобы автоматически решить проблему, и только после этого возвращает окончательный результат.
Сначала она сбрасывает неверное значение, возвращая его к значению по умолчанию. Таким образом, конфигурация, в некотором смысле, временно "исправляется", не позволяя ошибке помешать процессу программы следовать своему потоку. Затем, чтобы не оставлять ситуацию без объяснения, функция формирует подробное сообщение, четко указывая, где произошла ошибка, и что требуется скорректировать. И, наконец, функция помечает переменную is_valid как false, сигнализируя, что что-то пошло не так. Только в конце, после принятия всех этих мер, она возвращает эту переменную с окончательным статусом, который покажет, прошла ли конфигурация и является ли она рабочей.
Но что делает это еще более интересным, так это то, как функция обрабатывает несколько ошибок. Если одновременно присутствует более одного неверного значения, она не сосредотачивается на исправлении первой появившейся ошибки, оставляя остальные на потом. Напротив, она обрабатывает их все сразу, исправляя все одновременно. В конце функция возвращает сообщение, объясняющее, какая последняя ошибка была исправлена, гарантируя, что ничего не упущено.
Такой подход очень ценен и помогает в работе разработчика. Во время разработки системы часто бывает, что некоторые значения определяются неправильно или по ошибке. Прелесть здесь в том, что функция имеет дополнительный уровень безопасности, исправляя ошибки автоматически, не дожидаясь, пока программист заметит их одну за другой. Ведь небольшие ошибки, если их не исправить, могут привести к серьезным сбоям, например, к невозможности сохранить лог-записи. Эта автоматизация обработки ошибок, которую я создал, в конечном итоге предотвращает небольшие сбои, прерывающие работу системы, и помогает поддерживать ее бесперебойную работу.
Реализация класса CLogifyHandlerFile
У нас есть класс, который был создан в прошлой статье, мы просто внесем изменения, чтобы он стал функциональным. Здесь я подробно опишу каждую доработку, чтобы вы поняли, как все работает.
В частной области класса мы добавляем переменные и некоторые важные вспомогательные методы:
- Конфигурация. Мы создаем переменную m_config типа MqlLogifyHandleFileConfig для хранения настроек, связанных с системой логирования.
- Я также реализовал методы SetConfig() и GetConfig() для определения и доступа к настройкам класса.
Вот начальная структура класса с основными определениями и методами:
//+------------------------------------------------------------------+ //| class : CLogifyHandlerFile | //| | //| [PROPERTY] | //| Name : CLogifyHandlerFile | //| Heritage : CLogifyHandler | //| Description : Log handler, inserts data into file, supports | //| rotation modes. | //| | //+------------------------------------------------------------------+ class CLogifyHandlerFile : public CLogifyHandler { private: //--- Config MqlLogifyHandleFileConfig m_config; public: //--- Configuration management void SetConfig(MqlLogifyHandleFileConfig &config); MqlLogifyHandleFileConfig GetConfig(void); }; //+------------------------------------------------------------------+ //| Set configuration | //+------------------------------------------------------------------+ void CLogifyHandlerFile::SetConfig(MqlLogifyHandleFileConfig &config) { m_config = config; //--- Validade string err_msg = ""; if(!m_config.ValidateConfig(err_msg)) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg); } } //+------------------------------------------------------------------+ //| Get configuration | //+------------------------------------------------------------------+ MqlLogifyHandleFileConfig CLogifyHandlerFile::GetConfig(void) { return(m_config); } //+------------------------------------------------------------------+
Я перечислю вспомогательные методы и более подробно объясню, как они работают. Я реализовал три полезных метода, которые будут использоваться в управлении файлами:
-
LogFileExtensionToStr() преобразует значение перечисления ENUM_LOG_FILE_EXTENSION в строку, представляющую расширение файла. Перечисление определяет возможные значения для типа файла, такие как .log, .txt и .json.
//+------------------------------------------------------------------+ //| Convert log file extension enum to string | //+------------------------------------------------------------------+ string CLogifyHandlerFile::LogFileExtensionToStr(ENUM_LOG_FILE_EXTENSION file_extension) { switch(file_extension) { case LOG_FILE_EXTENSION_LOG: return(".log"); case LOG_FILE_EXTENSION_TXT: return(".txt"); case LOG_FILE_EXTENSION_JSON: return(".json"); } //--- Default return return(".txt"); } //+------------------------------------------------------------------+
-
LogPath() отвечает за генерацию полного пути к лог-файлу на основе текущих настроек класса. Сначала она преобразует настроенное расширение файла с помощью функции LogFileExtensionToStr(). Затем она проверяет настроенный тип ротации. Если ротация основана на размере файла или ротация отсутствует, она возвращает только имя файла в настроенном каталоге. Если ротация основана на дате, она включает текущую дату (в формате ГГГГ-ММ-ДД) в качестве префикса в имени файла.
//+------------------------------------------------------------------+ //| Generate log file path based on config | //+------------------------------------------------------------------+ string CLogifyHandlerFile::LogPath(void) { string file_extension = this.LogFileExtensionToStr(m_config.file_extension); string base_name = m_config.base_filename + file_extension; if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE || m_config.rotation_mode == LOG_ROTATION_MODE_NONE) { return(m_config.directory + "\\\\" + base_name); } else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { MqlDateTime date; TimeCurrent(date); string date_str = IntegerToString(date.year) + "-" + IntegerToString(date.mon, 2, '0') + "-" + IntegerToString(date.day, 2, '0'); base_name = date_str + (m_config.base_filename != "" ? "-" + m_config.base_filename : "") + file_extension; return(m_config.directory + "\\\\" + base_name); } //--- Default return return(base_name); } //+------------------------------------------------------------------+
Метод Emit() отвечает за запись логов в файл. В текущем коде он просто отображает логи в консоли терминала. Давайте улучшим его, чтобы он открывал лог-файл, добавлял новую строку с отформатированными данными и закрывал файл после записи. Если файл не может быть открыт, в консоли будет отображаться сообщение об ошибке.
void CLogifyHandlerFile::Emit(MqlLogifyModel &data) { //--- Checks if the configured level allows if(data.level >= this.GetLevel()) { //--- Get the full path of the file string log_path = this.LogPath(); //--- Open file ResetLastError(); int handle_file = FileOpen(log_path, FILE_READ|FILE_WRITE|FILE_ANSI, '\t', m_config.codepage); if(handle_file == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")"); return; } //--- Write FileSeek(handle_file, 0, SEEK_END); FileWrite(handle_file, data.formated); //--- Close file FileClose(handle_file); } }
Итак, у нас есть базовая версия класса, которая записывает логи в файл. Давайте выполним несколько простых тестов, чтобы проверить, правильно ли работают основные функции.
Первый тест с файлами
Мы будем использовать тестовый файл, который уже использовали в предыдущих примерах, LogifyTest.mqh. Цель состоит в том, чтобы настроить систему логирования для сохранения записей в файлах, используя базовый класс CLogify и обработчик файлов, который мы только что реализовали.
- Мы создаем переменную типа MqlLogifyHandleFileConfig для хранения конкретных настроек обработчика файлов.
- Мы настраиваем обработчик для использования желаемого формата и правил, таких как ротация файлов по размеру.
- Мы добавляем этот обработчик в базовый класс CLogify.
- Мы настраиваем форматтер, чтобы определить, как каждая запись будет отображаться в файле.
Смотрите полный код:
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,10); //--- Handler File CLogifyHandlerFile *handler_file = new CLogifyHandlerFile(); handler_file.SetConfig(m_config); handler_file.SetLevel(LOG_LEVEL_DEBUG); //--- Add handler in base class logify.AddHandler(handler_file); logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
При запуске вышеуказанного кода в настроенном каталоге (logs) будет создан новый лог-файл. Его можно просмотреть в файловом браузере.

При открытии файла в Блокноте или в любом текстовом редакторе мы увидим содержимое, сгенерированное тестами сообщений:

Прежде чем перейти к улучшениям, я выполню тест производительности, чтобы оценить, насколько она улучшилась, и у нас будет точка отсчета для сравнения в дальнейшем. Внутри функции OnTick() я добавлю запись в лог, чтобы при каждом новом тике лог-файл открывался, записывался и закрывался.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Logs logify.Debug("Debug Message"); } //+------------------------------------------------------------------+
Я буду использовать тестер стратегий для выполнения этого теста, даже в бэктесте система создания файлов работает нормально, но файлы сохраняются в другой папке, позже я покажу, как к ней получить доступ. Тест будет выполнен со следующими настройками:

Учитывая моделирование "OHLC за 1 минуту", на символе EURUSD с 7 днями тестирования, для завершения теста потребовалось 5 минут и 11 секунд, учитывая, что при каждом тике он генерирует новую запись в логе и сразу же сохраняет ее в файл.
Тестирование с файлами JSON
Наконец, я хочу показать использование лог-файлов JSON на практике, так как они могут быть полезны для некоторых конкретных сценариев. Чтобы сохранить в формате JSON, просто измените тип файла в настройках и определите корректный формат для формата JSON, вот пример реализации:
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,5,5,10); //--- Handler File CLogifyHandlerFile *handler_file = new CLogifyHandlerFile(); handler_file.SetConfig(m_config); handler_file.SetLevel(LOG_LEVEL_DEBUG); //--- Add handler in base class logify.AddHandler(handler_file); logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\\"datetime\\":\\"{date_time}\\", \\"level\\":\\"{levelname}\\", \\"msg\\":\\"{msg}\\"}")); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Вот файл, получившийся для тех же сообщений после запуска советника на графике:
{"datetime":"08:24:10", "level":"DEBUG", "msg":"RSI indicator value calculated: 72.56"}
{"datetime":"08:24:10", "level":"INFOR", "msg":"Buy order sent successfully"}
{"datetime":"08:24:10", "level":"ALERT", "msg":"Stop Loss adjusted to breakeven level"}
{"datetime":"08:24:10", "level":"ERROR", "msg":"Failed to send sell order"}
{"datetime":"08:24:10", "level":"FATAL", "msg":"Failed to initialize EA: Invalid settings"}
Заключение
В этой статье мы представили практическое и подробное руководство по выполнению основных операций с файлами: открытие, манипулирование содержимым и, наконец, закрытие файла простым способом. Я также обсудил важность настройки структуры обработчика ("handler"), благодаря которой можно адаптировать несколько характеристик, таких как тип используемого файла (например, txt, log или даже json) и каталог, в котором файл будет сохранен, что делает библиотеку действительно гибкой.
Кроме того, мы внесли конкретные улучшения в класс CLogifyHandlerFile. Эти изменения позволили записывать каждое сообщение непосредственно в лог-файл. После этой реализации в рамках статьи я также выполнил тест производительности, чтобы оценить эффективность решения. Мы использовали конкретный сценарий, в котором система имитировала выполнение торговой стратегии по EURUSD в течение недели. Во время этого теста для каждого нового "тика" рынка создавалась запись в логе. Этот процесс является чрезвычайно интенсивным, поскольку каждое изменение цены актива требует сохранения новой строки в файле.
Был зафиксирован окончательный результат: весь процесс занял 5 минут и 11 секунд. Этот результат послужит отправной точкой для следующей статьи, в которой мы реализуем систему кэша (временной памяти). Цель кэша — временно хранить записи, снижая необходимость постоянного доступа к файлу, что улучшает общую производительность.
Следите за следующей статьей, в которой мы рассмотрим еще более продвинутые методы повышения эффективности и производительности системы. До встречи!
| Имя файла | Описание |
|---|---|
| Experts/Logify/LogifTest.mq5 | Файл, в котором мы тестируем функции библиотеки, содержащий практический пример. |
| Include/Logify/Formatter/LogifyFormatter.mqh | Класс, отвечающий за форматирование сообщений, заменяющий заполнители конкретными значениями. |
| Include/Logify/Handlers/LogifyHandler.mqh | Базовый класс для управления обработчиками логов, включая настройку уровня и отправку логов. |
| Include/Logify/Handlers/LogifyHandlerConsole.mqh | Обработчик логов, который отправляет отформатированные логи непосредственно в консоль терминала в MetaTrader. |
| Include/Logify/Handlers/LogifyHandlerDatabase.mqh | Обработчик логов, который отправляет отформатированные логи в базу данных (в настоящее время он содержит только распечатку, но в скором времени мы будем сохранять их в реальной базе данных sqlite). |
| Include/Logify/Handlers/LogifyHandlerFile.mqh | Обработчик логов, который отправляет отформатированные логи в файл. |
| Include/Logify/Logify.mqh | Основной класс для управления логами, интегрирующий уровни, модели и форматирование. |
| Include/Logify/LogifyLevel.mqh | Файл, который определяет уровни логирования библиотеки Logify, позволяя осуществлять детальный контроль. |
| Include/Logify/LogifyModel.mqh | Структура, которая моделирует логи, включая такие аспекты, как уровень, сообщение, метка времени и контекст. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16986
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создание пользовательской системы определения рыночного режима на языке MQL5 (Часть 2): Советник
Создание самооптимизирующихся советников на MQL5 (Часть 4): Динамическое изменение размера позиции
Разработка продвинутых торговых систем ICT: Реализация сигналов в индикаторе Order Blocks
Разрабатываем мультивалютный советник (Часть 29): Доработка конвейера
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Ознакомьтесь с новой статьей: Освоение записи журналов (часть 4): Сохранение журналов в файлы.
Автор: joaopedrodev