
日志记录精通指南(第三部分):探索日志处理器(Handlers)实现方案
概述
在该系列首篇文章—— 《日志记录精通指南(第一部分):MQL5基础概念与第一步》中,我们开启了为智能交易系统(EA)开发量身定制日志库的创建之旅。我们探讨了构建这一核心工具的契机:突破MetaTrader 5原生日志的局限,为MQL5生态带来健壮、可定制且功能强大的解决方案。
回顾要点,我们通过以下基础规范奠定了日志库的基石:
- 健壮的架构 :采用 单例(Singleton)模式确保代码组件间的一致性。
- 高级持久化:支持数据库存储日志,提供可追溯的历史记录以支持深度审计与分析。
- 灵活的输出方式:支持日志在控制台、文件、终端界面或数据库中的便捷存储与展示。
- 日志级别分类:区分信息性消息、关键警报和错误日志。
- 输出格式定制满足不同开发者或项目的个性化需求。
基于这一稳固的基础,我们开发的日志框架已超越了简单事件记录的范畴,将成为实时解读、监控和优化EA行为的战略工具。
在第三篇文章中,我们将深入探讨处理器这一关键概念。如果格式化器(formatters)负责组织数据,那么处理器则决定日志的流向。它们如同"管道工",将消息导向文件、控制台、数据库甚至通知系统等目标。本文将解析处理器的工作逻辑,通过实战案例演示其在不同场景的应用,并探索与格式化器的集成方法。在本篇结束时,您将掌握构建高度可定制化高效日志流的所有必要工具。让我们开始吧!
什么是处理器?
处理器是定义日志消息发送目标的核心组件。可将其视为"消息调度员":从日志记录器(Logger)接收信息,并转发至适当目的地(控制台、文件、邮件或远程服务器)。
想象您正在管理一家工厂。产品(日志消息)需要被运送到不同目的地:一些进入仓库,一些发往发货区,还有一些作为历史记录存档。调度员负责决定每件产品的去向,而处理器正是扮演这一角色的“调度员”。
每个处理器都可以配置特定参数,如:严重级别(比如仅发送错误消息)、输出格式(比如是否包含时间戳)和目标地址。
这些组件对于实现日志消息的分流与智能路由至关重要,尤其在中大型应用中。处理器可实现一些核心功能,例如:支持开发者在控制台实时调试错误、存储详细日志以供未来深度分析、在紧急事件发生时发送关键邮件警报,以及将监控信息转发至中央服务器进行集中管理。所有功能均可同步实现,且无需复杂配置。
处理器如何工作
为深入理解处理器的实际运作机制,我们通过日志库中的示例进行解析。请参考下方图表,该图展示了日志消息处理的基础流程:
在当前流程中,CLogify类的主函数是Append方法,其职责为接收日志数据,包括日志级别、消息内容、来源及时间戳。基于这些数据创建MqlLogifyModel类型的变量,最终通过MetaTrader 5原生Print函数将日志输出至终端控制台。
该流程虽能运行,但存在明显局限:所有日志仅能显示在控制台,并且无法灵活处理或存储日志到别处。
引入处理器后,流程得到显著优化。请参考下方更新后的架构图:
在新流程中:
- Append方法仍负责接收日志信息(级别、消息、来源、时间等)。
- 其创建相同的MqlLogifyModel变量来封装数据。
- 与之前直接输出到控制台不同,现在日志会传递给由处理器数组维护的处理器列表。
每个处理器都能独立处理数据,实现日志消息的多目标路由。
以下是三种本系统使用的基础处理器示例:
- HandlerTerminal: 在MetaTrader 5终端直接显示日志,适用于实时诊断与调试。
- HandlerFile: 将日志保存为.txt或.log文件,适合存储执行历史或生成分析报告。
- HandlerDatabase: 将日志存入数据库(如SQLite),支持高级查询,如趋势分析、复杂审计等。
我将为您提供一个实际示例,以展示处理器的实用性。假设您开发了一个需要高效日志系统的交易机器人。您可以按如下方式配置处理器:
- 仅保存DEBUG和INFO级别日志,记录所有操作(如开仓和平仓信号)。
- 实时输出WARN和ERROR级别日志,便于即时监控跟踪问题。
- 将关键错误存入数据库,供工程师后续分析。
此架构使日志系统更健壮、有序且高效。借助处理器,您可以完全掌控日志的处理与存储方式及位置。
实现处理器基类
接下来,我们将实现一个名为CLogifyHandler的基类,作为后续所有处理器的父类。在库的根目录下新建Handlers文件夹。在其中创建LogifyHandler.mqh文件。在创建的文件中,我们将定义CLogifyHandler类。初始版本仅包含基础构造函数和析构函数,代码如下所示:
//+------------------------------------------------------------------+ //| LogifyHandler.mqh | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" //+------------------------------------------------------------------+ //| class : CLogifyHandler | //| | //| [PROPERTY] | //| Name : CLogifyHandler | //| Heritage : No heritage | //| Description : Base class for all log handlers. | //| | //+------------------------------------------------------------------+ class CLogifyHandler { public: CLogifyHandler(void); ~CLogifyHandler(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandler::CLogifyHandler(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandler::~CLogifyHandler(void) { } //+------------------------------------------------------------------+
此时,CLogifyHandler类仅作为基础“框架”(骨架),后续将逐步扩展其功能。
我们需要引入日志模型定义文件LogifyModel.mqh,该文件包含了所有处理器共用的日志数据结构。
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../LogifyModel.mqh"
该文件定义了MqlLogifyModel类/结构体,用于封装每条日志消息的核心数据,例如:日志级别、消息内容、来源等。
接下来,我们将为基类添加两个受保护属性:
- m_name:存储处理器名称,便于调试或报告时识别。
- m_level:定义处理器将处理的严重性级别,例如:DEBUG、INFO、ERROR。
此外,我们将创建公共方法来设置和检索这些值。
class CLogifyHandler { protected: string m_name; ENUM_LOG_LEVEL m_level; public: //--- Set/Get void SetLevel(ENUM_LOG_LEVEL level); string GetName(void); ENUM_LOG_LEVEL GetLevel(void); }; //+------------------------------------------------------------------+ //| Set level | //+------------------------------------------------------------------+ void CLogifyHandler::SetLevel(ENUM_LOG_LEVEL level) { m_level = level; } //+------------------------------------------------------------------+ //| Get name | //+------------------------------------------------------------------+ string CLogifyHandler::GetName(void) { return(m_name); } //+------------------------------------------------------------------+ //| Get level | //+------------------------------------------------------------------+ ENUM_LOG_LEVEL CLogifyHandler::GetLevel(void) { return(m_level); } //+------------------------------------------------------------------+
m_name 属性仅允许通过派生类的构造函数进行设置,以此强化安全性与封装性。因此未提供SetName方法。
所有处理器都必须实现的三大基本方法:
- Emit(MqlLogifyModel &data): 处理日志消息并发送至目标(文件/控制台/数据库等)
- Flush(): 终止或清空所有待处理操作。
- Close():关闭处理器并释放关联资源。
当前,这些方法将被声明为虚(virtual)函数,并默认为空值。这样的设计可使每个子类根据实际需求自定义具体行为。
class CLogifyHandler { public: //--- Handler methods virtual void Emit(MqlLogifyModel &data); // Processes a log message and sends it to the specified destination virtual void Flush(void); // Clears or completes any pending operations virtual void Close(void); // Closes the handler and releases any resources }; //+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandler::Emit(MqlLogifyModel &data) { } //+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandler::Flush(void) { } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandler::Close(void) { } //+------------------------------------------------------------------+
当前阶段,这些方法暂不实现具体功能,每个处理器的具体行为将通过子类(如HandlerFile、HandlerDatabase等)实现。
处理器的实现
在完成基类CLogifyHandler的开发后,我们可以开始创建继承自它的专用处理器类。这种设计严格遵循面向对象编程的核心原则,例如继承和多态,为代码带来了显著的模块化与灵活性优势。每个专用处理器都将负责以特定方式处理日志:它们共用基类的统一结构,但在Emit、Flush 和Close方法中实现各自的专属逻辑。
在实现具体处理器之前,让我们先规范项目。请在Handlers目录下创建以下三个新文件:分别是:LogifyHandlerConsole.mqh、LogifyHandlerDatabase.mqh和LogifyHandlerFile.mqh。最终结构示例如下:
在LogifyHandlerConsole.mqh文件中,我们将创建CLogifyHandlerConsole类,该类继承自CLogifyHandler基类的所有属性和方法。在类的构造函数中,首要任务是设置基类的m_name变量为 "console"。这一修改将使处理器在运行时可通过名称被清晰识别。以下是该类的初始定义:
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "LogifyHandler.mqh" //+------------------------------------------------------------------+ //| class : CLogifyHandlerConsole | //| | //| [PROPERTY] | //| Name : CLogifyHandlerConsole | //| Heritage : CLogifyHandler | //| Description : Log handler, inserts data into terminal window. | //| | //+------------------------------------------------------------------+ class CLogifyHandlerConsole : public CLogifyHandler { public: CLogifyHandlerConsole(void); ~CLogifyHandlerConsole(void); virtual void Emit(MqlLogifyModel &data); // Processes a log message and sends it to the specified destination virtual void Flush(void); // Clears or completes any pending operations virtual void Close(void); // Closes the handler and releases any resources }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandlerConsole::CLogifyHandlerConsole(void) { m_name = "console"; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandlerConsole::~CLogifyHandlerConsole(void) { } //+------------------------------------------------------------------+
Emit函数的核心职责是处理日志消息并将其发送至指定目标。对于控制台而言,这仅仅意味着在MetaTrader终端中显示已格式化的消息。以下是该方法的实现过程:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerConsole::Emit(MqlLogifyModel &data) { if(data.level >= this.GetLevel()) { Print("Console handler: ",data.formated); } } //+------------------------------------------------------------------+
请注意,在显示消息之前,我们会先检查日志级别(data.level)是否与处理器中配置的级别相匹配。这样确保只有重要或相关的消息才会被显示出来。
遵循与控制台处理器相同的原理,我们可以创建其他专用处理器,例如数据库和文件处理器。需要修改的文件是LogifyHandlerDatabase.mqh和LogifyHandlerFile.mqh。虽然这些处理器可能共享相同的基本逻辑,但它们对Emit方法的具体实现会有所不同。
该处理器旨在将日志存储到数据库中,不过目前为了演示目的,我们仅向控制台输出一条消息。Emit函数的代码如下:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerDatabase::Emit(MqlLogifyModel &data) { if(data.level >= this.GetLevel()) { Print("Database handler: ",data.formated); } } //+------------------------------------------------------------------+
LogifyHandlerFile处理器用于将日志写入指定文件。以下是Emit的初始实现:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerFile::Emit(MqlLogifyModel &data) { if(data.level >= this.GetLevel()) { Print("File handler: ",data.formated); } } //+------------------------------------------------------------------+
尽管我们在基类中定义了Flush与Close方法,但并非所有处理器都需要立即实现这两种方法。
- Flush 方法对复杂处理器(如文件写入或实时流)尤为实用。
- Close 方法则用于释放资源,例如关闭数据库连接或输出流。
对于控制台处理器,这两种方法留空即可,因为无需额外操作。请记住文中仅展示代码段,完整版代码可在文章末尾下载。
将处理器集成到CLogify类
现在处理器已经单独运行正常,接下来要把它们整合至日志库的主类CLogify中。首先导入基类CLogifyHandler,它包含所有处理器所需的通用结构:
#include "Handlers/LogifyHandler.mqh" #include "Handlers/LogifyHandlerConsole.mqh" #include "Handlers/LogifyHandlerDatabase.mqh" #include "Handlers/LogifyHandlerFile.mqh"
在CLogify类的实现中,我们将添加一个私有成员,用于存储待使用的处理器。我选用CLogifyHandler类型的指针数组,以便动态管理这些处理器。
此外,该类具有其专属的管理处理器的方法:
- AddHandler:向数组新增处理器。
- HasHandler:按名称检查某处理器是否已存在。
- GetHandler:按数组名称或索引获取处理器。
- SizeHandlers:返回列表中的处理器总数。
以下是更新后的CLogify类代码,已包含上述方法:
class CLogify { private: CLogifyHandler *m_handlers[]; public: //--- Handler void AddHandler(CLogifyHandler *handler); bool HasHandler(string name); CLogifyHandler *GetHandler(string name); CLogifyHandler *GetHandler(int index); int SizeHandlers(void); }; //+------------------------------------------------------------------+ //| Add handler to handlers array | //+------------------------------------------------------------------+ void CLogify::AddHandler(CLogifyHandler *handler) { int size = ArraySize(m_handlers); ArrayResize(m_handlers,size+1); m_handlers[size] = GetPointer(handler); } //+------------------------------------------------------------------+ //| Checks if handler is already in the array by name | //+------------------------------------------------------------------+ bool CLogify::HasHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(true); } } return(false); } //+------------------------------------------------------------------+ //| Get handler by name | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(m_handlers[i]); } } return(NULL); } //+------------------------------------------------------------------+ //| Get handler by index | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(int index) { return(m_handlers[index]); } //+------------------------------------------------------------------+ //| Gets the total size of the handlers array | //+------------------------------------------------------------------+ int CLogify::SizeHandlers(void) { return(ArraySize(m_handlers)); } //+------------------------------------------------------------------+
借助处理器管理方法,我们接下来修改Append函数,使其在记录日志时调用这些处理器。
现在,Append会遍历数组中的所有处理器,并逐一调用它们的Emit方法,从而把日志发送到对应目标(控制台、数据库等)。
以下是更新后的Append方法代码:
//+------------------------------------------------------------------+ //| Generic method for adding logs | //+------------------------------------------------------------------+ bool CLogify::Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { //--- If the formatter is not configured, the log will not be recorded. if(m_formatter == NULL) { return(false); } //--- Textual name of the log level string levelStr = ""; switch(level) { case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break; case LOG_LEVEL_INFOR: levelStr = "INFOR"; break; case LOG_LEVEL_ALERT: levelStr = "ALERT"; break; case LOG_LEVEL_ERROR: levelStr = "ERROR"; break; case LOG_LEVEL_FATAL: levelStr = "FATAL"; break; } //--- Creating a log template with detailed information datetime time_current = TimeCurrent(); MqlLogifyModel data("",levelStr,msg,args,time_current,time_current,level,origin,filename,function,line); data.formated = m_formatter.FormatLog(data); //--- Call handlers int size = this.SizeHandlers(); for(int i=0;i<size;i++) { m_handlers[i].Emit(data); } return(true); } //+------------------------------------------------------------------+
说明如下:
- 格式化后的日志保存在data.formatted中,确保所有处理器都能获得完整信息。
- 每个处理器独立处理日志,调用各自的Emit方法。
最后要做的调整是在类析构函数中释放数组指针:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogify::~CLogify() { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter; } //--- Delete handlers int size_handlers = ArraySize(m_handlers); for(int i=0;i<size_handlers;i++) { delete m_handlers[i]; } } //+------------------------------------------------------------------+
测试处理器
本例将使用之前提到的测试文件LogifyTest.mq5。目标是演示如何配置并启用两个日志处理器,它们分别具有不同的日志级别,以及如何基于所配置的过滤器记录日志。
首先创建两个处理器,负责在不同位置和级别记录日志:
- 控制台:通过配置CLogifyHandlerConsole实例,记录DEBUG级别消息(即所有消息,从最详细到关键的错误)。
- 文件:通过配置CLogifyHandlerFile实例,仅捕获INFO及以上级别消息,过滤掉DEBUG消息。
配置这些处理器的代码如下:
int OnInit() { //--- Console CLogifyHandler *handler_console = new CLogifyHandlerConsole(); handler_console.SetLevel(LOG_LEVEL_DEBUG); //--- File CLogifyHandler *handler_file = new CLogifyHandlerFile(); handler_file.SetLevel(LOG_LEVEL_INFOR); return(INIT_SUCCEEDED); }
配置好处理器后,下一步是将它们加入基础类CLogify中,由该类统一管理应用的所有日志。
此外,我们还将添加一个 formatter,用于定义日志的显示格式。本例中的格式化器将采用以下模式:"时:分:秒, [日志级别], 消息"。
配置完处理器和格式化器后,我们将发出三条不同级别的日志消息。以下是此步骤的完整代码:
int OnInit() { //--- Console CLogifyHandler *handler_console = new CLogifyHandlerConsole(); handler_console.SetLevel(LOG_LEVEL_DEBUG); //--- File CLogifyHandler *handler_file = new CLogifyHandlerFile(); handler_file.SetLevel(LOG_LEVEL_INFOR); //--- Config logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); logify.AddHandler(handler_console); logify.AddHandler(handler_file); //--- Logs logify.Debug("Debug Message"); logify.Infor("Information Message"); logify.Error("Error Message"); return(INIT_SUCCEEDED); }
当运行上述代码时,我们将在控制台中看到如下输出:
Console handler: 03:20:05 [DEBUG] Debug Message Console handler: 03:20:05 [INFOR] Information Message File handler: 03:20:05 [INFOR] Information Message Console handler: 03:20:05 [ERROR] Error Message File handler: 03:20:05 [ERROR] Error Message
结果解析:
- 控制台处理器( handler_console):该处理器捕获所有日志消息,涵盖从DEBUG到 ERROR的全部级别。因此,控制台共记录了3条日志(对应每次调用的输出)。
- 文件处理器(handler_file):该处理器仅配置记录INFO级别及以上的消息。因此,忽略了DEBUG日志,最终在日志文件中仅记录了2条日志(INFO和ERROR各一条)。
结论
通过本文,我们在构建MQL5日志库的道路上迈出了关键的一步。我们深入探讨了处理器(handlers) 的概念,掌握它们作为日志消息“指挥家”的核心作用——将消息导向不同的目标。同时,我们学习了处理器如何与格式化器(formatters)协同工作,共同构成一个模块化、可扩展的日志处理系统。
在实践中,我们已构建好处理器的基础结构——定义了一个抽象类,作为所有后续实现的基石。同时开发了三大初始处理器:控制台、数据库与文件,分别负责将日志导向控制台、数据库和文件。目前它们均用Print()函数做模拟,但这一坚实基础让我们能在未来文章中扩展并专业化各类,赋予其最终功能。
测试环节验证了处理器与日志库的集成,展示了其灵活添加与使用的方式。整个过程展示了处理器作为模块化组件的潜力,可按需定制以满足不同日志需求。
本文中使用的所有代码都附在下方。下表列出了库中各文件的说明:
文件名 | 描述 |
---|---|
Experts/Logify/LogiftTest.mq5 | 测试日志库功能的文件,包含一个实用示例 |
Include/Logify/Formatter/LogifyFormatter.mqh | 负责格式化日志记录的类,将占位符替换为具体值 |
Include/Logify/Handlers/LogifyHandler.mqh | 日志处理器管理基类,包含日志级别设置与消息发送功能 |
Include/Logify/Handlers/LogifyHandlerConsole.mqh | MetaTrader终端控制台日志处理器,直接输出格式化日志至终端 |
Include/Logify/Handlers/LogifyHandlerDatabase.mqh | 数据库日志处理器(当前仅实现打印功能,后续将支持SQLite数据库存储) |
Include/Logify/Handlers/LogifyHandlerFile.mqh | 文件日志处理器(当前仅实现打印预览,后续将支持真实文件存储) |
Include/Logify/Logify.mqh | 日志管理核心类,集成日志级别、输出模型与格式化功能 |
Include/Logify/LogifyLevel.mqh | Logify库日志级别定义文件,支持精细化日志控制 |
Include/Logify/LogifyModel.mqh | 日志记录数据结构模型,涵盖级别、消息、时间戳及内容信息 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16866
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



