精通日志记录(第六部分):数据库日志存储方案·综合运用
(3/3)·当日志行数破万,翻文本文件找错误已成性能噩梦,结构化存储才是终点解法
「把日志表读回结构体」
在 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 初始化流程才算干净退出。
"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() 去掉对红字字段的校验,编译不报未定义即说明裁剪到位。
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 日志丢失可能让你错过关键信号,这类兜底建议直接编译进基建类而非每次手写判断。
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 跑实盘时日志量可能很大,这种批写设计能降低掉线重连时的库压力,但数据库本身故障仍可能导致日志丢失,属高风险环节的辅助手段。
<span class="keyword">class</span> CLogifyHandlerDatabase : <span class="keyword">class="kw">public</span> CLogifyHandler { <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//--- Config</span> MqlLogifyHandleDatabaseConfig m_config; <span class="comment">class=class="str">"cmt">//--- Update utilities</span> CIntervalWatcher m_interval_watcher; <span class="comment">class=class="str">"cmt">//--- Cache data</span> MqlLogifyModel m_cache[]; <span class="keyword">class="type">int</span> m_index_cache; <span class="keyword">class="kw">public</span>: CLogifyHandlerDatabase(<span class="keyword">class="type">void</span>); ~CLogifyHandlerDatabase(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">//--- Configuration management</span> <span class="keyword">class="type">void</span> SetConfig(MqlLogifyHandleDatabaseConfig &config); MqlLogifyHandleDatabaseConfig GetConfig(<span class="keyword">class="type">void</span>); <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> Emit(MqlLogifyModel &data); <span class="comment">class=class="str">"cmt">// Processes a log message and sends it to the specified destination</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> Flush(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">// Clears or completes any pending operations</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> Close(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">// Closes the handler and releases any resources</span> }; CLogifyHandlerDatabase::CLogifyHandlerDatabase(<span class="keyword">class="type">void</span>) { m_name = <span class="class="type">class="kw">string">"database"</span>; m_interval_watcher.SetInterval(<span class="macro">PERIOD_D1</span>); <span class="functions">ArrayFree</span>(m_cache); m_index_cache = <span class="number">class="num">0</span>; } CLogifyHandlerDatabase::~CLogifyHandlerDatabase(<span class="keyword">class="type">void</span>) { <span class="keyword">this</span>.Close(); } <span class="keyword">class="type">void</span> CLogifyHandlerDatabase::SetConfig(MqlLogifyHandleDatabaseConfig &config) { m_config = config; <span class="keyword">class="type">class="kw">string</span> err_msg = <span class="class="type">class="kw">string">""</span>; <span class="keyword">if</span>(!m_config.ValidateConfig(err_msg)) { <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); } } MqlLogifyHandleDatabaseConfig CLogifyHandlerDatabase::GetConfig(<span class="keyword">class="type">void</span>) { <span class="keyword">class="kw">return</span>(m_config); } <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 &data) { <span class="comment">class=class="str">"cmt">//--- Checks if the configured level allows</span> <span class="keyword">if</span>(data.level >= <span class="keyword">this</span>.GetLevel()) { <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 稳定性倾向更有利,但仍属高风险环境。
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。下面这段是落库循环的核心,逐行对应上面说的绑定顺序。
{
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 线、哪个函数抛的错。
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 执行。 下面这段是实际可用的成员函数,覆盖等级、时间窗、来源、消息、参数、文件、函数名七个维度:
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[]) {
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 处理的风险。
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。
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 报价过期)可能几秒内重复出现;把返回码写进日志,你才能在策略测试器或终端里用文本过滤直接捞出某次失单的根因,而不是盲调参数。
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 一行行换掉。