English Deutsch 日本語
preview
精通日志记录(第八部分):具备自动翻译能力的错误日志记录

精通日志记录(第八部分):具备自动翻译能力的错误日志记录

MetaTrader 5示例 |
19 6
joaopedrodev
joaopedrodev

引言

经过前几期的讲解,想必您已深知日志记录绝非简单的事件流水账。在算法交易的日常操作中,K线跳动、策略决策与市场不确定性交织成一张复杂的网,日志的真正价值是精准记录EA程序的"真实意图"。

在日常使用Logify时,我发现一个亟待解决的问题:错误处理机制仍然停留在表层。即使拥有完善的日志格式结构,最终输出的日志内容仍只能显示原始错误代码,无法直观体现背后的真实含义。比如以下这段日志:

logify.Error("Failed to send sell order", "Order Management", "Code: "+IntegerToString(GetLastError()));
// Console:
// 2025.06.06 10:30:00 [ERROR]: (Order Management) Failed to send sell order | Code: 10016

结果呢?一句模棱两可的提示。我们能知道报错发生在哪个模块,但无法得知失败的具体原因。试问,谁不曾为了查阅文档而翻阅那几十个晦涩难懂的 MQL5 错误代码?我以前就常常面临这种困境:拿到错误代码后,还得打开文档慢慢搜索,才能搞清楚问题的真正起因。正是源于这种真实的痛点,我萌生了一个想法:如果 Logify 能帮我解读报错会怎样?它能不能不只给我一串数字代码,还自动生成清晰、贴合场景的错误解释,直接写入日志文件?

由此,Logify迎来了一次重要的功能进化:现在用户只需将错误代码直接传入库中,剩下的翻译、格式化与显示工作都由库自动完成。将前面的示例按新方案重构后,代码将变得更加简洁高效:

logify.Error("Failed to send sell order", GetLastError(), "Order Management");
// Console:
// 2025.06.06 10:30:00 [ERROR]: (Order Management) Failed to send sell order | 10016: Invalid stops in the request

日志的可读性和实用性已经提升了一大截,但我决定更进一步。一个致力于传递清晰信号的系统,不仅要在代码层面简洁易懂,更要在自然语言表达上保持一致。因此,本文的Logify新增了多语言报错支持,可自动转换为11种语言,覆盖英语、葡萄牙语、西班牙语、德语意大利语、俄语、土耳其语、中文、日语和韩语。从此,你的日志可以直接使用团队或客户的母语,无需手动调整语言包。

本期学习目标:

  1. 直接调用MQL5官方文档,为报错日志补充精准的语义描述。
  2. 实现多语言报错提示,根据使用场景动态切换适配语言
  3. 根据日志的重要级别自定义格式,为错误、警告和消息创建差异化的呈现模板

归根结底,Logify 将变得更加智能、易用,在实战中也更加实用。在算法交易领域,细节决定成败,每一秒浪费在破解错误代码的时间,都可能错过下一个关键决策窗口。


建立报错处理体系

万丈高楼平地起,我们首先定义一个结构体来表示报错信息。我们将这个结构称为 MqlError,它将在整个系统中用于存储任何报错的三个基本要素:

  • 数值代码 (code)
  • 符号常量 (constant)
  • 可读描述 (description)

我们在 <Include/Logify/Error/Error.mqh> 文件中创建了这个结构体:

//+------------------------------------------------------------------+
//| Data structure for error handling                                |
//+------------------------------------------------------------------+
struct MqlError
  {
   int      code;          // Cod of error
   string   description;   // Description of error
   string   constant;      // Type error
   
   MqlError::MqlError(void)
     {
      code = 0;
      description = "";
      constant = "";
     }
  };
//+------------------------------------------------------------------+

这个结构虽然简单,但足以表示平台返回的任何报错,无论是执行报错、交易报错,还是用户自定义报错。既然有了容器,我们需要用实际的报错数据来填充它。

MetaQuotes 提供了数百个错误代码,每个代码都有其符号常量和英文描述,但我们想走得更远。我们希望库能说德语、西班牙语、法语、葡萄牙语、中文……并且日志能自动适应配置的语言。

这里的策略是为每种语言创建一个 .mqh 文件,其中包含一个用于初始化所有已知报错数组的函数。文件名遵循 ErrorMessages.xx.mqh 的标准,其中 xx 代表语言缩写(例如 en 代表英语,de 代表德语,pt 代表葡萄牙语)。

以下是英语文件的样貌:

//+------------------------------------------------------------------+
//|                                             ErrorMessages.en.mqh |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/en/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/en/users/joaopedrodev"
//+------------------------------------------------------------------+
//| Import struct                                                    
//+------------------------------------------------------------------+
#include "../Error.mqh"
void InitializeErrorsEnglish(MqlError &errors[])
  {
   //--- Free and resize
   ArrayFree(errors);
   ArrayResize(errors,274);
   
   //+------------------------------------------------------------------+
   //| Unknown error                                                    |
   //+------------------------------------------------------------------+
   errors[0].code = 0;
   errors[0].description = "No error found";
   errors[0].constant = "ERROR_UNKNOWN";
   //+------------------------------------------------------------------+
   //| Server error                                                     |
   //+------------------------------------------------------------------+
   errors[1].code = 10004;
   errors[1].description = "New quote";
   errors[1].constant = "TRADE_RETCODE_REQUOTE";
   //---
   errors[2].code = 10006;
   errors[2].description = "Request rejected";
   errors[2].constant = "TRADE_RETCODE_REJECT";
   //---
   // Remaining error codes...
   //---
   errors[272].code = 5625;
   errors[272].description = "Parameter binding error, wrong index";
   errors[272].constant = "ERR_DATABASE_RANGE";
   //---
   errors[273].code = 5626;
   errors[273].description = "Open file is not a database file";
   errors[273].constant = "ERR_DATABASE_NOTADB";
  }
//+------------------------------------------------------------------+

InitializeErrorsEnglish() 函数接收一个 MqlError 数组,并用英语的所有已知错误填充它。我们对德语、西班牙语、法语、意大利语、日语、韩语、葡萄牙语、俄语、土耳其语和中文重复此模式。所有这些文件都保存在 <Include/Logify/Error/Languages> 文件夹中。总共有 11 个语言文件,每个文件包含 274 个条目。是的,这是一项艰苦的工作,但现在您只需简单包含即可享用。请记住,所有代码都附在文章末尾。

既然数据都已整理完毕,我们需要一个接口来查询它们。这就是 CLogifyError 类的用武之地。该类将负责加载正确语言的报错,并为任何请求的错误代码返回完整信息。

类的接口非常简洁:

//+------------------------------------------------------------------+
//| class : CLogifyError                                             |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyError                                        |
//| Heritage    : No heritage                                        |
//| Description : class to look up the error code and return details |
//|               of each error code.                                |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyError
  {
private:
   
   ENUM_LANGUAGE     m_language;
   MqlError          m_errors[];
   
public:
                     CLogifyError(void);
                    ~CLogifyError(void);
   
   //--- Set/Get
   void              SetLanguage(ENUM_LANGUAGE language);
   ENUM_LANGUAGE     GetLanguage(void);
   
   //--- Get error
   MqlError          Error(int code);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyError::CLogifyError()
  {
   InitializeErrorsEnglish(m_errors);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyError::~CLogifyError(void)
  {
  }
//+------------------------------------------------------------------+

实例化时,默认加载英文报错信息。您可以随时使用 SetLanguage() 更改语言,它将重新加载为相应语言的数据。

Error(int code) 方法是该类的核心。它在错误数组中搜索输入的代码,并返回对应的 MqlError。如果未找到该代码,函数将返回一个通用错误作为回退。此外,它会自动检测错误是否位于为用户定义错误保留的范围内(ERR_USER_ERROR_FIRST 到 ERR_USER_ERROR_LAST),并同样为它们返回答案。

//+------------------------------------------------------------------+
//| Returns error information based on the error code received       |
//+------------------------------------------------------------------+
MqlError CLogifyError::Error(int code)
  {
   int size = ArraySize(m_errors);
   for(int i=0;i<size;i++)
     {
      if(m_errors[i].code == code)
        {
         //--- Return
         return(m_errors[i]);
        }
     }
   
   //--- User error
   if(code >= ERR_USER_ERROR_FIRST && code < ERR_USER_ERROR_LAST)
     {
      MqlError error;
      error.code = code;
      error.constant = "User error";
      error.description = "ERR_USER_ERROR";
      
      //--- Return
      return(m_errors[274]);
     }
   
   //--- Return
   return(m_errors[0]);
  }
//+------------------------------------------------------------------+

最后,我们将此错误结构添加到 MqlLogifyModel 日志数据模型中,以便我们可以在记录的数据结构内部访问错误数据。

#include "LogifyLevel.mqh"
#include "Error/Error.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct MqlLogifyModel
  {
   string formated;        // The log message formatted according to the specified format.
   string levelname;       // Textual name of the log level (e.g., "DEBUG", "INFO")
   string msg;             // Main content of the log message
   string args;            // Additional arguments associated with the log message
   ulong timestamp;        // Timestamp of the log event, represented in seconds since the start of the Unix epoch
   datetime date_time;     // Date and time of the log event, in datetime format
   ENUM_LOG_LEVEL level;   // Enumeration representing the severity level of the log
   string origin;          // Source or context of the log message (e.g., class or module)
   string filename;        // Name of the source file where the log message was generated
   string function;        // Name of the function where the log was called
   ulong line;             // Line number in the source file where the log was generated
   MqlError error;         // Error data
   
   void MqlLogifyModel::Reset(void)
     {
      formated = "";
      levelname = "";
      msg = "";
      args = "";
      timestamp = 0;
      date_time = 0;
      level = LOG_LEVEL_DEBUG;
      origin = "";
      filename = "";
      function = "";
      line = 0;
     }
   
   MqlLogifyModel::MqlLogifyModel(void)
     {
      this.Reset();
     }
   MqlLogifyModel::MqlLogifyModel(string _formated,string _levelname,string _msg,string _args,ulong _timestamp,datetime _date_time,ENUM_LOG_LEVEL _level,string _origin,string _filename,string _function,ulong _line,MqlError &_error)
     {
      formated = _formated;
      levelname = _levelname;
      msg = _msg;
      args = _args;
      timestamp = _timestamp;
      date_time = _date_time;
      level = _level;
      origin = _origin;
      filename = _filename;
      function = _function;
      line = _line;
      error = _error;
     }
  };
//+------------------------------------------------------------------+

完成这些更改后,我们的库函数结构如下所示:


将错误集成到主类中

我们已经有了表示错误的可靠结构、承载其描述的多语言文件以及一个专门根据语言检索这些消息的类。但到目前为止,所有这些都还是空中楼阁,与库的中央引擎——CLogify 类——处于断开状态。

是时候将错误系统集成到 Logify 的核心中了,以便生成的每条日志(如有必要)都能包含对问题清晰、多语言的解释。为此,我们将遵循三个步骤:实例化错误类、调整添加日志的方法(Append),最后调整格式化器以识别新的占位符 {err_code}、{err_constant} 和 {err_description}。

1. 将智能报错处理引入 Logify 核心

CLogify 类负责连接日志的格式化、处理和发送,因此,首要任务是让该类直接对接我们的错误处理系统。要实现这点,只需导入 LogifyError.mqh 文件并将 CLogifyError 类实例化为 CLogify 的私有成员。这保证了它在内部始终可用,而无需用户操心。这一小小的补充打开了通往成功的大门。现在,每当发生报错,我们都可以快速查询其代码、描述和常量。

由于 CLogifyError 的默认语言是英语,如果不允许用户选择最合适的语言就太浪费了。这就是为什么我们添加了两个简单的方法。代码最终如下所示:

#include "LogifyModel.mqh"
#include "Handlers/LogifyHandler.mqh"
#include "Handlers/LogifyHandlerComment.mqh"
#include "Handlers/LogifyHandlerConsole.mqh"
#include "Handlers/LogifyHandlerDatabase.mqh"
#include "Handlers/LogifyHandlerFile.mqh"
#include "Error/LogifyError.mqh"
//+------------------------------------------------------------------+
//| class : CLogify                                                  |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : Logify                                             |
//| Heritage    : No heritage                                        |
//| Description : Core class for log management.                     |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogify
  {
private:
   CLogifyError      m_error;
         
public:
                     CLogify();
                    ~CLogify();
   
   //--- Language
   void              SetLanguage(ENUM_LANGUAGE language);
   ENUM_LANGUAGE     GetLanguage(void);
  };
//+------------------------------------------------------------------+

2. 用错误代码丰富 Append 方法

到目前为止,Append() 方法接收主消息、附加参数、来源、文件名、行号、函数……但唯独不包含错误代码本身。如果我们希望日志携带有关故障的上下文,就需要允许传递此代码,即使是可选的。这就是为什么我们在方法签名的末尾添加了 code_error 参数:

bool Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0, int code_error = 0);

这样一来,对于那些不想传递错误的用户,对 Append() 的调用不会发生变化;而对于想传递错误的用户,只需将其附加在末尾即可。这种方法避免了对现有代码兼容性的破坏。

现在,手头有了错误代码,我们使用 CLogifyError 类的 Error() 方法来获取相应的 MqlError 结构:

//+------------------------------------------------------------------+
//| 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,int code_error=0)
  {
   //--- Ensures that there is at least one handler
   this.EnsureDefaultHandler();
   
   //--- Textual name of the log level
   string levelStr = "";
   switch(level)
     {
      case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break;
      case LOG_LEVEL_INFO : levelStr = "INFO"; 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,m_error.Error(code_error));
   
   //--- Call handlers
   int size = this.SizeHandlers();
   for(int i=0;i<size;i++)
     {
      data.formated = m_handlers[i].GetFormatter().Format(data);
      m_handlers[i].Emit(data);
     }
   
   return(true);
  }
//+------------------------------------------------------------------+

MqlLogifyModel 对象现在不仅携带日志信息,还携带了导致报错的“灵魂”。现在剩下要做的就是为 Error() 和 Fatal() 方法添加一个新的函数重载,同时不删除现有的重载,因为这可能会引发错误,因为库用户并不总是需要添加错误代码。代码如下所示:

class CLogify
  {
public:
   //--- Specific methods for each log level
   bool              Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Info(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Error(string msg, int code_error, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Fatal(string msg, int code_error, string origin = "", string args = "",string filename="",string function="",int line=0);
  };
bool CLogify::Error(string msg, int code_error, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   return(this.Append(LOG_LEVEL_ERROR,msg,origin,args,filename,function,line,code_error));
  }
bool CLogify::Fatal(string msg, int code_error, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   return(this.Append(LOG_LEVEL_FATAL,msg,origin,args,filename,function,line,code_error));
  }

3. 使用报错占位符格式化消息

到目前为止,报错信息已被加载,但尚未出现在格式化的日志中。为此,我们需要扩展 CLogifyFormatter 的占位符以包含错误属性。逻辑简单而巧妙:我们仅在日志级别为 ERROR 或更高时显示错误数据。信息或调试日志不需要这种多余的内容,我们要的是简洁的日志,而非干扰信息。

//+------------------------------------------------------------------+
//| Format logs                                                      |
//+------------------------------------------------------------------+
string CLogifyFormatter::FormatLog(MqlLogifyModel &data)
  {
   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));
   
   if(data.level >= LOG_LEVEL_ERROR)
     {
      StringReplace(formated,"{err_code}",IntegerToString(data.error.code));
      StringReplace(formated,"{err_constant}",data.error.constant);
      StringReplace(formated,"{err_description}",data.error.description);
     }
   else
     {
      StringReplace(formated,"{err_code}","");
      StringReplace(formated,"{err_constant}","");
      StringReplace(formated,"{err_description}","");
     }
   
   return(formated);
  }
//+------------------------------------------------------------------+

至此,我们为Logify新增了三个定制化扩展接口:

  • {err_code}:显示确切的错误编号(例如 10006)
  • {err_constant}:关联的常量(例如 TRADE_RETCODE_REJECT)
  • {err_description}:可读的解释(例如 Request rejected)

假设我们定义了一个全局格式,如下所示:

"{date_time} [{levelname}] {msg} ({err_constant} {err_code}: {err_description})"

在您的设想中,这应该产生类似这样的结果:

2025.06.09 14:22:15 [ERROR] Failed to send order (10016 TRADE_RETCODE_REJECT: Request rejected)

实际上,对于 ERROR 和 FATAL 日志级别,这非常完美。但是,当我们将同一格式用于 DEBUG、INFO 或 ALERT 日志时会怎样?结果会是这样:

2025.06.09 14:22:15 [INFO] Operation completed successfully (  : )

或者更糟,根据格式的不同,甚至可能导致日志视觉上的错位。尽管我们小心翼翼地清除了较低级别的错误占位符,但问题依然存在:该格式是预期 {err_code}、{err_constant} 和 {err_description} 有值的。当它们没有值时,日志看起来就会……怪异且残缺。我们这里遇到的是语义上的断裂:同一套格式掩码被应用到了上下文完全不同的消息上。

解决方案不在于试图修正数据,而在于将每个日志级别视为它本来的样子:一种不同类型的消息,有着不同的需求和格式。

因此,我们不再使用具有通用格式的 CLogifyFormatter,而是让每个级别(DEBUG、INFO、ALERT、ERROR、FATAL)都有自己的格式化器。这允许我们进行定制,例如:

  • DEBUG:显示函数、文件和行号
  • INFO:极简风格,只显示发生了什么
  • ALERT:添加上下文,如来源和参数
  • ERROR:包含错误数据
  • FATAL:尽可能完整


实现多种格式

首先,我们调整格式的存储方式,不再是一个简单的字符串,而是将其更改为一个包含 5 种可能性(每个重要级别一种)的数组。

原代码 新代码
class CLogifyFormatter
  {
private:
   
   //--- Stores the formats
   string            m_date_format;
   string            m_log_format;
  };
class CLogifyFormatter
  {
private:
   
   //--- Stores the formats
   string            m_date_format;
   string            m_format[5];
  };

我们将 m_log_format 替换为一个字符串数组,每个位置对应一个日志级别。现在我们有五种不同的格式,每一种都可以根据将要发送的消息类型进行调整。

格式按级别分离后,我们需要一种便捷的方法来设置它们。为此,我们创建了两个 SetFormat 方法,一个将相同的格式应用于所有级别,另一个单独配置特定级别:

class CLogifyFormatter
  {
public:
   void              SetFormat(string format);
   void              SetFormat(ENUM_LOG_LEVEL level, string format);
  };
//+------------------------------------------------------------------+
//| Sets the format for all levels                                   |
//+------------------------------------------------------------------+
void CLogifyFormatter::SetFormat(string format)
  {
   m_format[LOG_LEVEL_DEBUG] = format;
   m_format[LOG_LEVEL_INFO] = format;
   m_format[LOG_LEVEL_ALERT] = format;
   m_format[LOG_LEVEL_ERROR] = format;
   m_format[LOG_LEVEL_FATAL] = format;
  }
//+------------------------------------------------------------------+
//| Sets the format to a specific level                              |
//+------------------------------------------------------------------+
void CLogifyFormatter::SetFormat(ENUM_LOG_LEVEL level, string format)
  {
   m_format[level] = format;
  }
//+------------------------------------------------------------------+

这种设计不仅方便,而且既通用又具体。对于追求简单的用户,一个 SetFormat(...) 即可搞定。对于追求精准的用户,只需为每个所需级别调用 SetFormat(level, format)。

格式分离后,格式化方法现在需要反映这种划分。以前,我们使用通用变量 m_log_format 来获取消息掩码。。现在,我们直接在数组中查找格式,通过日志级别进行索引:

原代码 新代码
string CLogifyFormatter::FormatLog(MqlLogifyModel &data)
  {
   string formated = m_log_format;
   ...
  }
string CLogifyFormatter::Format(MqlLogifyModel &data)
  {
   string formated = m_format[data.level];
   ...
  }

这以一种干净、可扩展且可预测的方式解决了问题。无需额外检查,无需抑制空占位符的变通方法。每个级别都确切知道自己应该包含什么。


测试

接下来,我们来到了让所有努力焕发生机的部分——实际测试,看看它在不同场景和日志级别下的表现如何。同上一篇文章一样,在用于测试的文件 LogifyTest.mqh 中,我们准备了以下元素:

  • 一个通过 Comment() 的可视化处理程序,带有边框、标题和行数限制。
  • 一个通过 Print() 的控制台处理程序,用于在平台内部日志中记录消息。
  • 一个在处理程序之间共享的格式化器。
  • 四条不同级别的日志消息:两条 DEBUG,一条 INFO 和一条带代码的 ERROR。
//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify logify;
//+------------------------------------------------------------------+
//| Expert initialization                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Handler config
   MqlLogifyHandleCommentConfig m_config;
   m_config.size = 5;                                      // Max log lines
   m_config.frame_style = LOG_FRAME_STYLE_SINGLE;          // Frame style
   m_config.direction = LOG_DIRECTION_UP;                  // Log direction (up)
   m_config.title = "Expert name";                         // Log panel title
   
   CLogifyFormatter *formatter = new CLogifyFormatter("{date_time} [{levelname}]: {msg}");
   formatter.SetFormat(LOG_LEVEL_ERROR,"{date_time} [{levelname}]: {msg} [{err_constant} | {err_code} | {err_description}]");
   
   //--- Create and configure handler
   CLogifyHandlerComment *handler_comment = new CLogifyHandlerComment();
   handler_comment.SetConfig(m_config);
   handler_comment.SetLevel(LOG_LEVEL_DEBUG);              // Min log level
   handler_comment.SetFormatter(formatter);
   
   CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole();
   handler_console.SetLevel(LOG_LEVEL_DEBUG);              // Min log level
   handler_console.SetFormatter(formatter);
   
   //--- Add handler to Logify
   logify.AddHandler(handler_comment);
   logify.AddHandler(handler_console);
   
   //--- Test logs
   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

当您运行此代码时,MetaTrader 控制台会显示以下消息,格式正如我们所预期。视觉效果清楚地表明,较低级别(如 DEBUG 和 INFO)遵循较精简的格式,而 ERROR 则调用包含报错信息的更详细格式:

09/06/2025 14:22:31 [DEBUG]: Initializing Expert Advisor...
09/06/2025 14:22:31 [DEBUG]: RSI indicator value calculated: 72.56
09/06/2025 14:22:31 [INFO]: Buy order sent successfully
09/06/2025 14:22:31 [ERROR]: Failed to send sell order [TRADE_DISABLED | 10016 | Trade operations not allowed]

请注意最后一条日志如何在方括号中携带了一个附加块,其中包含符号常量、数字代码和直观易懂的报错描述。而这一切的实现,都无需在 EA 代码中添加额外的条件逻辑。每个日志级别都带有自己的格式,正确的占位符仅在有意义的地方出现。


结论

在这篇文章中,我们对 Logify 库进行了改进。在第一篇文章中,我们首先构建了数据模型,然后为不同的输出通道创建了灵活的处理程序。而今天,我们增加了对报错消息的多语言支持,允许每条日志为开发者带来清晰的技术上下文,无论使用何种语言。

我们通过动态格式化器取得了进展,它能够处理自定义占位符并自动适应日志的重要级别。当我们发现通用格式导致日志风格不统一后,对架构进行了升级,让系统可以为每个重要等级配置专属格式模板。最终我们通过一场实战测试完成了全部功能验证,Logify在真实交易环境下运行稳定高效,不仅输出的日志简洁规范,表现符合预期,而且可以灵活拓展新功能。

如果您坚持读到了这里,说明您已经学会了如何:

  • 设计可扩展且解耦的日志结构;
  • 基于 MetaTrader 代码集成多语言报错消息;
  • 结合日志的重要等级,使用占位符动态格式化上下文信息;
  • 构建具有独立配置的可重用处理程序;

Logify 像任何好工具一样,可以继续演进。如果实现了新功能,我会在未来的文章中带给大家。在此期间,请尽情使用、调整和改进。


文件名 说明
Experts/Logify/LogifyTest.mq5
用于测试库功能的文件,包含一个实际示例
Include/Logify/Error/Languages/ErrorMessages.XX.mqh 统计每种语言的报错消息数量,其中 X 代表语言的缩写
Include/Logify/Error/Error.mqh
用于存储报错的数据结构
Include/Logify/Error/LogifyError.mqh
用于获取详细报错信息的类
Include/Logify/Formatter/LogifyFormatter.mqh
负责格式化日志记录的类,将占位符替换为具体值
Include/Logify/Handlers/LogifyHandler.mqh
用于管理日志处理器的基类,包括级别设置和日志发送
Include/Logify/Handlers/LogifyHandlerComment.mqh
负责格式化日志记录的类,将占位符替换为具体值
Include/Logify/Handlers/LogifyHandlerConsole.mqh
将格式化后的日志直接发送到 MetaTrader 终端控制台的日志处理器
Include/Logify/Handlers/LogifyHandlerDatabase.mqh
将格式化后的日志发送到数据库的日志处理器(目前仅包含打印输出,但很快我们会将其保存到真正的 sqlite 数据库中)
Include/Logify/Handlers/LogifyHandlerFile.mqh
将格式化后的日志发送到文件的日志处理器
Include/Logify/Utils/IntervalWatcher.mqh
检查一个时间间隔是否已经过去,允许您在库内部创建定时执行的例程。
Include/Logify/Logify.mqh 日志管理的核心类,集成了级别、模型和格式化功能
Include/Logify/LogifyLevel.mqh 定义 Logify 库日志级别的文件,支持精细控制
Include/Logify/LogifyModel.mqh 用于建模日志记录的结构,包含级别、消息、时间戳和上下文等详细信息

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18467

附加的文件 |
Logify.zip (151.36 KB)
最近评论 | 前往讨论 (6)
joaopedrodev
joaopedrodev | 20 6月 2025 在 12:44
谢谢,我很乐意提供帮助。如有任何改进建议,请与我联系!
hini
hini | 20 6月 2025 在 17:42
joaopedrodev #:
谢谢,我很乐意提供帮助。如有任何改进建议,请与我联系!
如果日志记录非常频繁,但我又想避免重复打印,该怎么办?例如,检测到交易开仓信号,但价差过大且持续数十秒--如果使用日志记录,至少会打印数十次甚至数百次。如何解决这种情况?我知道一种解决方案是使用变量确保日志只显示一次,但如果日志库本身能处理这个问题就更好了。您能提供一些建议吗?
hini
hini | 20 6月 2025 在 17:54

我编译了日志库 Logify,在 MT5 build 5100 之后,出现了几个与 CLogifyHandlerDatabase::Query 中的 类型有关的编译错误。我相信您应该已经解决了这个问题。
joaopedrodev
joaopedrodev | 23 6月 2025 在 13:24
感谢您的建议,我将在今后的文章中讨论这些建议。
Carl Schreiber
Carl Schreiber | 26 9月 2025 在 20:01
joaopedrodev #:
感谢您的建议,我会在今后的文章中采纳这些建议。

关于语言的一个小想法。

或许可以将原始英文信息编排成这样一种方式,即可以很容易地将其复制并粘贴到翻译器(deepl.com 或 translate.google.com)中,然后将结果放回程序中。这样,任何人都可以轻松地用自己的语言设置程序。

Deeple 可识别 36 种语言,Google 甚至可识别约 130 种语言。

在 MQL5 中实现其他语言的实用模块(第 01 部分):构建受 Python 启发的 SQLite3 库 在 MQL5 中实现其他语言的实用模块(第 01 部分):构建受 Python 启发的 SQLite3 库
Python 中的 sqlite3 模块提供了一种使用 SQLite 数据库的简单方法,它既快速又方便。在本文中,我们将在内置的 MQL5 函数的基础上构建一个类似的模块,用于处理数据库,使在 MQL5 中使用 SQLite3 数据库更容易,就像在 Python 中一样。
数据科学和机器学习(第 37 部分):利用烛条形态和人工智能战胜市场 数据科学和机器学习(第 37 部分):利用烛条形态和人工智能战胜市场
蜡条形态有助于交易者理解市场心理,并辨别金融市场趋势,令交易决策更加明智,从而带来更佳成果。在本文中,将探讨如何利用蜡条形态与 AI 模型,达成最优交易绩效。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
在 MQL5 中构建自优化智能交易系统(第八部分):多策略分析 在 MQL5 中构建自优化智能交易系统(第八部分):多策略分析
如何才能最有效地整合多种策略,构建一个强大的策略组合?欢迎加入本次讨论,我们将探讨如何将三种不同的策略整合到我们的交易应用程序中。交易员通常会采用专门的策略来开仓和平仓。我们想探究的是,机器能否在这项任务上表现得比人类更出色。我们将首先从熟悉策略测试器的各项功能开始讨论,以及完成此任务所需的面向对象编程(OOP)原则。