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

《精通日志记录(第二部分):格式化日志》·进阶篇

第 2/3 篇

「日志格式器的构造与括号校验」

在自建 MT5 日志工具时,CLogifyFormatter 的构造函数只做两件事:把外部传入的日期格式存进 m_date_format,再用 CheckLogFormat 验证日志模板合法性,通过后写入 m_log_format。析构函数目前是空壳,说明这个类没有需要手动释放的资源,直接交给 MT5 运行时回收即可。 校验逻辑的核心是对花括号配对的扫描。openBraces 计数、openIndex 记录最近一次 '{' 位置,遍历时用 StringGetCharacter 逐字符读取;遇到 '{' 计数器加一,遇到 '}' 且计数器为零就立即报错返回 false。 空字符串会被第一行 len==0 拦截并打出葡萄牙语报错。实际跑下来,如果日志模板写成 "[time]{msg" 缺右括号,循环结束也不会补判——这段代码只在遇到多余 '}' 时拦错,左括号遗漏要等后续格式化阶段才可能暴露,开 MT5 挂个 EA 测一下便能确认。

MQL5 / C++
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,能少踩一半日志乱码坑。

MQL5 / C++
   }

   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() 对照终端右下角时钟验证。

MQL5 / C++
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}" 就能直接跑,验证占位符是否按预期展开。外汇与贵金属波动剧烈,日志仅作复盘辅助,不预示任何方向。

MQL5 / C++
  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 文件粘进去就能编译验证。

MQL5 / C++
<span class="keyword">class="kw">struct</span> MqlLogifyModel
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> formated;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// The log message formatted according to the specified format.</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> levelname;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Textual name of the log level(e.g., "DEBUG", "INFO")</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> msg;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Main content of the log message</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> args;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Additional arguments associated with the log message</span>
&nbsp;&nbsp; <span class="keyword">class="type">ulong</span> timestamp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Timestamp of the log event, represented in seconds since the start of the Unix epoch</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">datetime</span> date_time;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Date and time of the log event, in class="type">class="kw">datetime format</span>
&nbsp;&nbsp; ENUM_LOG_LEVEL level;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Enumeration representing the severity level of the log</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> origin;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Source or context of the log message(e.g., class or module)</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> filename;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Name of the source file where the log message was generated</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> function;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Name of the function where the log was called</span>
&nbsp;&nbsp; <span class="keyword">class="type">ulong</span> line;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Line number in the source file where the log was generated</span>
&nbsp;&nbsp;
&nbsp;&nbsp; MqlLogifyModel::MqlLogifyModel(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;formated = <span class="class="type">class="kw">string">""</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;levelname = <span class="class="type">class="kw">string">""</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg = <span class="class="type">class="kw">string">""</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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}",跑一轮就能看到带函数和行号的输出。外汇与贵金属行情跳动快,这类细粒度日志有助于定位重算逻辑里的异常分支,但交易本身仍属高风险,日志再全也不能替代仓位控制。

MQL5 / C++
   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">//+------------------------------------------------------------------+

常见问题

在格式化类中用 ToString 拼接各时间分量,小时用 Hour() 取 12 小时值并补前导零,上下午用字符串判断后追加,避免直接丢原始时间对象。
构造时遍历模板字符,遇 '{' 加计数、'}' 减计数,结束不为 0 直接抛错;收口处用独立方法统一检查而非散落各处。
可以,小布能读取你的日志结构体定义与模板字符串,自动跑括号配平校验并标出失衡位置,省去手动核对。
在日志模型里加来源标识与时间戳字段,初始化时从触发函数传入品种与柱号,格式化落点统一输出即可。
把格式化落点收口成结构体的唯一 Format() 方法,所有记录动作只调它,避免分散调用导致字段没拼全。