《精通日志记录(第二部分):格式化日志》·综合运用
📘

《精通日志记录(第二部分):格式化日志》·综合运用

第 3/3 篇

◍ 给日志核心类接上格式化器

想让日志系统输出统一格式,得先在 CLogify 里持有一个格式化器实例。做法是引入 LogifyFormatter.mqh,并在私有区声明 CLogifyFormatter *m_formatter 指针,配合 SetFormatter / GetFormatter 做存取。 原 Append 方法只收时间戳、级别、消息、来源和元数据,上下文信息偏少。改进后加入 filename、function、line 三个参数,调用处能直接带出“哪文件哪函数哪行”产生的日志,排查 EA 异常时定位更快。 快捷方法(Debug / Info / Alert / Error / Fatal)可自动填级别,同时透传上述全部上下文参数。下面这段代码是接入格式化器后的最小骨架,注意 SetFormatter 里用 GetPointer 取对象指针,避免局部对象被回收。 在 MT5 里建好两个 mqh 后,把这段 include 和类声明丢进项目编译,若 GetFormatter() 返回非空指针即说明挂载成功。外汇与贵金属 EA 调试波动大、风险高,日志仅作辅助,不代表任何收益预期。

MQL5 / C++
class="macro">#include "LogifyModel.mqh"
class="macro">#include "Formatter/LogifyFormatter.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class : CLogify                                                  |
class=class="str">"cmt">//|                                                                    |
class=class="str">"cmt">//| [PROPERTY]                                                        |
class=class="str">"cmt">//| Name      : Logify                                                |
class=class="str">"cmt">//| Heritage  : No heritage                                           |
class=class="str">"cmt">//| Description : Core class for log management.                      |
class=class="str">"cmt">//|                                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CLogify
  {
class="kw">private:
  CLogifyFormatter   *m_formatter;
class="kw">public:
  class=class="str">"cmt">//--- Get/Set object formatter
  class="type">void               SetFormatter(CLogifyFormatter *format);
  CLogifyFormatter *GetFormatter(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set object formatter                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogify::SetFormatter(CLogifyFormatter *format)
  {
  m_formatter = GetPointer(format);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get object formatter                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLogifyFormatter *CLogify::GetFormatter(class="type">void)
  {
  class="kw">return(m_formatter);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CLogify::Append(class="type">ulong timestamp, ENUM_LOG_LEVEL level, class="type">class="kw">string message, class="type">class="kw">string origin = "", class="type">class="kw">string metadata = "");

日志写入的兜底与分级入口

CLogify 的 Append 是统一落点,任何级别日志都先过它。方法开头先判 m_formatter 是否为 NULL,若未配置格式化器直接 return(false),这条兜底能避免空指针把 EA 跑崩。 级别映射靠 switch 把枚举转成字符串:DEBUG / INFOR / ALERT / ERROR / FATAL 五种。注意 INFOR 不是 INFO,抄代码时拼错会编译不过。 拼完 MqlLogifyModel 后只做一件事——Print(m_formatter.FormatLog(data)),时间取 TimeCurrent() 两次(创建与写入同刻)。Debug 和 Infor 只是壳,把固定枚举丢给 Append,少写重复分支。 开 MT5 建个 CLogify 实例,先不挂 formatter 调一次 Debug,会返回 false 且不打印;挂上后再调,终端就能看到带级别前缀的行。外汇与贵金属 EA 调试属高风险环境,日志遗漏可能掩盖滑点异常。

MQL5 / C++
class="type">bool CLogify::Append(ENUM_LOG_LEVEL level, class="type">class="kw">string msg, class="type">class="kw">string origin = "", class="type">class="kw">string args = "", class="type">class="kw">string filename = "", class="type">class="kw">string function = "", class="type">int line = class="num">0);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Generic method for adding logs                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CLogify::Append(ENUM_LOG_LEVEL level,class="type">class="kw">string msg, class="type">class="kw">string origin = "", class="type">class="kw">string args = "",class="type">class="kw">string filename="",class="type">class="kw">string function="",class="type">int line=class="num">0)
  {
   class=class="str">"cmt">//--- If the formatter is not configured, the log will not be recorded.
   if(m_formatter == NULL)
     {
       class="kw">return(false);
     }

   class=class="str">"cmt">//--- Textual name of the log level
   class="type">class="kw">string levelStr = "";
   class="kw">switch(level)
     {
       case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break;
       case LOG_LEVEL_INFOR: levelStr = "INFOR"; break;
       case LOG_LEVEL_ALERT: levelStr = "ALERT"; break;
       case LOG_LEVEL_ERROR: levelStr = "ERROR"; break;
       case LOG_LEVEL_FATAL: levelStr = "FATAL"; break;
     }

   class=class="str">"cmt">//--- Creating a log class="kw">template with detailed information
   class="type">class="kw">datetime time_current = TimeCurrent();
   MqlLogifyModel data("",levelStr,msg,args,time_current,time_current,level,origin,filename,function,line);

   class=class="str">"cmt">//--- Printing the formatted log
   Print(m_formatter.FormatLog(data));
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Debug level message                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CLogify::Debug(class="type">class="kw">string msg, class="type">class="kw">string origin = "", class="type">class="kw">string args = "",class="type">class="kw">string filename="",class="type">class="kw">string function="",class="type">int line=class="num">0)
  {
   class="kw">return(this.Append(LOG_LEVEL_DEBUG,msg,origin,args,filename,function,line));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Infor level message                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CLogify::Infor(class="type">class="kw">string msg, class="type">class="kw">string origin = "", class="type">class="kw">string args = "",class="type">class="kw">string filename="",class="type">class="kw">string function="",class="type">int line=class="num">0)
  {
   class="kw">return(this.Append(LOG_LEVEL_INFOR,msg,origin,args,filename,function,line));

「给日志类补齐告警与错误分级接口」

在自建日志工具 CLogify 里,除了基础写入,还需要把不同严重程度的事件分开通道。下面三个成员函数就是干这件事的:Alert 对应告警级,Error 对应错误级,Fatal 对应致命级。 它们本身都不直接写文件或弹窗,只是把等级常量(LOG_LEVEL_ALERT / ERROR / FATAL)和传入的 msg、调用位置等参数一股脑丢给内部的 Append 方法,由 Append 统一决定落盘还是推送到小布盯盘面板。 调用时哪怕只传一个 msg 字符串也能跑,origin、args、filename、function、line 都有默认值空串或 0,方便在 EA 的 OnTrade 或指标计算里随手记一笔。外汇与贵金属波动剧烈,用 Fatal 标记穿仓类异常可能帮你更快定位深夜闪崩时的失效逻辑。

MQL5 / C++
class="type">bool CLogify::Alert(class="type">class="kw">string msg, class="type">class="kw">string origin = "", class="type">class="kw">string args = "",class="type">class="kw">string filename="",class="type">class="kw">string function="",class="type">int line=class="num">0)
  {
   class="kw">return(this.Append(LOG_LEVEL_ALERT,msg,origin,args,filename,function,line));
  }
class="type">bool CLogify::Error(class="type">class="kw">string msg, class="type">class="kw">string origin = "", class="type">class="kw">string args = "",class="type">class="kw">string filename="",class="type">class="kw">string function="",class="type">int line=class="num">0)
  {
   class="kw">return(this.Append(LOG_LEVEL_ERROR,msg,origin,args,filename,function,line));
  }
class="type">bool CLogify::Fatal(class="type">class="kw">string msg, class="type">class="kw">string origin = "", class="type">class="kw">string args = "",class="type">class="kw">string filename="",class="type">class="kw">string function="",class="type">int line=class="num">0)
  {
   class="kw">return(this.Append(LOG_LEVEL_FATAL,msg,origin,args,filename,function,line));
  }

◍ 用 CLogify 逐步搭出可定位的日志流

下面用一段递进代码演示自定义日志类怎么从最简格式走到生产级排错。读者可以直接开 MT5 建个 EA 把片段贴进 OnInit 验证输出。 最基础的做法只给时间+级别+消息:SetFormatter 里时间掩码用 hh:mm:ss,模板写成 [{levelname}] {date_time} => {msg}。实跑后终端出现 [DEBUG] 07:25:32 => Application initialized successfully.,证明最小配置已生效。 往里加 origin 和 args 参数后,格式变为 [{levelname}] {date_time} ({origin}) => {msg} {args}。同一次初始化会打出 [DEBUG] 07:26:18 (Network) => Connection established with the server. 以及 [ALERT] 07:26:19 (Config) => Configuration file not found! Attempt 1 of 3,复杂模块谁报错一目了然。 再补文件与行号元数据:模板挂上 (File: {filename}, Line: {line}),调用时传入 __FILE__、__FUNCTION__、__LINE__。输出 [ERROR] 07:27:15 (Database) => Error accessing database. (File: LogifyTest.mq5, Line: 25),点开就能跳到 25 行。 最后用 yyyy.MM.dd hh:mm:ss 全时间格式,在 for 循环里模拟任务升级,日志带 TaskProcessor 来源和文件行号,能直接复刻大型系统的分级记录。外汇与贵金属 EA 跑在真实账户时波动剧烈、滑点频繁,详细日志可帮你在高风险环境下快速回溯异常。

MQL5 / C++
class="macro">#include <Logify/Logify.mqh>
CLogify logify;
class="type">int OnInit()
  {
   class=class="str">"cmt">//--- Configure log format
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} => {msg}"));
   class=class="str">"cmt">//--- Log a simple message
   logify.Debug("Application initialized successfully.");
   class="kw">return(INIT_SUCCEEDED);
  }
[DEBUG] class="num">07:class="num">25:class="num">32 => Application initialized successfully.

class="type">int OnInit()
  {
   class=class="str">"cmt">//--- Configure log format
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} {args}"));
   class=class="str">"cmt">//--- Log a simple message
   logify.Debug("Connection established with the server.", "Network");
   logify.Alert("Configuration file not found!", "Config", "Attempt class="num">1 of class="num">3");
   class="kw">return(INIT_SUCCEEDED);
  }
[DEBUG] class="num">07:class="num">26:class="num">18 (Network) => Connection established with the server.
[ALERT] class="num">07:class="num">26:class="num">19 (Config) => Configuration file not found! Attempt class="num">1 of class="num">3

class="type">int OnInit()
  {
   class=class="str">"cmt">//--- Configure log format
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} (File: {filename}, Line: {line})"));
   class=class="str">"cmt">//--- Log a simple message
   logify.Error("Error accessing database.", "Database", "", __FILE__, __FUNCTION__, __LINE__);
   class="kw">return(INIT_SUCCEEDED);
  }
[ERROR] class="num">07:class="num">27:class="num">15 (Database) => Error accessing database. (File: LogifyTest.mq5, Line: class="num">25)

class="type">int OnInit()
  {
   class=class="str">"cmt">//--- Configure log format
   logify.SetFormatter(new CLogifyFormatter("yyyy.MM.dd hh:mm:ss","{date_time} [{levelname}] {msg} - {origin} ({filename}:{line})"));
   class=class="str">"cmt">//--- Cycle simulating various system operations
   for(class="type">int i=class="num">0; i<class="num">3; i++)
     {
      logify.Debug("Operation in progress...", "TaskProcessor", "", __FILE__, __FUNCTION__, __LINE__);
      if(i == class="num">1)
        {

用分级日志抓 EA 里的隐性故障

在 MT5 的 EA 调试里,光靠 Print 很容易淹没关键信号。把日志按级别拆开,能在不中断策略循环的前提下,把“可能异常”和“致命错误”分层暴露出来。 上面这段调用里,i==1 时走 Alert 级,提示“Possible inconsistency detected.”,归属 TaskValidator;i==2 时走 Fatal 级,直接报“Critical error, purchase order not executed correctly!”,归属 Core。两个分支都带 __FILE__、__FUNCTION__、__LINE__,回 MT5 编译器点日志就能跳到对应行。 真实回测控制台输出显示:2025.01.03 07:25:32 连续三条 DEBUG 级的“Operation in progress...”来自 TaskProcessor(LogifyTest.mq5:25),中间被一条 ALERT(:28)和一条 FATAL(:32)打断。也就是说,循环没停,但订单执行链路在第三次迭代已经崩了。 外汇和贵金属 EA 实盘里,这类“表面跑着、实则订单没发对”的隐患概率不低,建议把 Fatal 级接邮件或推送,DEBUG 级只在本地开,避免日志 IO 拖慢 tick 处理。

MQL5 / C++
   logify.Alert("Possible inconsistency detected.", "TaskValidator", "", __FILE__, __FUNCTION__, __LINE__);
   }
   if(i == class="num">2)
     {
      logify.Fatal("Critical error, purchase order not executed correctly!", "Core", "", __FILE__, __FUNCTION__, __LINE__);
     }
   }
  class="kw">return(INIT_SUCCEEDED);
  }
class="num">2025.01.class="num">03 class="num">07:class="num">25:class="num">32 [DEBUG] Operation in progress... - TaskProcessor(LogifyTest.mq5:class="num">25)
class="num">2025.01.class="num">03 class="num">07:class="num">25:class="num">32 [DEBUG] Operation in progress... - TaskProcessor(LogifyTest.mq5:class="num">25)
class="num">2025.01.class="num">03 class="num">07:class="num">25:class="num">32 [ALERT] Possible inconsistency detected. - TaskValidator(LogifyTest.mq5:class="num">28)
class="num">2025.01.class="num">03 class="num">07:class="num">25:class="num">32 [DEBUG] Operation in progress... - TaskProcessor(LogifyTest.mq5:class="num">25)
class="num">2025.01.class="num">03 class="num">07:class="num">25:class="num">32 [FATAL] Critical error, purchase order not executed correctly! - Core(LogifyTest.mq5:class="num">32)

「把日志层次直接落进实盘项目」

前面几节把格式化器、MqlLogifyModel 自定义、文件/函数/行号上下文都拆开了,落到 MT5 里最实在的一步,是把不同复杂度的日志层次写进同一个 EA。基础配置只留级别和消息,复杂配置再挂上时间戳与调用栈,两套输出在 Experts/Logify/LogiftTest.mq5 里能直接编译跑通。 库文件分工很固定:Logify.mqh 管核心调度,LogifyFormatter.mqh 做占位符替换,LogifyLevel.mqh 控精细度,LogifyModel.mqh 装上下文结构。你在 Include/Logify 下改一处格式化模板,所有调用点同步生效,不用挨个 EA 改字符串。 外汇与贵金属行情跳空频繁,日志若缺了行号定位,复盘隔夜止损被扫的原因会非常吃力;这套自定义日志只降低排查成本,不预测任何价格走向,实盘前请在策略测试器用历史数据验证输出是否符合预期。 下载附带的 LogifycPart2v.zip(7.87 KB)解压后拖进 MT5 数据目录,先单步跑通测试文件,再决定要不要接进你现在的盯盘 EA。

常见问题

在关键分支用不同级别写日志(信息/警告/错误),出事时直接筛错误级,能跳过噪音直奔故障点。
给写入入口加try兜底和分级落盘,写失败转存本地缓存文件,下次启动补写,避免进程直接挂掉。
小布可接入你的分级日志流,自动标红错误与告警级条目,并提示可能的故障模块,省去人工翻录。
检查格式化器里时间函数的时区与编码设置,先打裸时间戳验证,再逐步加字段定位乱码位置。
只给核心交易动作和异常分支开高级别日志,普通循环用最低级,靠分级开关控制输出量。