Искусство ведения логов (Часть 5): Оптимизация обработчика с помощью кэширования и ротации
Введение
В первой статье этой серии "Искусство ведения логов (Часть 1): Основные понятия и первые шаги в MQL5", мы приступили к созданию пользовательской библиотеки журналов для разработки советников. Также мы исследовали мотивацию создания столь важного инструмента: преодолеть ограничения собственных журналов MetaTrader 5 и предоставить надежное, настраиваемое и мощное решение для экосистемы MQL5.
Мы заложили основу нашей библиотеки, установив следующие основные требования:
- Создание устойчивой конструкции с помощью паттерна Singleton, обеспечивающего согласованность между компонентами кода.
- Расширенные возможности сохранения логов в базах данных для отслеживаемости истории в целях углубленного аудита и анализа.
- Гибкость в выводе данных, позволяющая удобно хранить и отображать журналы: в консоли, в файлах, в терминале или в базе данных.
- Классификация по уровням, отличающая информационные сообщения от важных оповещений и ошибок.
- Настройка выходного формата для удовлетворения уникальных потребностей каждого разработчика или проекта.
Благодаря этой прочной основе стало ясно, что разрабатываемая нами структура логирования будет представлять собой нечто большее, чем просто логи событий; это будет стратегический инструмент для понимания, мониторинга и оптимизации поведения советника в реальном времени.
До сих пор мы изучили основы логирования, научились их форматировать, а также рассмотрели, как обработчики управляют назначением сообщений. В прошлой статье мы узнали, как сохранять записи журнала в файл (.txt, .log, или .json). В пятой статье серии мы оптимизируем сохранение журналов в файлы, реализовав кэширование и ротацию файлов.
Добавление форматтера к каждому обработчику
До сих пор наша библиотека логов управляла форматированием сообщений через один экземпляр класса CFormatter, который централизованно размещен в базе библиотеки (CLogify). Этот подход хорошо работает в простых сценариях, но ограничивает гибкость обработчиков.
Проблема в том, что при использовании одного глобального форматтера все обработчики используют один и тот же формат, что может быть неидеально, когда для разных целей требуется разное форматирование. Например, обработчику, который записывает логи в формате JSON, может потребоваться определенная структура, а обработчику, который выводит логи на консоль, может потребоваться более удобочитаемый формат. Решение состоит в том, чтобы перенести ответственность за форматирование в базовый класс обработчика (CLogifyHandler). Таким образом, каждый обработчик может иметь свой собственный независимый форматтер, что обеспечивает больший контроль над форматированием сообщений журнала. Давайте реализуем это изменение и посмотрим, как оно повысит гибкость библиотеки.
Переходя непосредственно к коду, мы начнем с добавления экземпляра CFormatter внутрь CLogifyHandler. Поскольку это простая задача для тех, кто читал предыдущие статьи, я просто добавлю окончательный код, выделив то, что было добавлено:
//+------------------------------------------------------------------+ //| LogifyHandler.mqh | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../LogifyModel.mqh" #include "../Formatter/LogifyFormatter.mqh" //+------------------------------------------------------------------+ //| class : CLogifyHandler | //| | //| [PROPERTY] | //| Name : CLogifyHandler | //| Heritage : No heritage | //| Description : Base class for all log handlers. | //| | //+------------------------------------------------------------------+ class CLogifyHandler { protected: string m_name; ENUM_LOG_LEVEL m_level; CLogifyFormatter *m_formatter; public: CLogifyHandler(void); ~CLogifyHandler(void); //--- Handler methods virtual void Emit(MqlLogifyModel &data); // Processes a log message and sends it to the specified destination virtual void Flush(void); // Clears or completes any pending operations virtual void Close(void); // Closes the handler and releases any resources //--- Set/Get void SetLevel(ENUM_LOG_LEVEL level); void SetFormatter(CLogifyFormatter *format); string GetName(void); ENUM_LOG_LEVEL GetLevel(void); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandler::CLogifyHandler(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandler::~CLogifyHandler(void) { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter ; } } //+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandler::Emit(MqlLogifyModel &data) { } //+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandler::Flush(void) { } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandler::Close(void) { } //+------------------------------------------------------------------+ //| Set level | //+------------------------------------------------------------------+ void CLogifyHandler::SetLevel(ENUM_LOG_LEVEL level) { m_level = level; } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogifyHandler::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get name | //+------------------------------------------------------------------+ string CLogifyHandler::GetName(void) { return(m_name); } //+------------------------------------------------------------------+ //| Get level | //+------------------------------------------------------------------+ ENUM_LOG_LEVEL CLogifyHandler::GetLevel(void) { return(m_level); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogifyHandler::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
Продолжая простейшие изменения, мы удалили экземпляр CFormatter в CLogify. Части, которые были удалены из класса, выделены красным, а те, которые были добавлены, выделены зеленым:
//+------------------------------------------------------------------+ //| Logify.mqh | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" #property version "1.00" #include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh" #include "Handlers/LogifyHandler.mqh" #include "Handlers/LogifyHandlerConsole.mqh" #include "Handlers/LogifyHandlerDatabase.mqh" #include "Handlers/LogifyHandlerFile.mqh" //+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifyFormatter *m_formatter; CLogifyHandler *m_handlers[]; public: CLogify(); ~CLogify(); //--- Handler void AddHandler(CLogifyHandler *handler); bool HasHandler(string name); CLogifyHandler *GetHandler(string name); CLogifyHandler *GetHandler(int index); int SizeHandlers(void); //--- Generic method for adding logs bool Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Specific methods for each log level bool Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Get/Set object formatter void SetFormatter(CLogifyFormatter *format); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogify::CLogify() { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogify::~CLogify() { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter; } //--- Delete handlers int size_handlers = ArraySize(m_handlers); for(int i=0;i<size_handlers;i++) { delete m_handlers[i]; } } //+------------------------------------------------------------------+ //| Add handler to handlers array | //+------------------------------------------------------------------+ void CLogify::AddHandler(CLogifyHandler *handler) { int size = ArraySize(m_handlers); ArrayResize(m_handlers,size+1); m_handlers[size] = GetPointer(handler); } //+------------------------------------------------------------------+ //| Checks if handler is already in the array by name | //+------------------------------------------------------------------+ bool CLogify::HasHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(true); } } return(false); } //+------------------------------------------------------------------+ //| Get handler by name | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(m_handlers[i]); } } return(NULL); } //+------------------------------------------------------------------+ //| Get handler by index | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(int index) { return(m_handlers[index]); } //+------------------------------------------------------------------+ //| Gets the total size of the handlers array | //+------------------------------------------------------------------+ int CLogify::SizeHandlers(void) { return(ArraySize(m_handlers)); } //+------------------------------------------------------------------+ //| Generic method for adding logs | //+------------------------------------------------------------------+ bool CLogify::Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { //--- If the formatter is not configured, the log will not be recorded. if(m_formatter == NULL) { return(false); } //--- Textual name of the log level string levelStr = ""; switch(level) { case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break; case LOG_LEVEL_INFOR: levelStr = "INFOR"; break; case LOG_LEVEL_ALERT: levelStr = "ALERT"; break; case LOG_LEVEL_ERROR: levelStr = "ERROR"; break; case LOG_LEVEL_FATAL: levelStr = "FATAL"; break; } //--- Creating a log template with detailed information datetime time_current = TimeCurrent(); MqlLogifyModel data("",levelStr,msg,args,time_current,time_current,level,origin,filename,function,line); data.formated = m_formatter.FormatLog(data); //--- Call handlers int size = this.SizeHandlers(); for(int i=0;i<size;i++) { data.formated = m_handlers[i].GetFormatter().FormatLog(data); m_handlers[i].Emit(data); } return(true); } //+------------------------------------------------------------------+ //| Debug level message | //+------------------------------------------------------------------+ bool CLogify::Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_DEBUG,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Infor level message | //+------------------------------------------------------------------+ bool CLogify::Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_INFOR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Alert level message | //+------------------------------------------------------------------+ bool CLogify::Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ALERT,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Error level message | //+------------------------------------------------------------------+ bool CLogify::Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ERROR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Fatal level message | //+------------------------------------------------------------------+ bool CLogify::Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_FATAL,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogify::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogify::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
Единственная часть, которая была добавлена, была при форматировании сообщения. Раньше мы использовали форматтер внутри самого класса. Благодаря изменениям в каждом обработчике мы используем форматтер, предоставляемый обработчиком. Связывая форматтер непосредственно с каждым обработчиком, мы устраняем ограничение одного формата и делаем библиотеку более адаптируемой к различным потребностям. Теперь для каждого пункта назначения можно задать определенный стиль логирования, что гарантирует, что выходные данные будут лучше соответствовать контексту, в котором они будут использоваться. В следующей теме мы рассмотрим, как управлять логированием в запланированных циклах с помощью класса CIntervalWatcher, который будет вспомогательным классом для ротации файлов.
Создание класса CIntervalWatcher
Основная задача CIntervalWatcher — проверить, прошел ли определенный интервал времени с момента последнего вызова. Это необходимо для создания логов, которые нужно проверять через определенные промежутки времени. Независимо от того, нужно ли избегать перегрузки при записи или лучше структурировать записи, необходим механизм управления циклом, позволяющий избежать ненужной обработки на каждом этапе. Класс позволяет настроить:
- Интервал времени, который необходимо контролировать (в секундах).
- Источник времени (текущее время, GMT, локальное или торговое время сервера).
- Нужно ли возвращать true при первом выполнении.
Таким образом, класс полезен для проверки того, когда следует выполнять периодическое действие в библиотеке. Давайте создадим новую папку Utils, которая будет содержать этот файл. В конечном итоге файловый браузер должен выглядеть так:

Переходя к построению класса, сначала создадим перечисление для поддержки различных источников времени. Назовем его ENUM_TIME_ORIGIN.
//+------------------------------------------------------------------+ //| Enum for different time sources | //+------------------------------------------------------------------+ enum ENUM_TIME_ORIGIN { TIME_ORIGIN_CURRENT = 0, // [0] Current Time TIME_ORIGIN_GMT, // [1] GMT Time TIME_ORIGIN_LOCAL, // [2] Local Time TIME_ORIGIN_TRADE_SERVER // [3] Server Time }; //+------------------------------------------------------------------+
Мы добавили в класс частные переменные для хранения последнего зафиксированного момента (m_last_time), желаемого временного интервала (m_interval), начала отсчета времени (m_time_origin) и флага (m_first_return) для управления первым возвратом. В результате мы создали Set и Get для каждого частного атрибута. Чтобы упростить настройку интервалов, начала отсчета времени и первого возврата, я решил создать несколько дополнительных конструкторов для класса в помощь разработчику. Ниже представлен код с конструкторами и методами для доступа и получения приватных данных.
//+------------------------------------------------------------------+ //| class : CIntervalWatcher | //| | //| [PROPERTY] | //| Name : CIntervalWatcher | //| Type : Report | //| Heritage : No heredirary. | //| Description : Monitoring new time periods | //| | //+------------------------------------------------------------------+ class CIntervalWatcher { private: //--- Auxiliary attributes ulong m_last_time; ulong m_interval; ENUM_TIME_ORIGIN m_time_origin; bool m_first_return; public: CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(void); ~CIntervalWatcher(void); //--- Setters void SetInterval(ENUM_TIMEFRAMES interval); void SetInterval(ulong interval); void SetTimeOrigin(ENUM_TIME_ORIGIN time_origin); void SetFirstReturn(bool first_return); //--- Getters ulong GetInterval(void); ENUM_TIME_ORIGIN GetTimeOrigin(void); bool GetFirstReturn(void); ulong GetLastTime(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = PeriodSeconds(interval); m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = interval; m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(void) { m_interval = 10; // 10 seconds m_time_origin = TIME_ORIGIN_CURRENT; m_first_return = true; m_last_time = 0; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CIntervalWatcher::~CIntervalWatcher(void) { } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ENUM_TIMEFRAMES interval) { m_interval = PeriodSeconds(interval); } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ulong interval) { m_interval = interval; } //+------------------------------------------------------------------+ //| Set time origin | //+------------------------------------------------------------------+ void CIntervalWatcher::SetTimeOrigin(ENUM_TIME_ORIGIN time_origin) { m_time_origin = time_origin; } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ void CIntervalWatcher::SetFirstReturn(bool first_return) { m_first_return=first_return; } //+------------------------------------------------------------------+ //| Get interval | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetInterval(void) { return(m_interval); } //+------------------------------------------------------------------+ //| Get time origin | //+------------------------------------------------------------------+ ENUM_TIME_ORIGIN CIntervalWatcher::GetTimeOrigin(void) { return(m_time_origin); } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ bool CIntervalWatcher::GetFirstReturn(void) { return(m_first_return); } //+------------------------------------------------------------------+ //| Set last time | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetLastTime(void) { return(m_last_time); } //+------------------------------------------------------------------+
Чтобы помочь основному методу, создадим функцию GetTime, которая возвращает время на основе определенного источника:
//+------------------------------------------------------------------+ //| Get time in miliseconds | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetTime(ENUM_TIME_ORIGIN time_origin) { switch(time_origin) { case(TIME_ORIGIN_CURRENT): return(TimeCurrent()); case(TIME_ORIGIN_GMT): return(TimeGMT()); case(TIME_ORIGIN_LOCAL): return(TimeLocal()); case(TIME_ORIGIN_TRADE_SERVER): return(TimeTradeServer()); } return(0); } //+------------------------------------------------------------------+
Самый важный метод класса — Inspect(), который проверяет, достигнут ли заданный интервал. Логика следующая: при первом вызове проверяется, равно ли m_last_time нулю (вновь созданный класс), функция сохраняет текущее время и возвращает m_first_return. Если сохраненная временная метка отличается от текущей плюс интервал, это означает, что интервал достигнут, поэтому m_last_time обновляется, и функция возвращает true. Если временная метка та же, это означает, что интервал еще не достигнут, поэтому функция возвращает false.
//+------------------------------------------------------------------+ //| Check if there was an update | //+------------------------------------------------------------------+ bool CIntervalWatcher::Inspect(void) { //--- Get time ulong time_current = this.GetTime(m_time_origin); //--- First call, initial return if(m_last_time == 0) { m_last_time = time_current; return(m_first_return); } //--- Check interval if(time_current >= m_last_time + m_interval) { m_last_time = time_current; return(true); } return(false); } //+------------------------------------------------------------------+
Благодаря CIntervalWatcher мы получаем более точный контроль над созданием журналов, что позволяет программировать циклы и повышать эффективность обработки. Такой подход будет необходим для библиотеки журналирования, требующей периодического выполнения задач. Теперь, настроив периодическое выполнение действий логирования, мы можем сосредоточиться на оптимизации записи и поддержании производительности системы.
Оптимизация сохранения логов: кэширование и ротация файлов
Хотя прямая запись логов в файлы, которую мы реализовали в прошлой статье, является функциональным решением, по мере роста объема логов оно может стать неэффективным. Чтобы избежать негативного влияния на производительность, необходимо оптимизировать этот процесс. В этой теме мы рассмотрим, как реализовать систему кэширования и ротации файлов, чтобы гарантировать эффективную запись логов, не перегружая хранилище и сохраняя целостность данных.
В предыдущей статье мы более подробно обсудили, как работают эти улучшения и их преимущества:
"Представьте себе следующий сценарий: советник работает в течение нескольких недель или месяцев, записывая каждое событие, ошибку или уведомление в один и тот же файл. Вскоре этот лог достигает значительных размеров, что значительно усложняет чтение и интерпретацию информации. Именно здесь и приходит на помощь ротация. Она позволяет нам разделить эту информацию на более мелкие и организованные части, что значительно упрощает чтение и анализ.
Два наиболее распространенных способа сделать это:
- По размеру: Вы устанавливаете ограничение по размеру, обычно в мегабайтах (МБ), для лог-файла. Когда это ограничение достигается, автоматически создается новый файл, и цикл начинается заново. Этот подход очень практичен, когда основное внимание уделяется контролю роста лога, без необходимости придерживаться календаря. Как только текущий файл достигает ограничения по размеру (в мегабайтах), происходит следующий поток: Текущий лог-файл переименовывается, получая индекс, например "log1.log". Существующие файлы в каталоге также перенумеровываются, например "log1.log" становится "log2.log". Если количество файлов достигает максимально допустимого, самые старые файлы удаляются. Этот подход полезен для ограничения как места, занимаемого логами, так и количества сохраненных файлов.
- По дате: в этом случае каждый день создается новый лог-файл. Каждый из них имеет в своем имени дату создания, например log_2025-01-19.log, что решает большую часть проблем с организацией логов. Этот подход идеально подходит для тех случаев, когда вам нужно посмотреть конкретный день, не теряясь в одном гигантском файле. Это метод, который я чаще всего применяю для логов моих советников: он делает данные более понятными, структурированными и удобными для анализа.
Кроме того, вы также можете ограничить количество хранимых файлов логов. Этот контроль очень важен для предотвращения ненужного накопления старых логов. Представьте, что вы настроили сохранение 30 самых последних файлов, когда появляется 31-й, система автоматически удаляет самый старый, что предотвращает накопление очень старых логов на диске, а самые последние сохраняются.
Еще одна важная деталь — использование кэша. Вместо того чтобы записывать каждое сообщение непосредственно в файл сразу после его поступления, сообщения временно хранятся в кэше. Когда кэш достигает установленного предела, он сразу же сбрасывает все содержимое файла. Это приводит к уменьшению количества операций чтения и записи на диск, улучшению производительности и увеличению срока службы ваших устройств хранения данных."
Чтобы реализовать ротацию файлов журнала, нам сначала понадобится вспомогательный метод SearchForFilesInDirectory(). Этот метод отвечает за поиск всех файлов, присутствующих в определенном каталоге, и возврат их имен в массиве. Он использует функцию FileFindFirst() для запуска поиска. По мере нахождения файлов их имена добавляются в этот массив. После завершения процесса метод закрывает обработчик поиска, используя FileFindClose().
Но почему этот метод так важен? Всё просто! Он позволяет нам составить список существующих файлов логов, гарантируя, что класс, управляющий логами, при необходимости удалит старые файлы.
class CLogifyHandlerFile : public CLogifyHandler { private: bool SearchForFilesInDirectory(string directory, string &file_names[]); }; //+------------------------------------------------------------------+ //| Returns an array with the names of all files in the directory | //+------------------------------------------------------------------+ bool CLogifyHandlerFile::SearchForFilesInDirectory(string directory,string &file_names[]) { //--- Search for all log files in the specified directory with the given file extension string file_name; long search_handle = FileFindFirst(directory,file_name); ArrayFree(file_names); bool is_found = false; if(search_handle != INVALID_HANDLE) { do { //--- Add each file name found to the array of file names int size_file = ArraySize(file_names); ArrayResize(file_names,size_file+1); file_names[size_file] = file_name; is_found = true; } while(FileFindNext(search_handle,file_name)); FileFindClose(search_handle); } return(is_found); } //+------------------------------------------------------------------+
Теперь, когда у нас есть функция для извлечения файлов, мы можем включить ее в основной метод, отвечающий за публикацию логов, Emit(). Логика будет корректироваться в зависимости от выбранных настроек ротации.
Если ротация логов настроена на основе размера файла, функция:
- Проверяет, не превысил ли размер файла установленный лимит (m_config.max_file_size_mb).
- Выполняет поиск по всем файлам логов в каталоге.
- Удаляет старые файлы, количество которых превышает максимально допустимое (m_config.max_file_count).
- Переименовывает старые файлы, увеличивая их числовые индексы (log1.txt, log2.txt, etc.).
- Переименовывает текущий лог-файл в "log1" для сохранения последовательности.
Если ротация основана на дате, функция:
- Выполняет поиск по всем файлам логов в каталоге.
- Удаляет самые старые файлы, количество которых превышает максимально допустимое (m_config.max_file_count).
Теперь давайте рассмотрим реализацию метода Emit() с обеими логиками ротации:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerFile::Emit(MqlLogifyModel &data) { //--- Checks if the configured level allows if(data.level >= this.GetLevel()) { //--- Get the full path of the file string log_path = this.LogPath(); //--- Open file ResetLastError(); int handle_file = m_file.Open(log_path, FILE_READ | FILE_WRITE | FILE_ANSI); if(handle_file == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")"); return; } //--- Write m_file.Seek(0, SEEK_END); m_file.WriteString(data.formated + "\n"); //--- Size in megabytes ulong size_mb = m_file.Size() / (1024 * 1024); //--- Close file m_file.Close(); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } } //+------------------------------------------------------------------+
Сохранение по блокам для лучшей производительности
Введем логику, которую я считаю наиболее интересной в статье, — сохранение записей по блокам. Основная идея заключается в реализации кэша (временной памяти), где записи журнала будут храниться до тех пор, пока не достигнут определенного предела. При достижении этого лимита все записи в кэше одновременно сохраняются в лог-файле.
Реализуем эту логику поэтапно. Сначала мы создадим структуру кэша в классе CLogifyHandlerFile. В закрытом разделе класса мы добавим массив типа MqlLogifyModel для временного хранения логов. Мы также включаем переменную для управления текущим индексом последнего значения, сохраненного в кэше. При каждом добавлении новой записи этот индекс будет увеличиваться. Мы также создаем экземпляр класса CIntervalWatcher и устанавливаем в конструкторе интервал в один день. Вот как это выглядит:
class CLogifyHandlerFile : public CLogifyHandler { private: //--- Update utilities CIntervalWatcher m_interval_watcher; //--- Cache data MqlLogifyModel m_cache[]; int m_index_cache; }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::CLogifyHandlerFile(void) { m_interval_watcher.SetInterval(PERIOD_D1); ArrayFree(m_cache); m_index_cache = 0; } //+------------------------------------------------------------------+
Создав кэш и структуру обновления, переходим к следующему шагу: изменению метода Emit() для использования кэша.
Метод Emit() отвечает за обработку сообщения журнала и отправку его в указанное место назначения (в данном случае в файл). Мы адаптируем его таким образом, чтобы вместо сохранения данных напрямую в файл они временно сохранялись в кэше. Когда кэш достигает настроенного предела или заданного интервала (один день), метод вызывает функцию Flush(), которая сохраняет накопленные записи в файле. Этот интервал полезен, поскольку если данные кэшируются более одного дня, этот механизм обеспечивает ежедневное сохранение данных, а также позволяет выполнять процедуру ротации каждый день.
Измененный код:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerFile::Emit(MqlLogifyModel &data) { //--- Checks if the configured level allows if(data.level >= this.GetLevel()) { //--- Resize cache if necessary int size = ArraySize(m_cache); if(size != m_config.messages_per_flush) { ArrayResize(m_cache, m_config.messages_per_flush); size = m_config.messages_per_flush; } //--- Add log to cache m_cache[m_index_cache++] = data; //--- Flush if cache limit is reached or update condition is met if(m_index_cache >= m_config.messages_per_flush || m_interval_watcher.Inspect()) { //--- Save cache Flush(); //--- Reset cache m_index_cache = 0; for(int i=0;i<size;i++) { m_cache[i].Reset(); } } } } //+------------------------------------------------------------------+
Функция Flush() отвечает за сохранение данных кэша в файл. Этот процесс включает открытие файла, установку указателя в конец и вывод всех записей, хранящихся в кэше.
//+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Flush(void) { //--- Get the full path of the file string log_path = this.LogPath(); //--- Open file ResetLastError(); int handle_file = FileOpen(log_path, FILE_READ|FILE_WRITE|FILE_ANSI, '\t', m_config.codepage); if(handle_file == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")"); return; } //--- Loop through all cached messages int size = ArraySize(m_cache); for(int i=0;i<size;i++) { if(m_cache[i].timestamp > 0) { //--- Point to the end of the file and write the message FileSeek(handle_file, 0, SEEK_END); FileWrite(handle_file, m_cache[i].formated); } } //--- Size in megabytes ulong size_mb = FileSize(handle_file) / (1024 * 1024); //--- Close file FileClose(handle_file); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } //+------------------------------------------------------------------+
Благодаря этой реализации мы создали эффективное и масштабируемое решение для ведения логов, способное обрабатывать большие объемы данных, не ставя под угрозу производительность советника. Наконец, нам нужно убедиться, что при закрытии программы все кэшированные данные сохраняются в файле. Для этого просто вызовите метод Flush() в методе Close(), который уже вызывается в деструкторе базового класса CLogify.
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::~CLogifyHandlerFile(void) { this.Close(); } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Close(void) { //--- Save cache Flush(); } //+------------------------------------------------------------------+
Внедряя кэширование и ротацию файлов, мы сокращаем количество операций записи на диск и обеспечиваем более эффективное хранение наших логов. Это обеспечивает производительность и масштабируемость нашей библиотеки, делая ее более надежной для реальных приложений. Но действительно ли эти оптимизации что-то меняют? Давайте проверим.
Тесты производительности: измерение эффективности улучшений
Теперь, когда мы внедрили оптимизации, нам необходимо оценить их реальное влияние. Тесты производительности помогут нам понять, снижает ли кэш нагрузку записи и работает ли ротация файлов так, как ожидается. Для этого мы проведем тот же тест, что и в предыдущей статье, сравнив исходную версию библиотеки с оптимизированной версией.
Для запуска теста мы будем использовать тот же файл, но с измененным форматтером, поскольку теперь каждый обработчик имеет собственный форматтер. Изменения выделены следующим образом:
- Зеленый: дополнения к коду
- Красный: удаление
- Желтый: параметр, определяющий размер кэша. Чем больше кэш, тем быстрее обработка.
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,10); //--- Handler File CLogifyHandlerFile *handler_file = new CLogifyHandlerFile(); handler_file.SetConfig(m_config); handler_file.SetLevel(LOG_LEVEL_DEBUG); handler_file.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Add handler in base class logify.AddHandler(handler_file); logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Давайте начнем тестирование в тестере стратегий, используя те же параметры даты и символа.

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

Благодаря этому изменению нам удалось сократить время тестирования на 2 секунды, сохраняя при этом те же настройки моделирования, даты и символов. Если мы сравним с первым тестом, проведенным в предыдущей статье, который занял 5 итнут и 11 секунд, улучшение впечатляет!
Результаты показывают, что небольшая оптимизация может дать значительный прирост эффективности. Сочетание кэширования и ротации файлов делает управление журналами более гибким и надежным, подтверждая правильность сделанного выбора. Но как можно применить эти улучшения на практике? Давайте рассмотрим некоторые примеры использования.
Примеры использования библиотеки логов
Теперь, когда мы улучшили нашу библиотеку логов, пришло время применить ее на практике! Давайте рассмотрим практические примеры того, как использовать ее для создания различных типов лог-файлов, каждый из которых имеет собственное форматирование и уровень значимости.
Пример 1: Разделение логов на файлы .log и .json
В первом сценарии мы создаем два лог-файла: один в формате .log, другой - в формате .json. Каждый из них имеет определенный формат и разный уровень значимости, что упрощает управление логами и их анализ.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ALERT); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Здесь мы используем ту же переменную настройки m_config, изменяя только значения, необходимые для определения обоих форматов логов. Это упрощает настройку и делает ее более пригодной для повторного использования.
Пример 2: Хранение ошибок в файле JSON
Теперь давайте сделаем еще один шаг и настроим специальные логи для хранения только сообщений об ошибках. Для этого создадим отдельную папку, в которой будет сохранен этот .json-файл. Кроме того, мы добавляем обработчик консоли для отображения логов непосредственно в терминале.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs\\error",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ERROR); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Handler Console CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole(); handler_console.SetLevel(LOG_LEVEL_DEBUG); handler_console.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname} | {origin}] {msg}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); logify.AddHandler(handler_console); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
В этом примере мы используем три обработчика логов:
- Файл .log → Сохраняет журналы в традиционном формате.
- Файл .json → Сохраняет только сообщения об ошибках в отдельной папке.
- Консоль → Отображает логи в более удобном для пользователя виде.
Использование более "человеческого" форматирования в консоли помогает сделать вывод более понятным, а JSON ошибок упрощает их последующий анализ.
С помощью этих примеров становится ясно, как нашу библиотеку логов можно применять в реальных проектах. Гибкость в создании различных форматов и уровней значимости обеспечивает эффективное управление, помогая легче выявлять и устранять проблемы. Кроме того, модульная структура позволяет легко расширять систему логов по мере необходимости.
Теперь все, что вам осталось сделать, — это адаптировать реализацию к своим потребностям и убедиться, что ваши логи всегда хорошо организованы и доступны!
Заключение
В этой статье мы усовершенствовали нашу библиотеку логов, сделав ее более эффективной, масштабируемой и адаптируемой. Мы усовершенствовали форматирование, разрешив каждому обработчику иметь собственный форматтер, что делает сообщения более организованными и гибкими для различных нужд, таких как локальная отладка и аудит.
Мы реализовали класс CIntervalWatcher, который управляет циклами выполнения, гарантируя, что логи записываются и меняются с четко определенными интервалами. Мы также оптимизировали запись с помощью кэширования, сократив дисковые операции и лучше управляя ростом файла. Мы подтвердили эти улучшения с помощью тестов производительности, дополнительно доработав решение для поддержки высоких нагрузок. Кроме того, мы привели практические примеры, облегчающие освоение библиотеки.
Если из этой статьи можно извлечь один главный урок, то он заключается в важности рассмотрения логов как важного аспекта разработки программного обеспечения. Хорошо спроектированная система ведения логов не только облегчает отладку и последующий аудит, но и способствует безопасности, отслеживаемости и надежности советника. Внедрение хороших методов ведения логов на ранних этапах разработки может избавить вас от ряда проблем, связанных с разработкой, упростить обслуживание и повысить эффективность устранения неполадок. В следующей статье мы рассмотрим, как хранить логи в базе данных для расширенной аналитики. До встречи!
| Имя файла | Описание |
|---|---|
| Experts/Logify/LogiftTest.mq5 | Файл, в котором мы тестируем функции библиотеки, содержащий практический пример. |
| Include/Logify/Formatter/LogifyFormatter.mqh | Класс, отвечающий за форматирование сообщений, заменяющий заполнители конкретными значениями. |
| Include/Logify/Handlers/LogifyHandler.mqh | Базовый класс для управления обработчиками логов, включая настройку уровня и отправку логов. |
| Include/Logify/Handlers/LogifyHandlerConsole.mqh | Обработчик логов, который отправляет отформатированные логи непосредственно в консоль терминала в MetaTrader. |
| Include/Logify/Handlers/LogifyHandlerDatabase.mqh | Обработчик логов, который отправляет отформатированные логи в базу данных (в настоящее время он содержит только распечатку, но в скором времени мы будем сохранять их в реальной базе данных sqlite). |
| Include/Logify/Handlers/LogifyHandlerFile.mqh | Обработчик логов, который отправляет отформатированные логи в файл. |
| Include/Logify/Utils/IntervalWatcher.mqh | Проверка временного интервала для создания процедур в библиотеке |
| Include/Logify/Logify.mqh | Основной класс для управления логами, интегрирующий уровни, модели и форматирование. |
| Include/Logify/LogifyLevel.mqh | Файл, который определяет уровни логирования библиотеки Logify, позволяя осуществлять детальный контроль. |
| Include/Logify/LogifyModel.mqh | Структура, которая моделирует логи, включая такие аспекты, как уровень, сообщение, метка времени и контекст. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17137
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Математические модели в сеточных стратегиях
Автоматизация торговых стратегий на MQL5 (Часть 17): Освоение стратегии скальпинга Grid-Mart с динамической информационной панелью
Разработка инструментария для анализа движения цен (Часть 12): Внешние библиотеки (III) TrendMap
Создание торговой панели администратора на MQL5 (Часть IX): Организация кода (I)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования