《精通日志记录(第二部分):格式化日志》·进阶篇
「日志格式器的构造与括号校验」
在自建 MT5 日志工具时,CLogifyFormatter 的构造函数只做两件事:把外部传入的日期格式存进 m_date_format,再用 CheckLogFormat 验证日志模板合法性,通过后写入 m_log_format。析构函数目前是空壳,说明这个类没有需要手动释放的资源,直接交给 MT5 运行时回收即可。 校验逻辑的核心是对花括号配对的扫描。openBraces 计数、openIndex 记录最近一次 '{' 位置,遍历时用 StringGetCharacter 逐字符读取;遇到 '{' 计数器加一,遇到 '}' 且计数器为零就立即报错返回 false。 空字符串会被第一行 len==0 拦截并打出葡萄牙语报错。实际跑下来,如果日志模板写成 "[time]{msg" 缺右括号,循环结束也不会补判——这段代码只在遇到多余 '}' 时拦错,左括号遗漏要等后续格式化阶段才可能暴露,开 MT5 挂个 EA 测一下便能确认。
class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CLogifyFormatter::CLogifyFormatter(class="type">class="kw">string date_formate,class="type">class="kw">string log_format) { m_date_format = date_formate; if(CheckLogFormat(log_format)) { m_log_format = log_format; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CLogifyFormatter::~CLogifyFormatter(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Validate format | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CLogifyFormatter::CheckLogFormat(class="type">class="kw">string log_format) { class=class="str">"cmt">//--- Variables to track the most recent &class="macro">#x27;{&class="macro">#x27; opening index and the number of &class="macro">#x27;{&class="macro">#x27; brace openings class="type">int openIndex = -class="num">1; class=class="str">"cmt">// Index of last &class="macro">#x27;{&class="macro">#x27; found class="type">int openBraces = class="num">0; class=class="str">"cmt">// &class="macro">#x27;{&class="macro">#x27; counter class="type">int len = StringLen(log_format); class=class="str">"cmt">// String length class=class="str">"cmt">//--- Checks if class="type">class="kw">string is empty if(len == class="num">0) { class=class="str">"cmt">//--- Prints error message and returns false Print("Erro de formatação: sequência inesperada encontrada. Verifique o padrão de placeholders usado."); class="kw">return(false); } class=class="str">"cmt">//--- Iterate through each character of the class="type">class="kw">string for(class="type">int i=class="num">0;i<len;i++) { class=class="str">"cmt">//--- Gets the current character class="type">class="kw">ushort character = StringGetCharacter(log_format,i); class=class="str">"cmt">//--- Checks if the character is an opening &class="macro">#x27;{&class="macro">#x27; if(character == &class="macro">#x27;{&class="macro">#x27;) { openBraces++; class=class="str">"cmt">// Increments the opening counter &class="macro">#x27;{&class="macro">#x27; openIndex = i; class=class="str">"cmt">// Updates the index of the last opening } class=class="str">"cmt">//--- Checks if the character is a closing &class="macro">#x27;}&class="macro">#x27; else if(character == &class="macro">#x27;}&class="macro">#x27;) { class=class="str">"cmt">//--- If there is no matching &class="macro">#x27;{&class="macro">#x27; if(openBraces == class="num">0) { class=class="str">"cmt">//--- Prints error message and returns false Print("Erro de formatação: o caractere &class="macro">#x27;}&class="macro">#x27; na posição ",i," não possui um &class="macro">#x27;{&class="macro">#x27; correspondente."); class="kw">return(false);
占位符配平与格式化类的收口
这段逻辑处在日志格式校验的尾段:遍历完字符串后若 openBraces 仍大于 0,说明有 '{' 没配上 '}',直接 Print 出错位置并返回 false。 openBraces-- 在匹配到 '}' 时递减,同时用 StringSubstr 从 openIndex+1 截到 i-openIndex-1 取出占位符名;若 StringLen(placeHolder)==0 则报“空占位符”并 return(false)。这两道检查能保证 {…} 结构在交给格式化类前是闭合且非空的。 下面定义的 CLogifyFormatter 类不带继承,只声明两个虚方法:FormatDate(datetime) 与 FormatLog(MqlLogifyModel&),职责是把日志模型按占位符替换成字符串。你在 MT5 里接这段时,先单独跑一遍 openBraces 校验,再挂 Formatter,能少踩一半日志乱码坑。
} class=class="str">"cmt">//--- Decreases the open count because a matching &class="macro">#x27;{&class="macro">#x27; was found openBraces--; class=class="str">"cmt">//--- Extracts the contents of the placeholder(between &class="macro">#x27;{&class="macro">#x27; and &class="macro">#x27;}&class="macro">#x27;) class="type">class="kw">string placeHolder = StringSubstr(log_format, openIndex + class="num">1, i - openIndex - class="num">1); class=class="str">"cmt">//--- Checks if placeholder is empty if(StringLen(placeHolder) == class="num">0) { class=class="str">"cmt">//--- Prints error message and returns false Print("Erro de formatação: placeholder vazio detectado na posição ",i,". Um nome é esperado dentro de &class="macro">#x27;{...}&class="macro">#x27;."); class="kw">return(false); } } } class=class="str">"cmt">//--- After traversing the entire class="type">class="kw">string, check if there are still any unmatched &class="macro">#x27;{&class="macro">#x27;}&class="macro">#x27; if(openBraces > class="num">0) { class=class="str">"cmt">//--- Prints error message indicating the index of the last opened &class="macro">#x27;{&class="macro">#x27; and returns false Print("Erro de formatação: o placeholder &class="macro">#x27;{&class="macro">#x27; na posição ",openIndex," não possui um &class="macro">#x27;}&class="macro">#x27; correspondente."); class="kw">return(false); } class=class="str">"cmt">//--- Format is correct class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class : CLogifyFormatter | class=class="str">"cmt">//| | class=class="str">"cmt">//| [PROPERTY] | 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=class="str">"cmt">//--- Date and log formatting methods class="kw">virtual class="type">class="kw">string FormatDate(class="type">class="kw">datetime date); class="kw">virtual class="type">class="kw">string FormatLog(MqlLogifyModel &data); };
◍ 自定义日志里的时间戳怎么拼
在 MT5 写自己的日志模块时,最烦的就是时间格式不统一。下面这段 CLogifyFormatter::FormatDate 给出了一种思路:先吃进一个 datetime,再用 TimeToStruct 拆成 MqlDateTime 结构,随后拿占位符去替换。 代码里准备了 12 个月和 7 个星期名的缩写与全称数组,但注意:只有当格式串里没有 MMM / MMMM 时,才走 MM / M 的数字替换分支,否则就留给后面的月份名逻辑处理。 年份支持 yyyy(如 2024)和 yy(取模 100 补零成 2 位,如 24);日支持 dd / d,年内第几天支持 DDD / D。小时只写了 24 小时制的 HH / H,没碰 12 小时制和 AM/PM——若你要接美股或贵金属夜盘时段显示,得自己补那段。 外汇和贵金属行情的高频日志里,时间戳错一位就可能把两根 K 线归因搞反,建议直接把这段拷进你的 EA 测试,打印 TimeCurrent() 对照终端右下角时钟验证。
class="type">class="kw">string CLogifyFormatter::FormatDate(class="type">class="kw">datetime date) { class="type">class="kw">string formated = m_date_format; class=class="str">"cmt">//--- Date and time structure class="type">MqlDateTime time; TimeToStruct(date, time); class=class="str">"cmt">//--- Array with months and days of the week in class="type">class="kw">string class="type">class="kw">string months_abbr[class="num">12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; class="type">class="kw">string months_full[class="num">12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; class="type">class="kw">string day_of_week_abbr[class="num">7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; class="type">class="kw">string day_of_week_full[class="num">7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; class=class="str">"cmt">//--- Replace year StringReplace(formated, "yyyy", IntegerToString(time.year)); StringReplace(formated, "yy", IntegerToString(time.year % class="num">100, class="num">2, &class="macro">#x27;class="num">0&class="macro">#x27;)); class=class="str">"cmt">//--- Replace month if(StringFind(formated,"MMM") < class="num">0 && StringFind(formated,"MMMM") < class="num">0) { StringReplace(formated, "MM", IntegerToString(time.mon, class="num">2, &class="macro">#x27;class="num">0&class="macro">#x27;)); StringReplace(formated, "M", IntegerToString(time.mon)); } class=class="str">"cmt">//--- Replace day StringReplace(formated, "dd", IntegerToString(time.day, class="num">2, &class="macro">#x27;class="num">0&class="macro">#x27;)); StringReplace(formated, "d", IntegerToString(time.day)); class=class="str">"cmt">//--- Replace day of year StringReplace(formated, "DDD", IntegerToString(time.day_of_year, class="num">3, &class="macro">#x27;class="num">0&class="macro">#x27;)); StringReplace(formated, "D", IntegerToString(time.day_of_year)); class=class="str">"cmt">//--- Replace Replace hours(24h and 12h) StringReplace(formated, "HH", IntegerToString(time.hour, class="num">2, &class="macro">#x27;class="num">0&class="macro">#x27;)); StringReplace(formated, "H", IntegerToString(time.hour));
「把 12 小时制与时间分量塞进日志模板」
下面这段 MQL5 代码承接前面的时间格式化函数,核心是把 24 小时制转成 12 小时制并补齐模板占位符。hour_12 取模 12 后若为 0 则强制置 12,保证午夜和正午显示成 12 而不是 0,这是欧美盘交易者看日志时常有的习惯。 分钟和秒先用两位补零版本替换 "mm"/"ss",再用无补零版本替换 "m"/"s",顺序不能反,否则 "mm" 会被 "m" 先截掉。AM/PM 由 time.hour < 12 判定,分别替换 "tt"/"TT" 的大小写。 月份和星期几直接调预设数组,索引用 time.mon - 1 和 time.day_of_week,注意 MQL5 里 mon 从 1 开始而数组从 0 开始。最后 FormatLog 方法把日志结构体里的 timestamp、level、origin、message、metadata 依次替换进 m_log_format 模板,返回拼好的字符串。 在 MT5 里把这段接进自己的日志类,改 m_log_format 成 "[{timestamp}] {level} {origin}: {message}" 就能直接跑,验证占位符是否按预期展开。外汇与贵金属波动剧烈,日志仅作复盘辅助,不预示任何方向。
class="type">int hour_12 = time.hour % class="num">12; if (hour_12 == class="num">0) hour_12 = class="num">12; StringReplace(formated, "hh", IntegerToString(hour_12, class="num">2, &class="macro">#x27;class="num">0&class="macro">#x27;)); StringReplace(formated, "h", IntegerToString(hour_12)); class=class="str">"cmt">//--- Replace minutes and seconds StringReplace(formated, "mm", IntegerToString(time.min, class="num">2, &class="macro">#x27;class="num">0&class="macro">#x27;)); StringReplace(formated, "m", IntegerToString(time.min)); StringReplace(formated, "ss", IntegerToString(time.sec, class="num">2, &class="macro">#x27;class="num">0&class="macro">#x27;)); StringReplace(formated, "s", IntegerToString(time.sec)); class=class="str">"cmt">//--- Replace AM/PM class="type">bool is_am = (time.hour < class="num">12); StringReplace(formated, "tt", is_am? "am" : "pm"); StringReplace(formated, "TT", is_am? "AM" : "PM"); class=class="str">"cmt">//--- Replace month StringReplace(formated, "MMMM", months_full[time.mon - class="num">1]); StringReplace(formated, "MMM", months_abbr[time.mon - class="num">1]); class=class="str">"cmt">//--- Replace day of week StringReplace(formated, "EEEE", day_of_week_full[time.day_of_week]); StringReplace(formated, "E", day_of_week_abbr[time.day_of_week]); class="kw">return(formated); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Format logs | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CLogifyFormatter::FormatLog(MqlLogifyModel &data) { class="type">class="kw">string formated = m_log_format; class=class="str">"cmt">//--- Replace placeholders StringReplace(formated,"{timestamp}",IntegerToString(data.timestamp)); StringReplace(formated,"{level}",IntegerToString(data.level)); StringReplace(formated,"{origin}",data.origin); StringReplace(formated,"{message}",data.message); StringReplace(formated,"{metadata}",data.metadata); class="kw">return(formated); } class=class="str">"cmt">//+------------------------------------------------------------------+
给日志模型补上溯源与格式化字段
MqlLogifyModel 是 MT5 日志库里承载单条日志事件的结构体。初版只留了 timestamp、level、origin、message、metadata 五个成员,写简单脚本够用,但要做详细调试或接外部监控就露怯了。 先把两个老字段改名:message 缩成 msg,少敲几个字符;metadata 改成 args,因为后者更贴「日志产生时附带的上下文数据」这个语义,不至于让人误以为是纯 JSON。 新增的字段才是重点。formated 存占位符被真实值替换后的终版文本,只读;levelname 给 "DEBUG"、"INFO" 这类文字级别,格式化串里直接引用;date_time 用 datetime 类型补上 timestamp 的易读版本;filename、function、line 三者合力定位到源码确切行,调试时不用全局搜字符串。 改完之后结构体从 5 个成员扩到 11 个,覆盖时间、级别、内容、上下文、源码位置全维度。下面这段是重构后的结构定义,开 MT5 新建 inc 文件粘进去就能编译验证。
<span class="keyword">class="kw">struct</span> MqlLogifyModel { <span class="keyword">class="type">class="kw">string</span> formated; <span class="comment">class=class="str">"cmt">// The log message formatted according to the specified format.</span> <span class="keyword">class="type">class="kw">string</span> levelname; <span class="comment">class=class="str">"cmt">// Textual name of the log level(e.g., "DEBUG", "INFO")</span> <span class="keyword">class="type">class="kw">string</span> msg; <span class="comment">class=class="str">"cmt">// Main content of the log message</span> <span class="keyword">class="type">class="kw">string</span> args; <span class="comment">class=class="str">"cmt">// Additional arguments associated with the log message</span> <span class="keyword">class="type">ulong</span> timestamp; <span class="comment">class=class="str">"cmt">// Timestamp of the log event, represented in seconds since the start of the Unix epoch</span> <span class="keyword">class="type">class="kw">datetime</span> date_time; <span class="comment">class=class="str">"cmt">// Date and time of the log event, in class="type">class="kw">datetime format</span> ENUM_LOG_LEVEL level; <span class="comment">class=class="str">"cmt">// Enumeration representing the severity level of the log</span> <span class="keyword">class="type">class="kw">string</span> origin; <span class="comment">class=class="str">"cmt">// Source or context of the log message(e.g., class or module)</span> <span class="keyword">class="type">class="kw">string</span> filename; <span class="comment">class=class="str">"cmt">// Name of the source file where the log message was generated</span> <span class="keyword">class="type">class="kw">string</span> function; <span class="comment">class=class="str">"cmt">// Name of the function where the log was called</span> <span class="keyword">class="type">ulong</span> line; <span class="comment">class=class="str">"cmt">// Line number in the source file where the log was generated</span> MqlLogifyModel::MqlLogifyModel(<span class="keyword">class="type">void</span>) { formated = <span class="class="type">class="kw">string">""</span>; levelname = <span class="class="type">class="kw">string">""</span>; msg = <span class="class="type">class="kw">string">""</span>; args = <span class="class="type">class="kw">string">""</span>;
◍ 日志结构体的初始化与格式化落点
MqlLogifyModel 的默认构造函数把所有字段清零:timestamp 和 date_time 置 0,level 给 LOG_LEVEL_DEBUG,origin、filename、function 等字符串字段留空,line 为 0。带参构造函数则一次性把外部传入的 11 个字段写入实例,调用方可以在日志产生时直接塞入函数名、行号和原始信息。 CLogifyFormatter::FormatLog 做的是模板替换:m_log_format 里预留了 {levelname}、{msg}、{args}、{timestamp}、{date_time}、{level}、{origin}、{filename}、{function}、{line} 共 10 个占位符,函数用 StringReplace 逐个换成 data 对应字段,其中 timestamp、level、line 经 IntegerToString 转字符串,date_time 走 this.FormatDate 做日期格式化。 在 MT5 里验证很简单:新建 EA 引用这套结构,把 m_log_format 设成 "[{levelname}][{function}:{line}] {msg}",跑一轮就能看到带函数和行号的输出。外汇与贵金属行情跳动快,这类细粒度日志有助于定位重算逻辑里的异常分支,但交易本身仍属高风险,日志再全也不能替代仓位控制。
timestamp = class="num">0; date_time = class="num">0; level = LOG_LEVEL_DEBUG; origin = ""; filename = ""; function = ""; line = class="num">0; } MqlLogifyModel::MqlLogifyModel(class="type">class="kw">string _formated,class="type">class="kw">string _levelname,class="type">class="kw">string _msg,class="type">class="kw">string _args,class="type">ulong _timestamp,class="type">class="kw">datetime _date_time,ENUM_LOG_LEVEL _level,class="type">class="kw">string _origin,class="type">class="kw">string _filename,class="type">class="kw">string _function,class="type">ulong _line) { formated = _formated; levelname = _levelname; msg = _msg; args = _args; timestamp = _timestamp; date_time = _date_time; level = _level; origin = _origin; filename = _filename; function = _function; line = _line; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Format logs | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CLogifyFormatter::FormatLog(MqlLogifyModel &data) { class="type">class="kw">string formated = m_log_format; StringReplace(formated,"{levelname}",data.levelname); StringReplace(formated,"{msg}",data.msg); StringReplace(formated,"{args}",data.args); StringReplace(formated,"{timestamp}",IntegerToString(data.timestamp)); StringReplace(formated,"{date_time}",this.FormatDate(data.date_time)); StringReplace(formated,"{level}",IntegerToString(data.level)); StringReplace(formated,"{origin}",data.origin); StringReplace(formated,"{filename}",data.filename); StringReplace(formated,"{function}",data.function); StringReplace(formated,"{line}",IntegerToString(data.line)); class="kw">return(formated); } class=class="str">"cmt">//+------------------------------------------------------------------+