精通日志记录(第十部分):通过抑制机制避免日志重复输出
引言
本文内容源自一位 Logify 库使用者的直接需求。他提出了一个很多开发者实际都会遇到的难题:当日志数量激增时,大量重复、无关信息会污染日志历史记录,导致难以定位关键信息。如果你有其他想法、疑问或是希望我讲解的技术难点,欢迎在文末留言。这是我们交流的平台,正是倾听大家的需求,这个函数库才能持续迭代完善。
正式展开内容之前,我们先要搞懂 “日志抑制(log suppression)” 的含义。简单来说,抑制机制就是管控哪些日志消息需要被记录,目的是防止信息过量、内容冗余以及日志污染。与其简单地输出系统产生的所有内容,不如进行过滤和限制,确保日志中只包含那些有用、相关且适时出现的信息。
本文将为 Logify 库提供一套可落地的日志抑制系统实现方案,兼顾灵活性与执行效率。你将学习如何组合多种管控策略:屏蔽连续重复的相同日志、限制同一条日志的出现频率、管控最大重复次数,甚至可以根据日志来源、所属文件进行过滤。整套方案基于位运算模式构建智能控制系统,能够轻松同时启用多条规则,无需复杂处理。
读完本文后,你将掌握构建稳定可靠方案的方法,让日志精简高效,自动过滤冗余输出。你将学会配置清晰的过滤规则,便于日志分析、减少无效信息,节约系统资源与排查时间。这项优化在实际运行环境中尤为重要,海量日志会拖累运行性能,提升维护难度。请注意,函数库最终版本附在文章末尾,可供下载。
文件目录结构
在你的 Logify 库项目主目录下,新建名为 Suppression 的文件夹。这样便于代码分层管理,所有和日志抑制相关的代码统一存放于此。在 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
基于位枚举实现日志抑制模式
想要掌握多种日志去重、限流管控方式,需要先理解一个核心概念:位枚举(bitwise enumerations)。
那么位枚举到底是什么?在编程中,枚举(enum)可以规范地定义一组带名称的常量。举个例子,你可以创建枚举用来表示日志等级:DEBUG、INFO、ERROR 等等,位运算则是直接操作整数内部二进制比特位的运算。每一个比特位如同开关,只有开启(1)和关闭(0)两种状态。将枚举与位运算结合时,我们定义取值为 2 的幂次的常量:1、2、4、8、16……每一个数值对应二进制数字里独立的一个比特位。
为什么要使用位枚举?设想一个场景:我们希望同时启用多条日志抑制规则。例如:既要限制连续重复日志,同时还要限制短时间内频繁刷屏的日志。如果只是普通枚举,同一时间只能选择单一模式,存在很大局限。这会极大限制系统能力。借助位枚举,我们可以激活对应比特位,自由组合多种工作模式。模式组合依靠按位或运算符(|)实现,它会把所需模式对应的比特位合并为一个数值。
下面我们来看项目中实际采用的定义:
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 (binary 00001)
- 1 << 1 = 2 (binary 00010)
- 1 << 2 = 4 (binary 00100)
- 1 << 3 = 8 (binary 01000)
- 1 << 4 = 16 (binary 10000)
如果你想要同时启用多种模式,只需使用按位或运算符 `|` 把多个值组合起来:
int mode = LOG_SUPRESSION_MODE_CONSECUTIVE | LOG_SUPRESSION_MODE_THROTTLE_TIME; // 1 | 2 = 3 (00011)
数值 3 的二进制 00011 代表前两种模式同时处于激活状态。这套机制对日志抑制有什么好处?
- 灵活性:系统可以同时启用多条抑制规则,不需要为每一种组合单独定义枚举。
- 高效性:判断某条模式是否启用简单迅速,只需使用按位与运算符 `&`,检测对应比特位是否被置 1。
- 可扩展性:未来新增日志抑制模式时,只需在枚举中新增比特位,不会破坏现有正常运行的代码。
举个例子,判断 “按来源抑制” 模式是否开启:
if((mode & LOG_SUPRESSION_MODE_BY_ORIGIN) == LOG_SUPRESSION_MODE_BY_ORIGIN) { // Apply filter by source }
只要对应的比特位为 1,条件判定结果就为真。
使用结构体进行配置
定义完各类可用抑制模式后,我们需要把所有配置逻辑统一封装,`MqlLogifySuppressionConfig` 结构体就承担这个作用。该结构体相当于一块 “控制面板”,用来定义日志消息在何种场景、何种条件下被抑制。设计思路很清晰:存放控制抑制行为的各项参数,提供整洁、可复用的配置方式,无论是 EA、指标还是工具函数库都可以直接使用。
我们逐一拆解结构体内部成员:
-
mode:组合多种抑制模式
这是整套配置的核心。我们用整型变量配合位运算,存放多种抑制模式的组合。开发者可以同时启用多条规则,例如:
- 屏蔽连续出现的相同日志
- 限定时间区间内重复消息不再输出
- 达到指定重复次数后停止打印
-
throttle_seconds:基于时间限制
该字段设定重复日志再次输出所需的最小间隔(单位:秒)。适用于这类场景:某个函数每秒多次输出同一条日志,短时间内刷屏,导致控制台难以阅读。
-
max_repeat_count:基于数量限制
该参数设定同一条消息最多允许打印多少次,超出后执行抑制。典型用途:捕获少量报错信息,但避免报错无限循环刷屏。
-
基于来源与文件的黑白名单
开发时常有这样的需求:仅对代码中特定位置产生的日志启用抑制,或是直接屏蔽某些来源的全部日志。
因此结构体内部包含四个数组:
- allowed_origins[]:如果数组不为空,仅允许名单内来源输出日志。
- blocked_origins[]:名单内的所有来源一律被屏蔽。
- allowed_filenames[]:规则同上,作用于源文件名称。
- blocked_filenames[]:名单内文件产生的日志全部屏蔽。
借助这些字段,我们可以实现精细化管控。例如:允许主 EA 输出日志,但屏蔽第三方库大量刷屏的日志。
接下来我们定义带默认参数的构造函数。对于交易系统内的日志抑制类,合理的默认配置至关重要:既要防止日志刷屏,又不能掩盖关键信息。给出推荐默认配置并说明理由:
- 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 不能低于最小阈值(禁止传入 0 或负数)。
- 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() 接收结构体引用,将配置保存在类内部。随后自动调用结构体的 ValidateConfig() 进行参数校验。如果参数超出合法范围(例如间隔小于最小值、非法 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:当前生效配置结构体实例。每一条日志评估时,都会参照这里定义的规则:最小间隔、允许重复次数、来源与文件过滤清单。
- m_last_message:保存上一条通过过滤的日志文本。用来对比新消息是否和上一条完全一致,是识别连续重复日志的核心依据。
- m_last_level:保存上一条日志的等级(信息、警告、错误等)。同一段文本搭配不同日志等级含义完全不同。例如:“信息:连接断开”和“错误:连接断开”,不能当作同一条重复日志处理。
- m_repeat_count:统计连续出现相同消息的次数。当消息内容、日志等级和上一条完全一致时计数器自增。一旦计数超过设定阈值,触发抑制。
- m_last_time:记录上一条放行日志的时间戳。- 用来计算距离上次输出日志的时间间隔,实现限流模式,抑制短时间内高频打印的日志。
这些变量共同构成抑制模块的内部状态。系统具备记忆能力,能够结合历史信息执行规则,保证高效、可靠地判断是否展示新日志。
创建 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; } //+------------------------------------------------------------------+
有三种模式:
- 连续重复抑制:如果同一条消息 连续多次 输出日志,仅展示第一条。
- 按重复次数抑制:不同于直接屏蔽所有连续相同消息,该模式支持设置 容忍阈值—— 即相同消息最多允许输出多少次,超出后才开始执行抑制。
- 按时间间隔抑制:即便消息内容完全一致,也只有当两次日志输出间隔 小于配置阈值时,才会执行抑制。
这套处理逻辑已经能够适配绝大多数场景。但目前仍然缺少一层精细化控制能力:依据来源(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; } //+------------------------------------------------------------------+
该函数会先将两段文本统一转为小写,再检索子串是否存在。也就是说,检索关键词 "signal" 或 "trade" 就能够匹配到 "Trade.Signal",无需完整填写来源全称。借助该特性,我们可以在各个来源之间构建一套 简易语义层级。例如,仅屏蔽关键词 "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 字段采用相同匹配逻辑,同时支持配置 白名单;也就是一种仅放行指定日志、屏蔽其余所有日志的过滤器,与传统黑名单作用相反。
将来源、文件名过滤器与不区分大小写文本匹配模式相互结合,最终构成一套功能极强的选择性日志抑制系统。开发者可以根据场景需求,将其配置为宽松模式或严苛模式,无论是 EA 机器人开发、策略回测,还是分析实际运行环境下的实时系统都适用。
创建 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)
即便整套抑制机制全自动运行,开发者时常仍需要了解内部运行情况。当系统屏蔽了你预期需要展示的日志时,能够查看类内部状态,便于定位问题成因。
为此我们新增若干简单的公开方法,也就是 读取器(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 () 返回上一条日志的等级(信息、警告、错误等),方便和系统的告警级别管控逻辑交叉核对。
日常使用类时,这些方法属于可选功能;但出现异常现象时作用巨大。毕竟自动屏蔽日志是一把双刃剑:虽然减少冗余信息,但也有可能掩盖故障。提供内部状态查看手段,可以大幅缩短异常出现时的排查耗时。
将日志抑制模块集成至 CLogify 主类
CLogifySuppression 开发完成后,接下来需要将它集成到函数库核心:CLogify 类。消息路由、处理器调度等所有核心流程都在该类中实现。现在,它还将承担判定何时屏蔽重复、无关日志的职责。第一步:引入抑制模块头文件,并在 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
最终展示 3 条日志,每秒一条;其余日志因不满足时间间隔要求被丢弃。该模式非常适合触发频率不稳定的事件,例如价格更新、市场状态检测等场景。
测试 4. 按来源抑制(LOG_SUPPRESSION_MODE_BY_ORIGIN)在此模式下,系统依据日志填写的来源(origin)拦截消息。倘若某个来源被加入黑名单,所有来自该来源的消息都会被忽略,不受消息内容、输出间隔影响。下面示例中,我们屏蔽 “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” 的消息正常输出。其余消息均被抑制,因为它们来自明确屏蔽的来源。
基于文件的抑制模式运行逻辑基本一致,区别在于屏蔽依据是触发日志的文件名,而非日志内定义的逻辑来源。因此,按来源抑制与按文件抑制的测试代码结构相同。二者差异仅存在于校验环节:一个校验来源,另一个校验文件名。因此我们认为,本组测试同样可以验证按文件抑制功能是否正常。
自动识别并适配语言
在此之前,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/LogifyTest.mq5 | 用于测试库功能的文件,包含一个实际示例 |
| Include/Logify/Error/Languages/ErrorMessages.XX.mqh | 统计每种语言的报错消息数量,其中 X 代表语言的缩写 |
| Include/Logify/Error/Error.mqh | 用于存储报错的数据结构 |
| Include/Logify/Error/LogifyError.mqh | 用于获取详细报错信息的类 |
| Include/Logify/Formatter/LogifyFormatter.mqh | 负责格式化日志记录的类,将占位符替换为具体值 |
| Include/Logify/Handlers/LogifyHandler.mqh | 用于管理日志处理器的基类,包括级别设置和日志发送 |
| Include/Logify/Handlers/LogifyHandlerComment.mqh | 将技术日志直接输出到MetaTrader图表注释的处理器 |
| Include/Logify/Handlers/LogifyHandlerConsole.mqh | 将格式化后的日志直接发送到 MetaTrader 终端控制台的日志处理器 |
| Include/Logify/Handlers/LogifyHandlerDatabase.mqh | 将格式化后的日志发送到数据库的日志处理器(目前仅包含打印输出,但很快我们会将其保存到真正的 sqlite 数据库中) |
| Include/Logify/Handlers/LogifyHandlerFile.mqh | 将格式化后的日志发送到文件的日志处理器 |
| Include/Logify/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 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
新手在交易中的10个基本错误
人工原子算法(A3)
感谢您提出的改进建议!
您好,作者,我有一个建议。请创建一些宏。您只需包含一个文件即可, 无需任何配置。 您可以直接使用默认配置的库。当日志记录被禁用时,该宏不会在最终编译生成的 ex5 文件中生成任何实际代码。