《精通日志记录(第二部分):格式化日志》(基础篇)
「给日志加时间戳和等级标记」
MT5 自带的 Print() 只把字符串原样丢进日志,排查多品种 EA 时很难定位是哪一行、什么时间出的信息。要在日志里带上时间戳和级别,得自己拼字符串。 下面这段在每次写日志前取当前时间并格式化成「时:分:秒」,再叠一个自定义等级前缀。实盘里把级别分清楚,回看 Experts 标签能省掉大量翻找时间。 外汇与贵金属杠杆高,日志只是辅助,任何信号都只是概率倾向,不代表方向必现。
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 类日志时,照第二行那种写法挂上时间和模块名,事后回查会省很多事。
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 上日志量很大,这种精确占位符结构能让你快速筛出某模块在某行的报错,高风险环境下排障效率直接影响仓位安全。
{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 注释原样保留即可。
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 类定义就是最小可用骨架:两个私有字符串成员分别存日期格式和整行日志格式,公有方法只做返回,不参与拼接。
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); }
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">//+------------------------------------------------------------------+