《精通日志记录(第二部分):格式化日志》(基础篇)
📘

《精通日志记录(第二部分):格式化日志》(基础篇)

第 1/3 篇

「给日志加时间戳和等级标记」

MT5 自带的 Print() 只把字符串原样丢进日志,排查多品种 EA 时很难定位是哪一行、什么时间出的信息。要在日志里带上时间戳和级别,得自己拼字符串。 下面这段在每次写日志前取当前时间并格式化成「时:分:秒」,再叠一个自定义等级前缀。实盘里把级别分清楚,回看 Experts 标签能省掉大量翻找时间。 外汇与贵金属杠杆高,日志只是辅助,任何信号都只是概率倾向,不代表方向必现。

MQL5 / C++
class="type">class="kw">string GetLogPrefix()
{
   class="type">class="kw">string time = TimeToString(TimeCurrent(), TIME_SECONDS);
   class="kw">return "[" + time + "][INFO] ";
}

为什么原生日志不够用

MT5 自带的日志在跑 EA 回测时经常把同类事件挤成一团,想事后按字段筛一笔订单的上下文得手动翻屏。我们在前一篇里先把底层骨架定死:用单例模式保证全局只有一套写日志入口,避免多个模块各写各的导致时间戳错乱。 持久化直接落库而不是只写 txt,这样同一段测试跑三次,也能在数据库里按 session_id 拉出横向对比。级别区分(INFO / WARN / ERROR)让关键报警不会被信息流淹没。 但骨架只是能写,真正影响排查效率的是「怎么呈现」。一条没有格式化锚点的日志,在 EA 高压测试里可能混着报价tick和订单回执,读起来像乱码。下一段我们直接拆格式化模块怎么解决这件事。

◍ 日志格式把杂乱记录变成可读线索

日志格式本质是一套组织规则,决定程序运行时的每条记录怎么呈现。它把事件级别、时间戳、来源模块和描述消息打包成固定结构,让排查的人一眼能定位上下文。 没有格式的日志只是裸文本,比如只写「订单发送成功」,你不知道是哪天、哪个模块、花了多久。结构化之后,同样的信息带上时间和来源,复杂系统里查问题效率差出数量级。 格式不是死标准,项目可以按需调。突出关键事件、标记来源、塞进上下文元数据,都是常见自定义做法。 下面两段对比很直观:第一行没头没尾,第二行带上了日期时间和 CTradeManager 来源,还标了 32 毫秒响应。MT5 里接 Trade 类日志时,照第二行那种写法挂上时间和模块名,事后回查会省很多事。

MQL5 / C++
DEBUG: 订单发送成功,服务器响应时间为 32毫秒
[class="num">2025-class="num">01-class="num">02 class="num">12:class="num">35:class="num">27] DEBUG(CTradeManager): 订单发送成功,服务器响应时间为 32毫秒

「占位符驱动的日志格式化器怎么搭」

格式化器的核心就是占位符:模板里留好替换点,运行时把日志事件里的原始字段填进去,产出可读文本。把它想成一台转换器,输入是带级别、消息、时间等字段的数据模型,输出是按你给的模板拼出来的字符串。 我们这套库会暴露几个关键占位符:{levelname} 给人类可读级别(DEBUG/ERROR/FATAL),{msg} 是事件描述,{args} 装动态上下文,{timestamp} 是毫秒级时间戳,{date_time} 是标准日期时间,{origin} 标来源模块,{filename}/{function}/{line} 直接定位代码行,调试时省事。 下面这段模板和渲染结果能直接看出替换逻辑——模板写 {date_time} {levelname}: {msg},实际跑出来就是 12/04/2025 00:25:45 DEBUG: IFR 指标已成功插入图表!。外汇与贵金属 EA 跑在 MT5 上日志量很大,这种精确占位符结构能让你快速筛出某模块在某行的报错,高风险环境下排障效率直接影响仓位安全。

MQL5 / C++
{date_time} {levelname}: {msg}
<span class="number">class="num">12</span>/<span class="number">class="num">04</span>/<span class="number">class="num">2025</span> <span class="number">class="num">00</span>:<span class="number">class="num">25</span>:<span class="number">class="num">45</span> DEBUG: IFR 指标已成功插入图表!

自己写一个日志格式化器

在 MT5 的 Include/Logify/Formatter 目录下建 LogifyFormatter.mqh,类 CLogifyFormatter 干一件事:按开发者给的模板把日志拼出来,同时支持自定义日期样式和 {占位符} 替换。私有属性 m_date_format 存日期模板(如 "yyyy/MM/dd hh:mm:ss"),m_log_format 存日志模板(如 "{timestamp} - {msg}"),外部通过两个 getter 读取。 构造函数带两个参数,实例化时直接传格式,并调用 CheckLogFormat 做校验。校验会拦掉四类无效模板:未闭合的 "{{msg}"、多右括号的 "{msg}}"、空占位符 "{}"、以及整串为空的格式;任一命中就返回 false 并打印错误,结构正确才返回 true。 FormatDate 和 FormatLog 都声明为 virtual,方便派生类重写。日期占位符覆盖很细:yy/yyyy 管年,M~MMMM 管月("1" 到 "January"),d/dd 管日,E/EEEE 管星期,H/h 配 HH/hh 管 24/12 小时制,m/mm、s/ss 管分秒,TT 出 AM/PM。用 "yyyy-MM-dd HH:mm:ss" 跑出来就是 "2025-01-02 14:30:00" 这种形态。 FormatLog 内部直接调 MQL5 原生 StringReplace(),把 {timestamp}、{message} 等按 MqlLogifyModel 实例里的真值逐条替换,模型就变成可读文本。下面这段是类的头文件框架,注意它 include 了上层 LogifyModel.mqh,版权和 link 注释原样保留即可。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 LogifyFormatter.mqh |
class=class="str">"cmt">//|                                                                 joaopedrodev |
class=class="str">"cmt">//|                                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "joaopedrodev"
class="macro">#class="kw">property link      "[MQL5官方文档]
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include files                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "../LogifyModel.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class : CLogifyFormatter                                           |
class=class="str">"cmt">//|                                                                   |
class=class="str">"cmt">//| [PROPERTY]                                                        |

◍ 日志格式化类的骨架与取值接口

把日志输出做成可配置字符串,核心思路是先有一个专管格式的类,把日期样式与日志模板分开存,再暴露只读接口让外部取用。下面这段 MQL5 类定义就是最小可用骨架:两个私有字符串成员分别存日期格式和整行日志格式,公有方法只做返回,不参与拼接。

MQL5 / C++
class=class="str">"cmt">// 类声明:负责把日志格式化成字符串,用占位符替换实际值
class CLogifyFormatter
  {
class="kw">private:
  class="type">class="kw">string      m_date_format;  class=class="str">"cmt">// 日期格式串
  class="type">class="kw">string      m_log_format;   class=class="str">"cmt">// 日志格式串
class="kw">public:
  class="type">class="kw">string      GetDateFormat(class="type">void);  class=class="str">"cmt">// 取日期格式
  class="type">class="kw">string      GetLogFormat(class="type">void);   class=class="str">"cmt">// 取日志格式
  };
class=class="str">"cmt">// 返回私有成员 m_date_format 的当前值
class="type">class="kw">string CLogifyFormatter::GetDateFormat(class="type">void)
  {
   class="kw">return(m_date_format);
  }
class=class="str">"cmt">// 返回私有成员 m_log_format 的当前值
class="type">class="kw">string CLogifyFormatter::GetLogFormat(class="type">void)
  {
   class="kw">return(m_log_format);
  }
构造函数与析构函数在原文里都是空实现,说明这个类初期只托管状态、不做事;真要落地,得在构造时给 m_date_format 赋例如 "%Y-%m-%d %H:%M:%S" 之类的模板。 在 MT5 里新建 EA 把这段代码贴进 include 文件,编译后通过 GetLogFormat() 读到的会是空串,这正好提醒你:未初始化的格式成员在第一次 Print 前必须显式赋值,否则日志行会丢时间戳。

MQL5 / C++
class=class="str">"cmt">//| Name      : CLogifyFormatter                                          |
class=class="str">"cmt">//| Heritage  : No heritage                                                  |
class=class="str">"cmt">//| Description : Class responsible for formatting the log into a            |
class=class="str">"cmt">//| class="type">class="kw">string, replacing placeholders with their respective values.             |
class=class="str">"cmt">//|                                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CLogifyFormatter
  {
class="kw">public:
                     CLogifyFormatter(class="type">void);
                    ~CLogifyFormatter(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLogifyFormatter::CLogifyFormatter(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLogifyFormatter::~CLogifyFormatter(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class CLogifyFormatter
  {
class="kw">private:

  class=class="str">"cmt">//--- Stores the formats
  class="type">class="kw">string            m_date_format;
  class="type">class="kw">string            m_log_format;
class="kw">public:
  class=class="str">"cmt">//--- Format query methods
  class="type">class="kw">string            GetDateFormat(class="type">void);
  class="type">class="kw">string            GetLogFormat(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get date format                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CLogifyFormatter::GetDateFormat(class="type">void)
  {
   class="kw">return(m_date_format);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get the log format                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CLogifyFormatter::GetLogFormat(class="type">void)
  {
   class="kw">return(m_log_format);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

原生 Print 不带时间戳和等级,多条混在一起难分辨。给每条日志加时间和 INFO/WARN/ERROR 标记,复盘时能快速定位异常发生节点。
建一个格式化器类,预留等级字段和取值接口,在写日志时传入自定义等级字符串即可,不必改底层输出逻辑。
可以。小布能接管重复的劳动,把品种页面的原始记录按时间戳和等级自动排版,你只管看异常信号。
用 {time}{level}{msg} 这类占位符定义模板,调用时填值即可。比拼接更不易写错,改格式只动模板不碰业务代码。
至少留取时间、取等级、取消息体三个取值接口,再加一个接收模板并返回字符串的格式化方法,后续接新字段只加接口。