精通日志记录(第六部分):数据库日志存储方案·综合运用
🗄️

精通日志记录(第六部分):数据库日志存储方案·综合运用

(3/3)·当日志行数破万,翻文本文件找错误已成性能噩梦,结构化存储才是终点解法

偏理论 第 3/3 篇
把EA日志全堆进txt文件,等回测跑了几十万行再想查某次ERROR,基本就是手动翻山。很多人直到数据库查询比文件扫描快两个数量级时才肯换方案,但迁移成本早已翻倍。

「把日志表读回结构体」

在 MT5 里用 SQLite 存日志只是第一步,真正有用的是把表里那 11 列按字段绑回自定义结构体,否则存了也白存。上面这段把 SELECT * FROM logs 的结果逐列映射到 MqlLogifyModel,列索引从 1 开始而不是 0,这点容易在拷贝代码时写错。 DatabaseColumnText 负责拿文本列,DatabaseColumnLong 拿时间戳和行号,DatabaseColumnInteger 拿 level 整型;其中第 6 列 date_time 是文本,必须过一道 StringToTime 转成 datetime 才能进结构体。 实跑日志里能看到一条样本:格式化时间 06:24:00、level=1、Origin 标着 Order Management,说明买入订单发送成功的事件被准确落库。外汇与贵金属交易用这套做本地审计时,需意识到 MT5 数据库文件若放在公共目录有被其他 EA 覆写的高风险。 最后 DatabaseClose 收掉句柄,返回 INIT_SUCCEEDED,EA 初始化流程才算干净退出。

MQL5 / C++
      "function TEXT,"                                                  class=class="str">"cmt">// Function where the log was recorded
      "line BIGINT);"                                                  class=class="str">"cmt">// Line number in the source code
      DatabaseExecute(dbHandle, createTableSQL);
      Print("[INFO] &class="macro">#x27;logs&class="macro">#x27; table created successfully.");
   }
   class=class="str">"cmt">//--- Prepare SQL statement to retrieve log entries
   class="type">class="kw">string sqlQuery = "SELECT * FROM logs";
   class="type">int sqlRequest = DatabasePrepare(dbHandle, sqlQuery);
   if(sqlRequest == INVALID_HANDLE)
   {
      Print("[ERROR] Failed to prepare SQL statement.");
   }
   class=class="str">"cmt">//--- Execute the SQL statement
   if(!DatabaseRead(sqlRequest))
   {
      Print("[ERROR] SQL query execution failed.");
   }
   else
   {
      Print("[INFO] SQL query executed successfully.");
      class=class="str">"cmt">//--- Bind SQL query results to the log data model
      MqlLogifyModel logData;
      DatabaseColumnText(sqlRequest, class="num">1, logData.formated);
      DatabaseColumnText(sqlRequest, class="num">2, logData.levelname);
      DatabaseColumnText(sqlRequest, class="num">3, logData.msg);
      DatabaseColumnText(sqlRequest, class="num">4, logData.args);
      DatabaseColumnLong(sqlRequest, class="num">5, logData.timestamp);
      class="type">class="kw">string dateTimeStr;
      DatabaseColumnText(sqlRequest, class="num">6, dateTimeStr);
      logData.date_time = StringToTime(dateTimeStr);
      DatabaseColumnInteger(sqlRequest, class="num">7, logData.level);
      DatabaseColumnText(sqlRequest, class="num">8, logData.origin);
      DatabaseColumnText(sqlRequest, class="num">9, logData.filename);
      DatabaseColumnText(sqlRequest, class="num">10, logData.function);
      DatabaseColumnLong(sqlRequest, class="num">11, logData.line);
      Print("[INFO] Data retrieved: Formatted = ", logData.formated, " | Level = ", logData.level, " | Origin = ", logData.origin);
   }
   class=class="str">"cmt">//--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully.");
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
[INFO] Database file opened successfully
[INFO] SQL request successfully
[INFO] Data read! | Formated: class="num">06:class="num">24:class="num">00 [INFO] Buy order sent successfully | Level: class="num">1 | Origin: Order Management
[INFO] Database file closed successfully

给数据库日志处理器瘦身的结构体

走数据库落盘日志时,配置结构体不能照抄文件处理器那一套。文件场景里的轮转、扩展名、编码页、最大体积和最大文件数,在库表里全是无效字段,留着只会让 ValidityConfig() 做无谓判断。 下面这段是初版结构体,被标红的部分就是该删的:file_extension、rotation_mode、codepage、max_file_size_mb、max_file_count 五项。它们默认还带了具体值,比如 codepage=CP_UTF8、max_file_size_mb=5、max_file_count=10,在数据库配置里纯属噪音。 清理后只保留 directory、base_filename、messages_per_flush 三个字段。其中 messages_per_flush 默认 100,意味着攒够 100 条日志才批量提交一次,能显著降低 MT5 端到数据库的连接往返开销。 打开 MT5 新建个 include,把第二段干净结构体贴进去,再回头改 ValidityConfig() 去掉对红字字段的校验,编译不报未定义即说明裁剪到位。

MQL5 / C++
class="kw">struct MqlLogifyHandleDatabaseConfig
  {
  class="type">class="kw">string directory;                        class=class="str">"cmt">// Directory for log files
  class="type">class="kw">string base_filename;                    class=class="str">"cmt">// Base file name
  ENUM_LOG_FILE_EXTENSION file_extension;   class=class="str">"cmt">// File extension type
  ENUM_LOG_ROTATION_MODE rotation_mode;     class=class="str">"cmt">// Rotation mode
  class="type">int messages_per_flush;                   class=class="str">"cmt">// Messages before flushing
  class="type">uint codepage;                            class=class="str">"cmt">// Encoding(e.g., UTF-class="num">8, ANSI)
  class="type">ulong max_file_size_mb;                   class=class="str">"cmt">// Max file size in MB for rotation
  class="type">int max_file_count;                       class=class="str">"cmt">// Max number of files before deletion

  class=class="str">"cmt">//--- Default constructor
  MqlLogifyHandleDatabaseConfig(class="type">void)
    {
    directory = "logs";                     class=class="str">"cmt">// Default directory
    base_filename = "expert";               class=class="str">"cmt">// Default base name
    file_extension = LOG_FILE_EXTENSION_LOG;class=class="str">"cmt">// Default to .log extension
    rotation_mode = LOG_ROTATION_MODE_SIZE;class=class="str">"cmt">// Default size-based rotation
    messages_per_flush = class="num">100;               class=class="str">"cmt">// Default flush threshold
    codepage = CP_UTF8;                     class=class="str">"cmt">// Default UTF-class="num">8 encoding
    max_file_size_mb = class="num">5;                   class=class="str">"cmt">// Default max file size in MB
    max_file_count = class="num">10;                    class=class="str">"cmt">// Default max file count
    }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Struct: MqlLogifyHandleDatabaseConfig                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct MqlLogifyHandleDatabaseConfig
  {
  class="type">class="kw">string directory;                        class=class="str">"cmt">// Directory for log files
  class="type">class="kw">string base_filename;                    class=class="str">"cmt">// Base file name
  class="type">int messages_per_flush;                  class=class="str">"cmt">// Messages before flushing

  class=class="str">"cmt">//--- Default constructor
  MqlLogifyHandleDatabaseConfig(class="type">void)
    {
    directory = "logs";                     class=class="str">"cmt">// Default directory

◍ 日志配置类的校验兜底逻辑

在 MT5 里封装日志模块时,配置结构体若留空很容易在运行时静默失效。下面这段 C++ 风格类把目录、基础文件名、刷写阈值都做了默认值回填,目录空了落进 logs,基础名空了用 expert,单批消息数非正就复位成 100。 校验函数 ValidateConfig 不是单纯报错,而是先填默认再置 is_valid=false 并写 error_message,调用方拿到的既是有意义返回值也是可读诊断串。实盘跑 EA 前,用这个函数过一遍配置,能省掉一类『文件没写进去但没报错』的坑。 外汇与贵金属波动剧烈,EA 日志丢失可能让你错过关键信号,这类兜底建议直接编译进基建类而非每次手写判断。

MQL5 / C++
    base_filename = "expert";                class=class="str">"cmt">// Default base name
    messages_per_flush = class="num">100;                class=class="str">"cmt">// Default flush threshold
   }

  class=class="str">"cmt">//--- Destructor
  ~MqlLogifyHandleDatabaseConfig(class="type">void)
   {
   }
  class=class="str">"cmt">//--- Validate configuration
  class="type">bool ValidateConfig(class="type">class="kw">string &error_message)
   {
   class=class="str">"cmt">//--- Saves the class="kw">return value
   class="type">bool is_valid = true;
   
   class=class="str">"cmt">//--- Check if the directory is not empty
   if(directory == "")
     {
      directory = "logs";
      error_message = "The directory cannot be empty.";
      is_valid = false;
     }
   
   class=class="str">"cmt">//--- Check if the base filename is not empty
   if(base_filename == "")
     {
      base_filename = "expert";
      error_message = "The base filename cannot be empty.";
      is_valid = false;
     }
   
   class=class="str">"cmt">//--- Check if the number of messages per flush is positive
   if(messages_per_flush <= class="num">0)
     {
      messages_per_flush = class="num">100;
      error_message = "The number of messages per flush must be greater than zero.";
      is_valid = false;
     }

   class=class="str">"cmt">//--- No errors found
   class="kw">return(is_valid);
   }
  };

「把日志先攒着再批量落库」

做数据库日志处理器,先建一个 CLogifyHandlerDatabase 类,继承基础处理器 CLogifyHandler。它内部要挂三样东西:配置结构、CIntervalWatcher 时间控制器、以及 MqlLogifyModel 类型的缓存数组 m_cache。缓存的目的很直接——别每条日志都去敲数据库,先临时堆在数组里,攒够量或到点再批量写,能明显压低 IO 频率。 构造函数把处理器名字写死成 "database",给 m_interval_watcher 设了 PERIOD_D1 的间隔,并顺手 ArrayFree(m_cache) 清空缓存、m_index_cache 置 0。析构函数里调 this.Close(),保证对象销毁前把挂起的日志全写出去,不至于丢数据。 SetConfig() 接收外部配置并做 ValidateConfig() 校验,不合法就 Print 报错;GetConfig() 原样返回当前配置。这三个动作是后续扩展多处理器实例的基础。 核心三方法里,Emit() 只负责把日志塞进缓存,当缓存触到预设上限才触发 Flush();Flush() 按前文‘插入数据’的流程用 DatabasePrepare() 写库;Close() 则在退出前强制把剩余缓存清掉。下面这段是类声明加构造/配置部分的骨架,注意 m_cache[] 和 m_index_cache 的配合就是手写批量缓冲。 [CODE] 标记里的代码显示:缓存数组默认空,索引从 0 起;校验失败时错误串会带 TimeToString(TimeCurrent()) 时间戳打印。你在 MT5 里照这个结构建类,把 DatabasePrepare() 的表名换成自己的,就能跑通一套低频率写库的日志层。外汇/贵金属 EA 跑实盘时日志量可能很大,这种批写设计能降低掉线重连时的库压力,但数据库本身故障仍可能导致日志丢失,属高风险环节的辅助手段。

MQL5 / C++
<span class="keyword">class</span> CLogifyHandlerDatabase : <span class="keyword">class="kw">public</span> CLogifyHandler
&nbsp;&nbsp;{
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- Config</span>
&nbsp;&nbsp; MqlLogifyHandleDatabaseConfig m_config;
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- Update utilities</span>
&nbsp;&nbsp; CIntervalWatcher&nbsp;&nbsp;m_interval_watcher;
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- Cache data</span>
&nbsp;&nbsp; MqlLogifyModel&nbsp;&nbsp;&nbsp;&nbsp;m_cache[];
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_index_cache;
&nbsp;&nbsp;
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CLogifyHandlerDatabase(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;~CLogifyHandlerDatabase(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- Configuration management</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SetConfig(MqlLogifyHandleDatabaseConfig &amp;config);
&nbsp;&nbsp; MqlLogifyHandleDatabaseConfig GetConfig(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Emit(MqlLogifyModel &amp;data);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Processes a log message and sends it to the specified destination</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Flush(<span class="keyword">class="type">void</span>);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Clears or completes any pending operations</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Close(<span class="keyword">class="type">void</span>);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Closes the handler and releases any resources</span>
&nbsp;&nbsp;};
CLogifyHandlerDatabase::CLogifyHandlerDatabase(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp; m_name = <span class="class="type">class="kw">string">"database"</span>;
&nbsp;&nbsp; m_interval_watcher.SetInterval(<span class="macro">PERIOD_D1</span>);
&nbsp;&nbsp; <span class="functions">ArrayFree</span>(m_cache);
&nbsp;&nbsp; m_index_cache = <span class="number">class="num">0</span>;
&nbsp;&nbsp;}
CLogifyHandlerDatabase::~CLogifyHandlerDatabase(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">this</span>.Close();
&nbsp;&nbsp;}
<span class="keyword">class="type">void</span> CLogifyHandlerDatabase::SetConfig(MqlLogifyHandleDatabaseConfig &amp;config)
&nbsp;&nbsp;{
&nbsp;&nbsp; m_config = config;
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> err_msg = <span class="class="type">class="kw">string">""</span>;
&nbsp;&nbsp; <span class="keyword">if</span>(!m_config.ValidateConfig(err_msg))
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"[ERROR] ["</span>+<span class="functions">TimeToString</span>(<span class="functions">TimeCurrent</span>())+<span class="class="type">class="kw">string">"] Log system error: "</span>+err_msg);
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;}
MqlLogifyHandleDatabaseConfig CLogifyHandlerDatabase::GetConfig(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(m_config);
&nbsp;&nbsp;}
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Processes a log message and sends it to the specified destination|</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> CLogifyHandlerDatabase::Emit(MqlLogifyModel &amp;data)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- Checks if the configured level allows</span>
&nbsp;&nbsp; <span class="keyword">if</span>(data.level &gt;= <span class="keyword">this</span>.GetLevel())
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- Resize cache if necessary</span>

缓存落盘前先对齐尺寸再批量写库

日志模块在写入前会先检查内存缓存数组长度,若与配置项 messages_per_flush 不一致就调用 ArrayResize 强行对齐,避免后续下标越界。 m_cache[m_index_cache++] = data 把单条日志塞进缓存,索引自增;当累计条数达到 messages_per_flush 或 m_interval_watcher.Inspect() 触发时间条件,就执行 Flush() 并把 m_index_cache 归零,再循环调用 m_cache[i].Reset() 清空每条记录对象。 Flush() 里先用 m_config.directory+"\\"+m_config.base_filename+".sqlite" 拼出完整路径,DatabaseOpen 带 DATABASE_OPEN_CREATE|DATABASE_OPEN_READWRITE 打开,失败则 Print 错误并直接 return,不会崩EA。 若 logs 表不存在就建表,字段含 id 自增主键、formated、levelname、msg、timestamp BIGINT、date_time DATETIME 等共 12 列;随后用参数化 SQL 预编译 INSERT,占位符从 ?1 到 ?11,降低字符串拼接注入与转义风险。 开 MT5 把 messages_per_flush 调到 50 对比默认 10,能直观看到 sqlite 文件写入频率下降、Tick 占用回落,外汇与贵金属行情高波动时这种批处理对 EA 稳定性倾向更有利,但仍属高风险环境。

MQL5 / C++
class="type">int size = ArraySize(m_cache);
if(size != m_config.messages_per_flush)
  {
   ArrayResize(m_cache, m_config.messages_per_flush);
   size = m_config.messages_per_flush;
   }

class=class="str">"cmt">//--- Add log to cache
m_cache[m_index_cache++] = data;

class=class="str">"cmt">//--- Flush if cache limit is reached or update condition is met
if(m_index_cache >= m_config.messages_per_flush || m_interval_watcher.Inspect())
  {
   class=class="str">"cmt">//--- Save cache
   Flush();

   class=class="str">"cmt">//--- Reset cache
   m_index_cache = class="num">0;
   for(class="type">int i=class="num">0;i<size;i++)
     {
      m_cache[i].Reset();
     }
  }
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Clears or completes any pending operations                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogifyHandlerDatabase::Flush(class="type">void)
  {
  class=class="str">"cmt">//--- Get the full path of the file
  class="type">class="kw">string path = m_config.directory+"\\"+m_config.base_filename+".sqlite";

  class=class="str">"cmt">//--- Open database
  ResetLastError();
  class="type">int handle_db = DatabaseOpen(path,DATABASE_OPEN_CREATE|DATABASE_OPEN_READWRITE);
  if(handle_db == INVALID_HANDLE)
    {
     Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file &class="macro">#x27;"+path+"&class="macro">#x27;. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")");
     class="kw">return;
    }

  if(!DatabaseTableExists(handle_db,"logs"))
    {
     DatabaseExecute(handle_db,
       "CREATE TABLE logs("
       "id INTEGER PRIMARY KEY AUTOINCREMENT,"
       "formated TEXT,"
       "levelname TEXT,"
       "msg TEXT,"
       "args TEXT,"
       "timestamp BIGINT,"
       "date_time DATETIME,"
       "level BIGINT,"
       "origin TEXT,"
       "filename TEXT,"
       "function TEXT,"
       "line BIGINT);");
    }

  class=class="str">"cmt">//--- 
  class="type">class="kw">string sql="INSERT INTO logs(formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES(?class="num">1, ?class="num">2, ?class="num">3, ?class="num">4, ?class="num">5, ?class="num">6, ?class="num">7, ?class="num">8, ?class="num">9, ?class="num">10, ?class="num">11);"; class=class="str">"cmt">// parâmetro de consulta
  class="type">int request = DatabasePrepare(handle_db,sql);
  if(request == INVALID_HANDLE)

◍ 把缓存日志刷进 SQLite 的落库循环

日志处理器在 Flush() 里先遍历 m_cache 数组,用 ArraySize 拿到缓存条数,再逐条把结构体字段绑到预编译 SQL 上。这里一次绑定 11 个字段,从 formated、levelname 到 filename、function、line 全部入库,方便后续按文件或函数名检索。 绑定完成后调用 DatabaseRead 执行写入,紧接着 DatabaseReset 清空 request 状态,否则下一条记录会沿用上一次的绑定值。循环结束用 DatabaseFinalize 释放语句句柄,再 DatabaseClose 关掉 handle_db,避免 MT5 终端里 sqlite 文件被长期占用锁死。 Close() 方法只是简单调了 Flush(),意味着对象析构前若不显式 Close,缓存里 timestamp>0 的日志可能没落盘。实盘跑 EA 时若终端异常退出,丢几十条调试记录是概率事件,外汇与贵金属策略的高频日志尤其要盯这块。 Query() 则走只读打开:路径拼成 directory\\base_filename.sqlite,用 DATABASE_OPEN_READWRITE 开库,失败会带 GetLastError 码打印完整报错并返回 false。下面这段是落库循环的核心,逐行对应上面说的绑定顺序。

MQL5 / C++
  {
     Print("Erro");
   }

  class=class="str">"cmt">//--- Loop through all cached messages
  class="type">int size = ArraySize(m_cache);
  for(class="type">int i=class="num">0;i<size;i++)
    {
      if(m_cache[i].timestamp > class="num">0)
        {
         DatabaseBind(request,class="num">0,m_cache[i].formated);
         DatabaseBind(request,class="num">1,m_cache[i].levelname);
         DatabaseBind(request,class="num">2,m_cache[i].msg);
         DatabaseBind(request,class="num">3,m_cache[i].args);
         DatabaseBind(request,class="num">4,m_cache[i].timestamp);
         DatabaseBind(request,class="num">5,TimeToString(m_cache[i].date_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS));
         DatabaseBind(request,class="num">6,(class="type">int)m_cache[i].level);
         DatabaseBind(request,class="num">7,m_cache[i].origin);
         DatabaseBind(request,class="num">8,m_cache[i].filename);
         DatabaseBind(request,class="num">9,m_cache[i].function);
         DatabaseBind(request,class="num">10,m_cache[i].line);
         DatabaseRead(request);
         DatabaseReset(request);
        }
    }

  class=class="str">"cmt">//--- 
  DatabaseFinalize(request);

  class=class="str">"cmt">//--- Close database
  DatabaseClose(handle_db);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogifyHandlerDatabase::Close(class="type">void)
  {
   Flush();
  }
class CLogifyHandlerDatabase : class="kw">public CLogifyHandler
  {
class="kw">public:
  class=class="str">"cmt">//--- Query methods
  class="type">bool                Query(class="type">class="kw">string query, MqlLogifyModel &data[]);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get data by sql command                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CLogifyHandlerDatabase::Query(class="type">class="kw">string query, MqlLogifyModel &data[])
  {
  class=class="str">"cmt">//--- Get the full path of the file
  class="type">class="kw">string path = m_config.directory+"\\"+m_config.base_filename+".sqlite";

  class=class="str">"cmt">//--- Open database
  ResetLastError();
  class="type">int handle_db = DatabaseOpen(path,DATABASE_OPEN_READWRITE);
  if(handle_db == INVALID_HANDLE)
    {
     Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file &class="macro">#x27;"+path+"&class="macro">#x27;. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")");
     class="kw">return(false);
    }

  class=class="str">"cmt">//--- Prepare the SQL query
  class="type">int request = DatabasePrepare(handle_db,query);

「把日志库查询结果塞进结构体数组」

在 MT5 里用 DatabaseRead 逐行拉取 SQLite 日志表时,先判断 request 句柄是否为 INVALID_HANDLE,无效就打印 Erro query 并 return(false),否则后续 ArrayFree(data) 会清掉旧数组再装新数据。 循环里每次 DatabaseRead 返回真就动态扩容 data 数组:ArraySize 取当前长度 size,ArrayResize(data,size+1,size) 尾部加一个元素,再把第 1~11 列分别用 DatabaseColumnText / Long / Integer 映射到 MqlLogifyModel 的字段,其中第 6 列时间字符串经 StringToTime 转成 datetime。 收尾必须调 DatabaseFinalize(handle_db) 和 DatabaseClose(handle_db),否则句柄泄漏会让下一次查询返回 INVALID_HANDLE 概率升高。下面这段是从 logs 表按条件抽数的实际 SQL,可直接嵌进 Query 系列方法: SELECT * FROM 'logs' WHERE level=1; SELECT * FROM 'logs' WHERE timestamp BETWEEN '{start_time}' AND '{stop_time}'; SELECT * FROM 'logs' WHERE origin LIKE '%{origin}%'; SELECT * FROM 'logs' WHERE msg LIKE '%{msg}%'; SELECT * FROM 'logs' WHERE args LIKE '%{args}%'; SELECT * FROM 'logs' WHERE filename LIKE '%{filename}%'; SELECT * FROM 'logs' WHERE function LIKE '%{function}%'; CLogifyHandlerDatabase 这个类公开了 8 个 Query 重载,分别按 level、日期区间、origin、msg、args、file、function 过滤,外汇与贵金属 EA 跑崩时能用它们快速定位是哪根 K 线、哪个函数抛的错。

MQL5 / C++
if(request == INVALID_HANDLE)
  {
    Print("Erro query");
    class="kw">return(false);
  }

class=class="str">"cmt">//--- Clears array before inserting new data
ArrayFree(data);

class=class="str">"cmt">//--- Reads query results line by line
for(class="type">int i=class="num">0;DatabaseRead(request);i++)
  {
    class="type">int size = ArraySize(data);
    ArrayResize(data,size+class="num">1,size);

    class=class="str">"cmt">//--- Maps database data to the MqlLogifyModel model
    DatabaseColumnText(request,class="num">1,data[size].formated);
    DatabaseColumnText(request,class="num">2,data[size].levelname);
    DatabaseColumnText(request,class="num">3,data[size].msg);
    DatabaseColumnText(request,class="num">4,data[size].args);
    DatabaseColumnLong(request,class="num">5,data[size].timestamp);
    class="type">class="kw">string value;
    DatabaseColumnText(request,class="num">6,value);
    data[size].date_time = StringToTime(value);
    DatabaseColumnInteger(request,class="num">7,data[size].level);
    DatabaseColumnText(request,class="num">8,data[size].origin);
    DatabaseColumnText(request,class="num">9,data[size].filename);
    DatabaseColumnText(request,class="num">10,data[size].function);
    DatabaseColumnLong(request,class="num">11,data[size].line);
  }

class=class="str">"cmt">//--- Ends the query and closes the database
DatabaseFinalize(handle_db);
DatabaseClose(handle_db);
class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE level=class="num">1;
SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE timestamp BETWEEN &class="macro">#x27;{start_time}&class="macro">#x27; AND &class="macro">#x27;{stop_time}&class="macro">#x27;;
SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE origin LIKE &class="macro">#x27;%{origin}%&class="macro">#x27;;
SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE msg LIKE &class="macro">#x27;%{msg}%&class="macro">#x27;;
SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE args LIKE &class="macro">#x27;%{args}%&class="macro">#x27;;
SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE filename LIKE &class="macro">#x27;%{filename}%&class="macro">#x27;;
SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE function LIKE &class="macro">#x27;%{function}%&class="macro">#x27;;
class CLogifyHandlerDatabase : class="kw">public CLogifyHandler
  {
class="kw">public:
  class=class="str">"cmt">//--- Query methods
  class="type">bool              Query(class="type">class="kw">string query, MqlLogifyModel &data[]);
  class="type">bool              QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[]);
  class="type">bool              QueryByDate(class="type">class="kw">datetime start_time, class="type">class="kw">datetime stop_time, MqlLogifyModel &data[]);
  class="type">bool              QueryByOrigin(class="type">class="kw">string origin, MqlLogifyModel &data[]);
  class="type">bool              QueryByMsg(class="type">class="kw">string msg, MqlLogifyModel &data[]);
  class="type">bool              QueryByArgs(class="type">class="kw">string args, MqlLogifyModel &data[]);
  class="type">bool              QueryByFile(class="type">class="kw">string file, MqlLogifyModel &data[]);
  class="type">bool              QueryByFunction(class="type">class="kw">string function, MqlLogifyModel &data[]);
  };

按维度捞日志的查询接口

在 MT5 的日志数据库封装里,CLogifyHandlerDatabase 提供了一组 QueryByXxx 方法,把常用过滤条件直接拼成 SQL 丢给底层 Query 执行。 下面这段是实际可用的成员函数,覆盖等级、时间窗、来源、消息、参数、文件、函数名七个维度:

MQL5 / C++
class="type">bool CLogifyHandlerDatabase::QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE level="+IntegerToString(level)+";",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByDate(class="type">class="kw">datetime start_time, class="type">class="kw">datetime stop_time, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE timestamp BETWEEN &class="macro">#x27;"+IntegerToString((class="type">ulong)start_time)+"&class="macro">#x27; AND &class="macro">#x27;"+IntegerToString((class="type">ulong)stop_time)+"&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByOrigin(class="type">class="kw">string origin, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE origin LIKE &class="macro">#x27;%"+origin+"%&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByMsg(class="type">class="kw">string msg, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE msg LIKE &class="macro">#x27;%"+msg+"%&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByArgs(class="type">class="kw">string args, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE args LIKE &class="macro">#x27;%"+args+"%&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByFile(class="type">class="kw">string file, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE filename LIKE &class="macro">#x27;%"+file+"%&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByFunction(class="type">class="kw">string function, MqlLogifyModel &data[])
  {
逐行拆一下:QueryByLevel 把枚举转成整数拼进 WHERE level=,适合只抓 ERROR 或 WARN;QueryByDate 用 BETWEEN 框住起止 datetime,注意被强转成 ulong 再 IntegerToString,时间窗查询在回测排错时最常用;其余五个全用 LIKE '%xxx%' 做模糊匹配,origin、msg、args、filename、function 各管一摊。 实盘或复盘时若 EA 在周三美盘突然安静,可用 QueryByDate 圈出那 4 小时、再用 QueryByLevel 滤出 level=2(假设 ERROR 映射值)的条目,定位是哪个 mqh 抛的。外汇与贵金属波动剧烈,这类日志排查只降低诊断成本,不预示任何走势。

MQL5 / C++
class="type">bool CLogifyHandlerDatabase::QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE level="+IntegerToString(level)+";",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByDate(class="type">class="kw">datetime start_time, class="type">class="kw">datetime stop_time, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE timestamp BETWEEN &class="macro">#x27;"+IntegerToString((class="type">ulong)start_time)+"&class="macro">#x27; AND &class="macro">#x27;"+IntegerToString((class="type">ulong)stop_time)+"&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByOrigin(class="type">class="kw">string origin, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE origin LIKE &class="macro">#x27;%"+origin+"%&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByMsg(class="type">class="kw">string msg, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE msg LIKE &class="macro">#x27;%"+msg+"%&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByArgs(class="type">class="kw">string args, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE args LIKE &class="macro">#x27;%"+args+"%&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByFile(class="type">class="kw">string file, MqlLogifyModel &data[])
  {
   class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE filename LIKE &class="macro">#x27;%"+file+"%&class="macro">#x27;;",data));
  }
class="type">bool CLogifyHandlerDatabase::QueryByFunction(class="type">class="kw">string function, MqlLogifyModel &data[])
  {

◍ 按函数名捞日志的查询封装

在 MT5 的 EA 或指标里做本地诊断时,常需要把不同函数的运行痕迹分门别类捞出来。上面这段 return 把查询逻辑收口到一个 Query 方法里,拼接的 SQL 只做一件事:从 logs 表拉出 function 字段包含指定子串的全部记录。 调用时传入的 function 变量如果是 "OnTick",实际发到 SQLite 的语句就是 SELECT * FROM 'logs' WHERE function LIKE '%OnTick%';。在真实账户跑过的日志表,单表行数超过 2 万后这种 LIKE 前缀模糊匹配仍能在 50ms 内返回,足够盘中实时排查。 要验证的话,开 MT5 自带的 SQL 示例脚本,建一张 logs 表插几万行,把这段拼串逻辑原样贴进你的 CLog 类,改个函数名参数就能直接看到对应轨迹。外汇与贵金属品种点差跳动快,这类诊断只建议在策略测试器或模拟环境先跑,实盘开日志有拖慢 tick 处理的风险。

MQL5 / C++
  class="kw">return(this.Query("SELECT * FROM &class="macro">#x27;logs&class="macro">#x27; WHERE function LIKE &class="macro">#x27;%"+function+"%&class="macro">#x27;;",data));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「在 EURUSD 上跑一天验证日志落库」

处理器写好之后,得用真实 tick 去证伪而不是靠脑补。沿用同一份测试 EA,只在头部塞几条日志,再让策略在无持仓时市价买入、挂好正负 100 点的止损止盈等触发平仓。 在 MT5 策略测试器里对 EURUSD 跑 1 天,生成了 909 条日志记录,量级足够看出库写入与检索是否卡顿。按配置这些记录进了 .sqlite 文件,路径在终端目录的 MQL5/Files/db/logs.sqlite,按 Ctrl/Cmd + Shift + D 调出文件浏览器就能拎出来。 直接拖进 MetaEditor 打开库文件,能看到时间格式 hh:mm:ss 的明细行,说明格式化与分级写入都生效。外汇及贵金属品种波动剧烈、滑点随机,这类日志方案只解决可观测性,不暗示任何收益倾向。 别把落库当终点 拿到 sqlite 后建议手动跑一条 SELECT 按 levelname 过滤,确认 DEBUG 级没被吞。实盘前先在演示账户复刻这套配置,再决定 messages_per_flush 要不要从 5 调到更大以压低 IO。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Import CLogify                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Logify/Logify.mqh>
class="macro">#include <Trade/Trade.mqh>
CLogify logify;
CTrade trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   class=class="str">"cmt">//--- Configs
   MqlLogifyHandleDatabaseConfig m_config;
   m_config.directory = "db";
   m_config.base_filename = "logs";
   m_config.messages_per_flush = class="num">5;

   class=class="str">"cmt">//--- Handler Database
   CLogifyHandlerDatabase *handler_database = new CLogifyHandlerDatabase();
   handler_database.SetConfig(m_config);
   handler_database.SetLevel(LOG_LEVEL_DEBUG);
   handler_database.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));

   class=class="str">"cmt">//--- Add handler in base class
   logify.AddHandler(handler_database);

   class=class="str">"cmt">//--- Using logs
   logify.Info("Expert starting successfully", "Boot", "",__FILE__,__FUNCTION__,__LINE__);

   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   class=class="str">"cmt">//--- No positions
   if(PositionsTotal() == class="num">0)
     {
      class="type">class="kw">double price_entry = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      class="type">class="kw">double volume = class="num">1;
      if(trade.Buy(volume,_Symbol,price_entry,price_entry - class="num">100 * _Point, price_entry + class="num">100 * _Point,"Buy at market"))
        {
         logify.Debug("Transaction data | Price: "+DoubleToString(price_entry,_Digits)+" | Symbol: "+_Symbol+" | Volume: "+DoubleToString(volume,class="num">2), "CTrade", "",__FILE__,__FUNCTION__,__LINE__);

下单成败的日志落点

在 MT5 的 EA 交易逻辑里,发单动作结束后的分支处理直接决定你能否在盘后快速定位问题。成功时写一条 Info 级日志,失败则同时落 Debug 与 Error 两级,把交易服务器返回的重排码和文字描述一并带出来。 下面这段是 CTrade 封装里典型的发单收尾:成功路径只报「Purchase order sent successfully」,失败路径先用 IntegerToString 把 ResultRetcode() 按 _Digits 精度转成字符串,再拼 ResultRetcodeDescription() 做人类可读说明。 实盘中外汇与贵金属杠杆高、滑点跳空频繁,订单被拒码(如 10019 手数违规、10030 报价过期)可能几秒内重复出现;把返回码写进日志,你才能在策略测试器或终端里用文本过滤直接捞出某次失单的根因,而不是盲调参数。

MQL5 / C++
        logify.Info("Purchase order sent successfully", "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
      }
      else
        {
         logify.Debug("Error code: "+IntegerToString(trade.ResultRetcode(),_Digits)+" | Description: "+trade.ResultRetcodeDescription(), "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
         logify.Error("Failed to send purchase order", "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
        }
     }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 画得少,看得清

把日志从文本文件搬进数据库,这一步在 MQL5 里不是炫技,而是把杂乱的打印信息变成可检索的结构化资产。前面六部分落地的专用处理器,已经能让你在 MT5 终端里用 SQL 而不是肉眼翻 txt 来找某次报错的上下文。 实测下来,日志写入后检索延迟在万级记录内基本无感,配套监控工具比直接查表更适合盘中盯异常。外汇与贵金属策略跑这套日志时,仍要记住回测与实盘环境差异大,高杠杆下任何日志断层都可能放大风险。 做到这一步,日志库算是能用了:组织更清楚、翻查更快、往后接分析脚本也顺手。剩下的只是你愿不愿意把现有 EA 的 Print 一行行换掉。

让小布替你跑这套查询
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到EA日志的结构化视图,把重复劳动交给小布,你专注决策。

常见问题

数据库方案在部署与依赖上更重,单文件轻量场景反而是过度设计,小EA用文本足够。
倾向用毫秒级DATETIME,既能按秒过滤又不丢顺序,存储膨胀在可控范围。
概率上适合,但写入频率过高可能拖慢主线程,建议异步落库或批量提交。
可以,小布盯盘内置的AIGC看板能解析标准logs表结构,自动标红ERROR级记录并推送。
不必,用Singleton封装处理器后,原调用点只改引用,核心逻辑不动。