English Русский Deutsch 日本語
preview
精通日志记录(第十部分):通过抑制机制避免日志重复输出

精通日志记录(第十部分):通过抑制机制避免日志重复输出

MetaTrader 5示例 |
21 3
joaopedrodev
joaopedrodev

引言

本文内容源自一位 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

附加的文件 |
Logify.zip (157.85 KB)
最近评论 | 前往讨论 (3)
hini
hini | 12 8月 2025 在 17:20
一篇非常精彩的文章,谢谢。现在日志库已经相当全面了。
joaopedrodev
joaopedrodev | 13 8月 2025 在 13:02
感谢您提出的改进建议!
hini
hini | 2 9月 2025 在 16:27
joaopedrodev # :
感谢您提出的改进建议!

您好,作者,我有一个建议。请创建一些宏。您只需包含一个文件即可, 无需任何配置 您可以直接使用默认配置的库。当日志记录被禁用时,该宏不会在最终编译生成的 ex5 文件中生成任何实际代码。

交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
利用灰色模型进行交易预测 利用灰色模型进行交易预测
本文探讨灰色模型(GM)在金融时间序列预测中的应用。我们将介绍GM的工作原理,以及其在金融序列上的具体应用方式。同时也会讨论该类模型在交易场景中的优势与局限性。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
人工原子算法(A3) 人工原子算法(A3)
本文介绍如何在 MQL5 中实现人工原子算法(A3)—— 一种受化学过程启发的元启发式优化算法。该算法只有两个可调参数:紧凑性和较小的种群规模,这保证了较高的运行速度,同时保持了足够的解质量。