Искусство работы с логами (Часть 10): Подавление повторяющихся логов (suppression)
Введение
Эта статья появилась благодаря прямому запросу от пользователя библиотеки Logify. Он указал на проблему, с которой многие сталкиваются на практике: когда объем логов становится слишком большим, повторяющиеся или неважные сообщения засоряют историю, затрудняя поиск действительно значимой информации. Если у вас есть другие идеи, вопросы или задачи, которые вы хотели бы, чтобы я разобрал, не стесняйтесь оставлять комментарии внизу. Это наше пространство, и именно благодаря вашей обратной связи библиотека развивается.
Прежде чем мы продолжим, важно понять, что означает "подавление логов". Если кратко, подавление — это процесс контроля того, какие сообщения лога записываются, с целью избежать избыточности, дублирования или информационного засорения. Вместо того чтобы просто сбрасывать все, что производит система, вы фильтруете и ограничиваете выводимые данные, гарантируя, что лог содержит только полезные, релевантные и своевременные сообщения.
В этой статье мы представим практическую реализацию системы подавления логов для Logify, разработанную для гибкости и эффективности. Вы увидите, как можно комбинировать различные методы контроля: избегать повторения одинаковых сообщений подряд, ограничивать частоту появления одного и того же лога, контролировать максимальное количество повторений и даже фильтровать по источнику или файлу, из которого пришел лог. Все это реализовано через интеллектуальную систему на основе битовых режимов (bitwise modes), которая позволяет активировать несколько правил одновременно без каких-либо сложностей.
Прочитав эту статью, вы освоите создание надежного решения для поддержания ваших логов в чистоте и эффективности, способного автоматически подавлять излишества. Вы поймете, как применять четкие правила, которые упрощают анализ и снижают уровень шума, экономя ресурсы и время. Это улучшение особенно полезно для производственных сред, где чрезмерное количество логов может снижать производительность и затруднять обслуживание. Напоминаем, что финальная версия библиотеки прикреплена в конце статьи и доступна для скачивания.
Организация файлов
В вашем проекте библиотеки Logify создайте новую папку под названием Suppression внутри основной папки Logify. Это помогает поддерживать порядок в коде и дает четкое понимание, что все, связанное с "подавлением" логов, сосредоточено здесь. Внутри папки Suppression создайте новый файл с названием LogifySuppression.mqh. Этот файл станет отправной точкой для нашего нового класса подавления, который будет контролировать, какие сообщения действительно должны появляться в логе, предотвращая повторы и избыточность.
На первом этапе класс может выглядеть просто, содержа лишь пустой конструктор и деструктор, например так:
//+------------------------------------------------------------------+ //| LogifySuppression.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CLogifySuppression { private: public: CLogifySuppression(void); ~CLogifySuppression(void); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CLogifySuppression::CLogifySuppression(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CLogifySuppression::~CLogifySuppression(void) { } //+------------------------------------------------------------------+
Это чистая стартовая точка, где ещё ничего не реализовано, только для структурирования и проверки подключения файла.
Разработка: импорты и определения
Чтобы наделить этот класс функциональностью, нам потребуется импортировать модель данных для логирования (LogifyModel.mqh), которая определяет формат сообщений, получаемых для обработки. Также мы определим константы для валидации параметров подавления, гарантируя соблюдение минимальных и максимальных пределов во избежание недопустимых конфигураций.
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../LogifyModel.mqh" //+------------------------------------------------------------------+ //| Validation constants | //+------------------------------------------------------------------+ #define MAX_SUPPRESSION_MODE 255 // Maximum valid mode combination #define MIN_THROTTLE_SECONDS 1 // Minimum interval between messages #define MIN_REPEAT_COUNT 1 // Minimum number of repetitions
Режимы подавления с битовыми перечислениями
Чтобы понять, как управлять различными способами предотвращения повторов и избыточности логов, необходимо разобраться с фундаментальной концепцией: использованием битовых перечислений.
Но что это такое? В программировании перечисление (enum)— это организованный способ определения набора именованных константы. Например, можно создать enum для уровней логирования: DEBUG, INFO, ERROR и т.д. Битовые операции— это действия, которые выполняются непосредственно над битами, составляющими целое число. Эти биты подобны переключателям, которые могут быть включены (1) или выключены (0). Когда мы объединяем enum и битовые операции, мы создаем уникальные значения, представляющие степени 2: 1, 2, 4, 8, 16 и так далее. Каждое из этих значений соответствует определенному биту в двоичном числе.
Зачем использовать битовые перечисления? Представьте, что мы хотим применить одновременно несколько режимов подавления. Например, ограничить последовательные повторяющиеся сообщения, а также ограничить сообщения, которые появляются слишком часто за короткий промежуток времени. Если бы мы использовали простые значения в enum, то могли бы выбрать только один режим за раз, верно? Это слишком ограничило бы систему. С помощью битовых перечислений мы можем комбинировать несколько режимов, активируя соответствующие биты. Комбинация создается с помощью оператора побитового ИЛИ (|), который "связывает" биты выбранных режимов в одно число.
Давайте посмотрим на определение, которое мы используем:
enum ENUM_LOG_SUPRESSION_MODE { LOG_SUPRESSION_MODE_NONE = 0, // No suppression LOG_SUPRESSION_MODE_CONSECUTIVE = 1 << 0, // 00001 = 1: Identical consecutive messages LOG_SUPRESSION_MODE_THROTTLE_TIME = 1 << 1, // 00010 = 2: Same message within X seconds LOG_SUPRESSION_MODE_BY_REPEAT_COUNT = 1 << 2, // 00100 = 4: After N repetitions LOG_SUPRESSION_MODE_BY_ORIGIN = 1 << 3, // 01000 = 8: Based on message origin LOG_SUPRESSION_MODE_BY_FILENAME = 1 << 4, // 10000 = 16: Based on source filename };
Здесь 1 << N означает "сдвинуть 1 влево N раз". Каждый сдвиг создает отдельный бит:
- 1 << 0 = 1 (двоичное 00001)
- 1 << 1 = 2 (двоичное 00010)
- 1 << 2 = 4 (двоичное 00100)
- 1 << 3 = 8 (двоичное 01000)
- 1 << 4 = 16 (двоичное 10000)
Если вы хотите активировать одновременно несколько режимов, просто объедините значения с помощью оператора ИЛИ (|):
int mode = LOG_SUPRESSION_MODE_CONSECUTIVE | LOG_SUPRESSION_MODE_THROTTLE_TIME; // 1 | 2 = 3 (00011)
Число 3 в двоичном виде 00011 указывает на то, что первые два режима активны одновременно. Почему это полезно для подавления логов?
- Гибкость: Система может применять несколько правил подавления одновременно, без необходимости создавать отдельные перечисления для каждой возможной комбинации.
- Эффективность: Проверка того, активен ли режим, проста и быстра: достаточно использовать побитовый оператор И (&), чтобы проверить, установлен ли бит этого режима.
- Расширяемость: Если в будущем потребуется добавить новые режимы подавления, мы сможем просто добавить новые биты в перечисление, не нарушая уже работающий код.
Например, чтобы узнать, активен ли режим "подавление по источнику", мы выполняем:
if((mode & LOG_SUPRESSION_MODE_BY_ORIGIN) == LOG_SUPRESSION_MODE_BY_ORIGIN) { // Apply filter by source }
Если соответствующий бит включен, условие будет истинным.
Конфигурация с помощью структуры
После определения возможных режимов подавления (супрессии) логов пришло время объединить всю логику конфигурации в одном месте. Здесь на сцену выходит структура MqlLogifySuppressionConfig. Эта структура работает как "панель управления", где вы определяете, как и когда сообщения лога должны подавляться. Идея проста: хранить параметры, управляющие поведением подавления, и обеспечить чистую, многократно используемую конфигурацию как для советников (EA), индикаторов, так и для вспомогательных библиотек.
Давайте разберем, что в ней находится:
-
mode: комбинирование режимов подавления
Это сердце конфигурации. Здесь мы храним комбинацию режимов подавления, используя целое число (int) с битовыми операциями. Это позволяет разработчику активировать несколько режимов подавления одновременно, например:
- Подавление идентичных последовательных логов
- Игнорирование повторяющихся сообщений в течение определенного интервала времени
- Остановка после определенного количества повторений
-
throttle_seconds: ограничение по времени
Это поле определяет интервал (в секундах), который должен пройти между повторяющимися сообщениями, чтобы они были показаны снова. Это полезно в случаях, когда функция инициирует один и тот же лог несколько раз в секунду, что быстро засоряет консоль и делает всё нечитаемым.
-
max_repeat_count: ограничение по количеству
Здесь мы определяем максимальное количество раз, которое одно и то же сообщение может появиться до того, как будет подавлено. Это полезно, например, для отслеживания ошибки, которая возникла несколько раз, но без ее бесконечного отображения.
-
Белый и черный списки по источнику и файлу
Часто разработчик хочет применять подавление только к сообщениям, поступающим из определенных мест кода, или же полностью исключить конкретные источники.
Именно поэтому структура включает четыре массива:
- allowed_origins[]: если этот массив заполнен, разрешены будут только эти источники.
- blocked_origins[]: любой источник, указанный здесь, всегда будет заблокирован.
- allowed_filenames[]: та же концепция, но применяемая к имени исходного файла.
- blocked_filenames[]: файлы из этого списка всегда блокируются.
Эти поля обеспечивают высочайшую степень детализации контроля. Вы могли бы, например, разрешить логи из вашего главного советника, но подавить все логи, поступающие от "шумной" сторонней библиотеки.
Давайте определим конструктор со значениями по умолчанию. Для класса подавления логов в торговых системах важно иметь сбалансированные настройки по умолчанию, которые предотвращают спам в логах, но не скрывают важную информацию. Я предложу и обосную конфигурацию по умолчанию:
- mode = LOG_SUPRESSION_MODE_THROTTLE_TIME | LOG_SUPRESSION_MODE_CONSECUTIVE | LOG_SUPRESSION_MODE_BY_REPEAT_COUNT: Объединяет три основных режима подавления, предотвращает спам в логах без потери критической информации и сохраняет отслеживаемую историю.
- throttle_seconds = 5: 5 секунд — это хороший баланс для большинства случаев: достаточно долго, чтобы не пропустить важные изменения, но достаточно коротко для сохранения отслеживаемости.
- max_repeat_count = 15: 15 повторений позволяют выявить закономерности, их достаточно для отладки в случае необходимости, и это предотвращает потоп сообщений при возникновении проблем.
//+------------------------------------------------------------------+ //| Struct: MqlLogifySuppressionConfig | //+------------------------------------------------------------------+ struct MqlLogifySuppressionConfig { // Basic configuration int mode; // Combination of suppression modes int throttle_seconds; // Seconds between messages int max_repeat_count; // Max repetitions before suppression // Origin whitelist/blacklist string allowed_origins[]; // If not empty, only these are allowed string blocked_origins[]; // Always blocked // Filename whitelist/blacklist string allowed_filenames[]; // If not empty, only these are allowed string blocked_filenames[]; // Always blocked //--- Default constructor MqlLogifySuppressionConfig(void) { mode = LOG_SUPRESSION_MODE_THROTTLE_TIME | LOG_SUPRESSION_MODE_CONSECUTIVE | LOG_SUPRESSION_MODE_BY_REPEAT_COUNT; throttle_seconds = 5; max_repeat_count = 15; ArrayResize(allowed_origins, 0); ArrayResize(blocked_origins, 0); ArrayResize(allowed_filenames, 0); ArrayResize(blocked_filenames, 0); } //--- Destructor ~MqlLogifySuppressionConfig(void) { } }; //+------------------------------------------------------------------+
Также мы определим два вспомогательных метода, чтобы пользователю не приходилось работать с массивами вручную через ArrayResize() и индексы. Будут созданы удобные методы, такие как AddAllowedOrigin(), AddBlockedFilename() и другие. Это делает конфигурацию понятной, читаемой и снижает вероятность ошибки.
//+------------------------------------------------------------------+ //| Struct: MqlLogifySuppressionConfig | //+------------------------------------------------------------------+ struct MqlLogifySuppressionConfig { //--- Helper methods for array configuration void AddAllowedOrigin(string origin) { int size = ArraySize(allowed_origins); ArrayResize(allowed_origins, size + 1); allowed_origins[size] = origin; } void AddBlockedOrigin(string origin) { int size = ArraySize(blocked_origins); ArrayResize(blocked_origins, size + 1); blocked_origins[size] = origin; } void AddAllowedFilename(string filename) { int size = ArraySize(allowed_filenames); ArrayResize(allowed_filenames, size + 1); allowed_filenames[size] = filename; } void AddBlockedFilename(string filename) { int size = ArraySize(blocked_filenames); ArrayResize(blocked_filenames, size + 1); blocked_filenames[size] = filename; } }; //+------------------------------------------------------------------+
И наконец, мы добавили метод ValidateConfig(). Он проверяет корректность предоставленных значений, чтобы избежать ошибок в работе. Среди проверок:
- throttle_seconds не может быть меньше минимального порога (исключаются нулевые или отрицательные значения).
- max_repeat_count должно быть больше некоторого минимального значения.
- Значение mode должно представлять собой допустимую комбинацию.
Этот метод возвращает false, если что-то не так, и заполняет параметр error_message описанием проблемы. Это полезно как для отладки, так и для использования в приложениях, где нужно выводить понятные пользователю сообщения об ошибках.
//+------------------------------------------------------------------+ //| Struct: MqlLogifySuppressionConfig | //+------------------------------------------------------------------+ struct MqlLogifySuppressionConfig { //--- Validates configuration parameters bool ValidateConfig(string &error_message) { if(throttle_seconds < MIN_THROTTLE_SECONDS) { error_message = "throttle_seconds must be greater than or equal to " + (string)MIN_THROTTLE_SECONDS; return false; } if(max_repeat_count < MIN_REPEAT_COUNT) { error_message = "max_repeat_count must be greater than or equal to " + (string)MIN_REPEAT_COUNT; return false; } if(mode < LOG_SUPRESSION_MODE_NONE || mode > MAX_SUPPRESSION_MODE) { error_message = "invalid suppression mode"; return false; } return true; } }; //+------------------------------------------------------------------+
Благодаря этой структуре вы можете настроить подавление логов всего несколькими командами, получить тонкий контроль над поведением системы и при этом гарантировать, что всё находится в допустимых пределах. И всё это без разрозненной логики, разбросанной по всему коду — всё централизовано, чисто и с возможностью лёгкого расширения.
Развиваем CLogifySuppression
Теперь, когда наша структура конфигурации хорошо определена, давайте создадим класс, отвечающий за применение этой конфигурации в реальном времени: CLogifySuppression. Он будет отвечать за принятие решения при каждом отправляемом логе — должно ли это сообщение появиться на консоли или нет, основываясь на активных правилах.
Прежде чем мы реализуем какую-либо реальную логику подавления, крайне важно позволить классу получать внешние инструкции о том, как он должен себя вести. Это означает, что конфигурация с правилами, лимитами и списками исключений должна поступать в класс извне, отделяя логику выполнения от способа ее параметризации. Это разделение между логикой и конфигурацией и дает системе гибкость и возможность повторного использования. Для этого мы добавили два метода в класс подавления:
class CLogifySuppression { public: //--- Configuration management void SetConfig(MqlLogifySuppressionConfig &config); MqlLogifySuppressionConfig GetConfig(void) const; }; //+------------------------------------------------------------------+ //| Updates suppression configuration | //+------------------------------------------------------------------+ void CLogifySuppression::SetConfig(MqlLogifySuppressionConfig &config) { m_config = config; string err_msg = ""; if(!m_config.ValidateConfig(err_msg)) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg); } } //+------------------------------------------------------------------+ //| Returns current configuration | //+------------------------------------------------------------------+ MqlLogifySuppressionConfig CLogifySuppression::GetConfig(void) const { return m_config; } //+------------------------------------------------------------------+
Метод SetConfig() получает по ссылке объект структуры MqlLogifySuppressionConfig и сохраняет его внутри себя. Затем он выполняет автоматическую валидацию, вызывая метод ValidateConfig() самой структуры. Если какая-либо конфигурация выходит за допустимые пределы, например, throttle_seconds меньше минимально разрешенного значения или указан недопустимый режим mode, ошибка немедленно выводится на печать, сигнализируя о проблеме. Это позволяет избежать скрытых ошибок в процессе выполнения, экономит время на отладку и сохраняет систему работоспособной, даже если она конфигурируется динамически во время выполнения.
Метод GetConfig() позволяет запросить текущее состояние сохраненной конфигурации. Это может быть полезно для диагностики, отладки или создания интерфейсов для отображения правил подавления в более крупных системах. Благодаря этому конфигурация подавления теперь является формальной, валидированной и централизованной.
Объявление приватных переменных
Теперь, когда у нас есть конфигурация, следующий шаг — сохранить данные, которые позволят применять эту конфигурацию на основе истории вызовов. Поскольку логика подавления должна "помнить", что происходило ранее — например, каким было последнее записанное сообщение или сколько раз оно повторялось, — нам нужно хранить эту информацию между вызовами. Мы добавили в класс следующие приватные поля:
class CLogifySuppression { private: //--- Configuration MqlLogifySuppressionConfig m_config; //--- State tracking string m_last_message; ENUM_LOG_LEVEL m_last_level; int m_repeat_count; datetime m_last_time; };
Давайте разберем роль каждой из них:
- m_config: это текущий экземпляр структуры конфигурации. При оценке каждого сообщения оно будет сравниваться с правилами, определенными здесь: будь то минимальный интервал (throttle_seconds), допустимое количество повторений (max_repeat_count) или списки источников и файлов.
- m_last_message: хранит содержимое последнего сообщения, прошедшего через фильтр подавления. Он служит ориентиром для определения того, является ли новое сообщение таким же, как предыдущее, — один из ключевых критериев для обнаружения последовательных повторений.
- m_last_level:хранит уровень последнего обработанного сообщения (info, warning, error и т.д). Это важно, потому что одна и та же строка сообщения может иметь разное значение на разных уровнях. Например, "Info: соединение потеряно" не должно обрабатываться так же, как "Error: соединение потеряно".
- m_repeat_count: подсчитывает, сколько раз подряд встречалось одно и то же сообщение. Он увеличивается каждый раз, когда сообщение и его уровень идентичны предыдущему вызову. Когда это число превышает установленный лимит, может быть активировано подавление.
- m_last_time:записывает временную метку последнего принятого лога. Это основа для расчета времени, прошедшего с момента последнего сообщения, и корректного применения режима throttle, который подавляет логи, возникающие с очень короткими интервалами.
Эти переменные вместе представляют внутреннее состояние подавления. Они позволяют системе применять правила с памятью, то есть с пониманием того, что произошло ранее. Это необходимо для надежного и производительного решения о том, показывать новое сообщение или нет.
Создание метода ShouldSuppress()
После того как мы определили внешнюю конфигурацию, внутреннее состояние и режимы работы, мы можем приступить к созданию сердца системы подавления: метода ShouldSuppress(). Этот метод вызывается каждый раз при отправке лога. Он получает в качестве аргумента объект MqlLogifyModel, который содержит все данные о логе: сообщение, уровень, источник, дату и имя файла. Задача ShouldSuppress() — принять решение на основе активной конфигурации о том, должен ли этот лог быть отображен или подавлен.
Мы начнем с логической основы метода, обработав самые простые режимы:
class CLogifySuppression { private: //--- Main suppression logic bool ShouldSuppress(MqlLogifyModel &data); }; //+------------------------------------------------------------------+ //| Checks if a message should be suppressed based on active modes | //+------------------------------------------------------------------+ bool CLogifySuppression::ShouldSuppress(MqlLogifyModel &data) { datetime now = data.date_time; //--- Reset counters if message or level changed if(data.msg != m_last_message || data.level != m_last_level) { m_repeat_count = 0; m_last_message = data.msg; m_last_level = data.level; m_last_time = now; return false; } //--- Increment counter once per check m_repeat_count++; //--- Check suppression modes if(((m_config.mode & LOG_SUPRESSION_MODE_BY_REPEAT_COUNT) == LOG_SUPRESSION_MODE_BY_REPEAT_COUNT) && m_repeat_count >= m_config.max_repeat_count) { return true; } if(((m_config.mode & LOG_SUPRESSION_MODE_THROTTLE_TIME) == LOG_SUPRESSION_MODE_THROTTLE_TIME) && (now - m_last_time) < m_config.throttle_seconds) { return true; } if((m_config.mode & LOG_SUPRESSION_MODE_CONSECUTIVE) == LOG_SUPRESSION_MODE_CONSECUTIVE) { return true; } m_last_time = now; return false; } //+------------------------------------------------------------------+
Здесь мы имеем дело с тремя режимами:
- По последовательности (Consecutive):если одно и то же сообщение логируется более одного раза подряд, только первое будет отображено.
- По количеству повторений (Repeat Count):вместо подавления всех одинаковых сообщений подряд, этот режим позволяет установить допуск— то есть сколько раз одно и то же сообщение может быть показано, прежде чем начнет подавляться.
- По временному интервалу (Time interval):даже если сообщения идентичны, они будут подавляться, только если отправляются с интервалом меньше заданного значения.
Такое поведение уже функционально для большинства случаев. Однако нам все еще не хватает более тонкого слоя: возможности игнорировать логи в зависимости от их источника (поле origin) или файла(filename). Эта функция ценна, когда разработчик хочет скрыть сообщения, поступающие от конкретного компонента системы, например, внутренние логи внешней библиотеки или очень подробные отладочные сообщения из одного .mq5 или .mqh файла.
Добавляем подавление по источнику и имени файла (с интеллектуальным поиском)
В более сложных средах, где несколько компонентов или модулей системы генерируют логи одновременно, разработчику часто требуется подавлять логи только из определенных частей кода, например, сообщения от автоматической торговой системы или логи, генерируемые второстепенными индикаторами. Для этой цели мы добавили возможность подавления логов на основе поля origin (логический источник лога) и поля filename (имя MQL-файла, который его сгенерировал).
Первая версия такой логики может использовать прямое сравнение с точными строками. Но на практике это оказывается ограничением. Представьте, что ваш источник называется "Trade.Signal", а вы указали строку "signal" как заблокированную. При точном сравнении это не сработает, потому что "Trade.Signal" и "signal" не идентичны. Именно поэтому мы создали вспомогательный метод под названием StringContainsIgnoreCase(). Этот метод выполняет поиск подстроки без учета регистра, что делает сравнения гораздо более гибкими и толерантными.
Вот его реализация:
class CLogifySuppression { private: //--- Helper methods for string comparison bool StringContainsIgnoreCase(string text, string search_term); }; //+------------------------------------------------------------------+ //| Checks if a string contains another string (case insensitive) | //+------------------------------------------------------------------+ bool CLogifySuppression::StringContainsIgnoreCase(string text, string search_term) { string text_lower = text; string term_lower = search_term; StringToLower(text_lower); StringToLower(term_lower); return StringFind(term_lower, text_lower) >= 0; } //+------------------------------------------------------------------+
Эта функция преобразует оба текста в нижний регистр перед поиском вхождения подстроки. Это означает, что "Trade.Signal" может быть идентифицирован как связанный со "signal" или "trade" без необходимости точно указывать полное имя источника. Это позволяет создать своего рода мини-семантическую иерархию между источниками. Например, заблокировав "signal", вы автоматически подавите логи, поступающие из "Trade.Signal", "Risk.Signal" или даже "Execution.Signal". Такая стратегия резко сокращает усилия, необходимые для настройки полезных фильтров, сохраняя при этом логику четкой и эффективной.
Применяем это в нашей логике подавления:
//+------------------------------------------------------------------+ //| Checks if a message should be suppressed based on active modes | //+------------------------------------------------------------------+ bool CLogifySuppression::ShouldSuppress(MqlLogifyModel &data) { datetime now = data.date_time; //--- Check origin-based suppression if((m_config.mode & LOG_SUPRESSION_MODE_BY_ORIGIN) == LOG_SUPRESSION_MODE_BY_ORIGIN) { //--- Check blacklist first if(ArraySize(m_config.blocked_origins) > 0) { for(int i = 0; i < ArraySize(m_config.blocked_origins); i++) { if(StringContainsIgnoreCase(data.origin, m_config.blocked_origins[i])) { return true; } } } //--- Then check whitelist if(ArraySize(m_config.allowed_origins) > 0) { bool origin_allowed = false; for(int i = 0; i < ArraySize(m_config.allowed_origins); i++) { if(StringContainsIgnoreCase(data.origin, m_config.allowed_origins[i])) { origin_allowed = true; break; } } if(!origin_allowed) { return true; } } } //--- Check filename-based suppression if((m_config.mode & LOG_SUPRESSION_MODE_BY_FILENAME) == LOG_SUPRESSION_MODE_BY_FILENAME) { //--- Check blacklist first if(ArraySize(m_config.blocked_filenames) > 0) { for(int i = 0; i < ArraySize(m_config.blocked_filenames); i++) { if(StringContainsIgnoreCase(data.filename, m_config.blocked_filenames[i])) { return true; } } } //--- Then check whitelist if(ArraySize(m_config.allowed_filenames) > 0) { bool filename_allowed = false; for(int i = 0; i < ArraySize(m_config.allowed_filenames); i++) { if(StringContainsIgnoreCase(data.filename, m_config.allowed_filenames[i])) { filename_allowed = true; break; } } if(!filename_allowed) { return true; } } } //--- Reset counters if message or level changed if(data.msg != m_last_message || data.level != m_last_level) { m_repeat_count = 0; m_last_message = data.msg; m_last_level = data.level; m_last_time = now; return false; } //--- Increment counter once per check m_repeat_count++; //--- Check suppression modes if(((m_config.mode & LOG_SUPRESSION_MODE_BY_REPEAT_COUNT) == LOG_SUPRESSION_MODE_BY_REPEAT_COUNT) && m_repeat_count >= m_config.max_repeat_count) { return true; } if(((m_config.mode & LOG_SUPRESSION_MODE_THROTTLE_TIME) == LOG_SUPRESSION_MODE_THROTTLE_TIME) && (now - m_last_time) < m_config.throttle_seconds) { return true; } if((m_config.mode & LOG_SUPRESSION_MODE_CONSECUTIVE) == LOG_SUPRESSION_MODE_CONSECUTIVE) { return true; } m_last_time = now; return false; } //+------------------------------------------------------------------+
Мы делаем то же самое для полей allowed_origins и allowed_filenames, также позволяя создавать "белый список" (whitelist), то есть фильтр, который пропускает только определенные логи и блокирует все остальные.Это является противоположностью традиционного "черного списка" (blacklist).
Такое сочетание фильтров по источнику, имени файла и регистронезависимому текстовому шаблону превращается в невероятно мощную систему выборочного подавления. Ее можно настроить как на максимальную доступность, так и на исключительную строгость, в зависимости от того, что нужно разработчику в данном контексте: будь то разработка робота, бэктестинг стратегии или анализ работы реальной системы на продакшене.
Создание метода Reset()
По мере использования системы подавления логов в ней накапливается внутренняя информация: последнее записанное сообщение, количество его повторений и время последнего появления. Это внутреннее состояние необходимо для корректного применения правил подавления. Однако в определенные моменты имеет смысл "сбросить" это состояние.
Классический пример — изменение контекста выполнения, например, при смене графика, символа или таймфрейма. В таких случаях сохранение предыдущей истории может привести к неверным решениям о подавлении, скрывая важные сообщения в новом контексте.
Чтобы решить эту проблему, мы создали метод Reset(). Он очищает внутренние данные, которые класс накопил к этому моменту, как если бы мы начинали с чистого листа:
//+------------------------------------------------------------------+ //| Resets all internal state tracking | //+------------------------------------------------------------------+ void CLogifySuppression::Reset(void) { m_last_message = ""; m_repeat_count = 0; m_last_time = 0; m_last_level = LOG_LEVEL_INFO; } //+------------------------------------------------------------------+
Этот метод чрезвычайно прост, но очень важен для обеспечения точности логики подавления. При вызове конструктора класса мы также обязательно вызываем Reset() внутри него. Это гарантирует, что каждый новый экземпляр класса начинается "с чистого листа", не неся с собой никакой предыдущей истории сообщений, повторений или временных меток.
В дальнейшем вы также можете вызывать Reset() вручную, если реализуете более продвинутое управление, например, перезапуск подавления между запусками или после определенных событий в вашем коде.
Создание вспомогательных геттеров (Getter-методов)
Даже при наличии всей автоматизации подавления разработчику во многих случаях необходимо знать, что происходит "за кулисами". Если система скрывает логи, которые вы ожидали увидеть, полезно иметь возможность исследовать внутреннее состояние класса и понять причину.
Для этого мы добавили несколько простых публичных методов, называемых геттерами, которые позволяют получить доступ к основным управляющим переменным подавления. Они не изменяют состояние класса, а просто возвращают значения, полезные для диагностики, логирования или инструментов отладки:
class CLogifySuppression { public: //--- Monitoring getters int GetRepeatCount(void) const { return m_repeat_count; } datetime GetLastMessageTime(void) const { return m_last_time; } string GetLastMessage(void) const { return m_last_message; } ENUM_LOG_LEVEL GetLastLevel(void) const { return m_last_level; } };
- GetRepeatCount() возвращает, сколько раз подряд появилось одно и то же сообщение. Это помогает понять, почему сообщение было или не было подавлено.
- GetLastMessageTime() сообщает, когда последнее сообщение прошло через фильтр. Это необходимо для проверки того, работает ли подавление по времени так, как ожидается.
- GetLastMessage() показывает буквальное содержание последнего обработанного сообщения.
- GetLastLevel() сообщает уровень (info, warning, error и т.д.) последнего сообщения, позволяя сопоставить его с контролем критичности в вашей системе.
Эти методы не обязательны для общего использования класса, но они становятся чрезвычайно ценными, когда возникает неожиданное поведение. В конце концов, автоматическое подавление сообщений — это палка о двух концах: оно уменьшает шум, но может скрывать проблемы. Наличие способов инспектировать логику изнутри резко сокращает время расследования, когда это происходит.
Интеграция подавления логов в основной класс CLogify
Теперь, когда CLogifySuppression готов и работает, пришло время интегрировать его в ядро нашей библиотеки — класс CLogify. Именно здесь происходит всё: от маршрутизации сообщений до управления обработчиками (handler'ами). И теперь здесь также будет приниматься решение о том, когда отключать повторяющиеся или неактуальные логи. Первый шаг — подключить файл с подавлением и объявить экземпляр класса suppression как приватный член класса CLogify:
//+------------------------------------------------------------------+ //| Imports | //+------------------------------------------------------------------+ #include "LogifyModel.mqh" #include "Suppression/LogifySuppression.mqh" #include "Handlers/LogifyHandler.mqh" #include "Handlers/LogifyHandlerComment.mqh" #include "Handlers/LogifyHandlerConsole.mqh" #include "Handlers/LogifyHandlerDatabase.mqh" #include "Handlers/LogifyHandlerFile.mqh" #include "Error/LogifyError.mqh" //+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifySuppression*m_suppression; }; //+------------------------------------------------------------------+
Этот экземпляр будет использоваться для внутреннего принятия решения о том, заслуживает ли конкретное сообщение быть записанным в лог или нет. В конструкторе CLogify мы инициализируем этот экземпляр:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogify::CLogify() { m_suppression = new CLogifySuppression(); } //+------------------------------------------------------------------+
И, конечно же, в деструкторе мы гарантируем освобождение памяти:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogify::~CLogify() { //--- Delete handlers int size_handlers = ArraySize(m_handlers); for(int i=0;i<size_handlers;i++) { if(CheckPointer(m_handlers[i]) != POINTER_INVALID) { m_handlers[i].Close(); delete m_handlers[i]; } } delete m_suppression; } //+------------------------------------------------------------------+
Теперь, внутри метода Append(), который отвечает за запись в лог, мы проверяем необходимость подавления сразу после создания шаблона сообщения. Если сообщение признано ненужным, оно игнорируется прямо здесь:
//+------------------------------------------------------------------+ //| Generic method for adding logs | //+------------------------------------------------------------------+ bool CLogify::Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0,int code_error=0) { //--- Ensures that there is at least one handler this.EnsureDefaultHandler(); //--- Textual name of the log level string levelStr = ""; switch(level) { case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break; case LOG_LEVEL_INFO : levelStr = "INFO"; break; case LOG_LEVEL_ALERT: levelStr = "ALERT"; break; case LOG_LEVEL_ERROR: levelStr = "ERROR"; break; case LOG_LEVEL_FATAL: levelStr = "FATAL"; break; } //--- Creating a log template with detailed information datetime time_current = TimeCurrent(); MqlLogifyModel data("",levelStr,msg,args,time_current,time_current,level,origin,filename,function,line,m_error.Error(code_error)); //--- Supression if(m_suppression.ShouldSuppress(data)) { return(true); } //--- Call handlers int size = this.SizeHandlers(); for(int i=0;i<size;i++) { data.formated = m_handlers[i].GetFormatter().Format(data); m_handlers[i].Emit(data); } return(true); } //+------------------------------------------------------------------+
Эта простая проверка предотвращает обработку ненужных сообщений. Если механизм определяет, что текущий лог является избыточным — либо потому, что он появлялся слишком много раз подряд, либо потому, что он поступил из того же места в коде, либо по любому другому настроенному критерию, — выполнение просто останавливается на этом. Вот и всё!
Тестирование
Теперь, когда мы понимаем, как работает система подавления изнутри, пришло время проверить её на практике с помощью нескольких тестов. Идея здесь — подтвердить, что каждый из режимов подавления ведёт себя так, как ожидается: подавляет дублирующиеся или нежелательные логи в соответствии с выбранной конфигурацией. Давайте двигаться шаг за шагом.
Тест 1. Подавление последовательных сообщений (LOG_SUPRESSION_MODE_CONSECUTIVE)Это самый базовый режим подавления. Логика проста: если одно и то же сообщение логируется более одного раза подряд, только первоебудет отображено. Это полезно, например, для предотвращения спама в консоли, когда один и тот же лог повторяется внутри цикла. Давайте протестируем это поведение с помощью кода, который отправляет 11 абсолютно одинаковых логов, всё с одним и тем же содержанием и из одного источника.
//+------------------------------------------------------------------+ //| Import | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify Logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MqlLogifySuppressionConfig config; config.mode = LOG_SUPRESSION_MODE_CONSECUTIVE; Logify.Suppression().SetConfig(config); for(int i=0;i<11;i++) { Logify.Info("Check signal buy", "Signal"); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
При запуске приведенного выше кода результат на консоли будет следующим:
2025.07.31 04:34:26 [INFO]: Check signal buy
Вот и всё. Никаких повторений. Даже если сообщение было записано в лог 11 раз, так как все они были идентичны и следовали подряд, система решила, что достаточно показать его один раз. Это демонстрирует, что режим работает корректно.
Тест 2. Подавление по количеству повторений (LOG_SUPPRESSION_MODE_BY_REPEAT_COUNT)Этот режим предлагает немного больше гибкости, чем предыдущий. Вместо подавления всех одинаковых сообщений подряд, он позволяет установить допуск— то есть сколько раз одно и то же сообщение может быть показано, прежде чем начнет подавляться. Этот вариант полезен, когда вы хотите увидеть ограниченное количество повторений, прежде чем система заглушит остальные.
Давайте настроим систему на допуск максимум 2 повторений одного и того же сообщения.
//+------------------------------------------------------------------+ //| Import | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify Logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MqlLogifySuppressionConfig config; config.mode = LOG_SUPRESSION_MODE_BY_REPEAT_COUNT; config.max_repeat_count = 2; Logify.Suppression().SetConfig(config); for(int i=0;i<11;i++) { Logify.Info("Check signal buy", "Signal"); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Ожидаемый результат на консоли:
2025.07.31 04:40:49 [INFO]: Check signal buy 2025.07.31 04:40:49 [INFO]: Check signal buy
Именно так, как мы настроили: появляются только первые двасообщения. Остальные были автоматически отброшены, поскольку превысили лимит повторений. Такой контроль особенно полезен в средах, генерирующих очень подробные логи, но где мы всё еще хотим видеть первые предупреждающие сигналы.
Тест 3. Подавление по временному интервалу (LOG_SUPPRESSION_MODE_THROTTLE_TIME)Здесь подавление основано на времени между сообщениями. Даже если сообщения идентичны, они будут подавляться,только если отправляются с интервалом меньше заданного значения.
Давайте настроим систему на разрешение одного и того же сообщения каждую 1 секунду. Чтобы смоделировать это, мы выведем одно и то же сообщение 11 раз с Sleep(200) между ними (то есть 200 миллисекунд между каждым логом). Таким образом, каждую секунду будет проходить 5 сообщений — и система должна отображать только одно в секунду, отбрасывая остальные.
//+------------------------------------------------------------------+ //| Import | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify Logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MqlLogifySuppressionConfig config; config.mode = LOG_SUPRESSION_MODE_THROTTLE_TIME; config.throttle_seconds = 1; Logify.Suppression().SetConfig(config); for(int i=0;i<11;i++) { Logify.Info("Check signal buy", "Signal"); Sleep(200); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
При запуске на консоли должно появиться что-то вроде:
2025.07.31 04:45:26 [INFO]: Check signal buy 2025.07.31 04:45:27 [INFO]: Check signal buy 2025.07.31 04:45:28 [INFO]: Check signal buy
Появляются три записи лога — по одной в секунду — в то время как остальные были отброшены, поскольку выходили за пределы разрешенного интервала. Этот режим особенно интересен для событий, частота которых может колебаться, таких как обновления цен или проверки состояния рынка.
Тест 4. Подавление по источнику (origin) (LOG_SUPPRESSION_MODE_BY_ORIGIN)В этом режиме система блокирует сообщения на основе источника,указанного в логе. Если конкретный источник отмечен в черном списке (blacklist), любое сообщение, исходящее от него, будет проигнорировано, независимо от содержания или временных интервалов между ними. В примере ниже мы заблокировали источник "Signal" и пропустили только источник "Trade":
//+------------------------------------------------------------------+ //| Import | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify Logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MqlLogifySuppressionConfig config; config.mode = LOG_SUPRESSION_MODE_BY_ORIGIN; config.AddBlockedOrigin("signal"); Logify.Suppression().SetConfig(config); for(int i=0;i<11;i++) { Logify.Info("Check signal buy", "Signal"); } Logify.Info("Purchase order sent successfully", "Trade"); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Результат на консоли:
2025.07.31 04:48:36 [INFO]: Purchase order sent successfully
Появилось только сообщение с источником "Trade". Все остальные были подавлены, поскольку принадлежат явно заблокированному источнику.
Режим подавления по файлу работает практически идентично, но критерием блокировки является имя файла, из которого был вызван лог, а не логический источник, определенный в самом сообщении. По этой причине тесты подавления по источнику и по файлу используют одну и ту же структуру кода. Разница была бы в вызове при проверке: один проверяет источник (origin), а другой — название файла (filename). Поэтому мы считаем, что этот тест также подтверждает корректность работы подавления по файлу.
Автоматическое определение и настройка языка
До сих пор класс CLogifyError всегда запускался с сообщениями об ошибках на английском языке. Поначалу это работало хорошо, но была серьезная проблема: язык ошибок всегда оставался одним и тем же, даже если терминал пользователя был настроен на другой язык, например, испанский, французский или португальский. С развитием многоязычной поддержки в Logify появился смысл сделать еще один шаг вперед: автоматически адаптировать язык сообщений об ошибках по умолчанию под язык, установленный в терминале MetaTrader. Для этого мы внесли небольшое, но важное изменение в конструктор класса. Теперь, вместо прямого запуска набора ошибок на английском, мы позволяем самому терминалу сообщить, какой язык использовать:
CLogifyError::CLogifyError()
{
SetLanguage(GetLanguageFromTerminal());
} Метод GetLanguageFromTerminal() использует нативную функцию TerminalInfoString(TERMINAL_LANGUAGE) для получения текущего языка, установленного в MetaTrader. Возвращаемое значение — это строка с названием языка, например "French", "Korean" или "Portuguese (Brazil)". Затем мы сопоставляем (маппим) её с нашим перечислением ENUM_LOG_LANGUAGE, которое представляет языки, поддерживаемые системой ошибок Logify:
ENUM_LOG_LANGUAGE CLogifyError::GetLanguageFromTerminal(void) { string lang = TerminalInfoString(TERMINAL_LANGUAGE); if(lang == "German") return LOG_LANGUAGE_DE; if(lang == "Spanish") return LOG_LANGUAGE_ES; if(lang == "French") return LOG_LANGUAGE_FR; if(lang == "Italian") return LOG_LANGUAGE_IT; if(lang == "Japanese") return LOG_LANGUAGE_JA; if(lang == "Korean") return LOG_LANGUAGE_KO; if(lang == "Portuguese (Brazil)" || lang == "Portuguese (Portugal)") return LOG_LANGUAGE_PT; if(lang == "Russian") return LOG_LANGUAGE_RU; if(lang == "Turkish") return LOG_LANGUAGE_TR; if(lang == "Chinese (Simplified)" || lang == "Chinese (Traditional)") return LOG_LANGUAGE_ZH; //--- Default language: English return LOG_LANGUAGE_EN; }
Такая автоматическая адаптация особенно полезна для дистрибьюторов, трейдеров или компаний, работающих с международной аудиторией. Теперь библиотека "говорит на языке терминала" без необходимости ручной настройки. Это снижает трение и избавляет менее технически подкованных пользователей от необходимости выяснять, как установить нужный язык для сообщений об ошибках. А что, если по какой-то причине язык терминала не распознан или не сопоставлен? Нет проблем — система автоматически переключается обратно на английский, гарантируя функциональный интерфейс даже в исключительных случаях.
Это изменение, хотя и простое в реализации, кардинально улучшает удобство использования библиотеки и приводит поведение Logify в соответствие с принципом автоматического комфорта: система адаптируется к пользователю, а не наоборот.
Заключение
Благодаря всему, что мы рассмотрели, Logify стал еще более интеллектуальным. Теперь он способен распознавать, когда становится слишком повторяющимся в логах, а также автоматически «говорит» на языке вашего терминала без необходимости что-либо настраивать.
Было создано несколько способов подавления повторяющихся сообщений, которые можно использовать по отдельности или вместе, в зависимости от того, что лучше всего подходит для вашего проекта:
- Повторяющиеся сообщения подряд:сокращает поток идентичных логов, следующих один за другим.
- Минимальное время между логами:предотвращает появление одного и того же сообщения несколько раз за несколько секунд.
- Повторение только после определенного количества:показывает сообщение снова, только если количество повторений превысило лимит.
- Один и тот же фрагмент кода:блокирует дублирующиеся логи, поступающие из одного и того же места.
- Одно и то же содержание из разных файлов:блокирует идентичные повторения, даже если они приходят из другого файла.
Все это можно активировать просто и напрямую через конфигурацию, не усложняя ваш код. Более того, благодаря новому автоматическому определению языка библиотека сама выбирает идеальный язык на основе настроек вашего терминала, что очень помогает при работе в международных средах или с другими командами.
Если у вас есть новые идеи, вы хотите предложить новый режим подавления или нашли что-то, что можно улучшить, просто оставьте комментарий. Logify всегда открыт для изменений и улучшений. По мере его развития мы будем публиковать новые статьи, чтобы держать вас в курсе.
| Название файла | Описание |
|---|---|
| Experts/Logify/LogiftTest.mq5 | Файл, в котором мы тестируем возможности библиотеки, содержащий практический пример использования |
| Include/Logify/Error/Languages/ErrorMessages.XX.mqh | Содержит сообщения об ошибках для каждого поддерживаемого языка, где XX представляет собой аббревиатуру языка |
| Include/Logify/Error/Error.mqh | Структура данных для хранения информации об ошибке |
| Include/Logify/Error/LogifyError.mqh | Класс для получения детальной информации об ошибках |
| Include/Logify/Formatter/LogifyFormatter.mqh | Класс, отвечающий за форматирование записей лога, замену плейсхолдеров на конкретные значения |
| Include/Logify/Handlers/LogifyHandler.mqh | Базовый класс для управления обработчиками (handler'ами) логов, включая установку уровня и отправку логов |
| Include/Logify/Handlers/LogifyHandlerComment.mqh | Обработчик логов, который отправляет отформатированные логи напрямую в комментарий на графике терминала MetaTrader |
| Include/Logify/Handlers/LogifyHandlerConsole.mqh | Обработчик логов, который отправляет отформатированные логи в консоль терминала MetaTrader |
| Include/Logify/Handlers/LogifyHandlerDatabase.mqh | Обработчик логов, который отправляет отформатированные логи в базу данных (в текущей версии только имитирует отправку, но вскоре будет сохранение в реальную базу данных SQLite) |
| Include/Logify/Handlers/LogifyHandlerFile.mqh | Обработчик логов, который отправляет отформатированные логи в файл |
| Include/Logify/Suppression/LogifySuppression.mqh | Отвечает за применение интеллектуальных правил подавления (супрессии) сообщений лога, отфильтровывая ненужные повторения |
| Include/Logify/Utils/IntervalWatcher.mqh | Проверяет, прошел ли заданный интервал времени, позволяя создавать периодические процедуры внутри библиотеки |
| Include/Logify/Logify.mqh | Основной класс для управления логированием, объединяющий уровни, модели и форматирование |
| Include/Logify/LogifyBuilder.mqh | Класс, отвечающий за создание объекта CLogify, упрощая его конфигурацию (паттерн "Строитель") |
| Include/Logify/LogifyLevel.mqh | Файл, определяющий уровни логирования библиотеки Logify, позволяя осуществлять детальный контроль |
| Include/Logify/LogifyModel.mqh | Структура, моделирующая записи лога, включающая такие детали, как уровень, сообщение, временная метка и контекст |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/19014
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Неопределенность как модель (Часть 1): Случайные величины — язык неопределенности
Знакомство с языком MQL5 (Часть 40): Руководство для начинающих по работе с файлами в MQL5 (II)
Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (STCA)
Моделирование рынка (Часть 22): Первые шаги на SQL (V)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Спасибо за ваши предложения по улучшению!
Здравствуйте, автор, у меня есть предложение. Создайте несколько макросов. Вам нужно будет включить только один файл. Конфигурация не требуется. Вы можете использовать библиотеку с конфигурацией по умолчанию. При отключении логирования макрос не генерирует фактический код в конечный скомпилированный ex5.