
《精通日志记录(第二部分):格式化日志》
引言
在本系列的第一篇文章《精通日志记录(第一部分):MQL5 中的基础概念与初步实践》中,我们开始为智能交易系统(EA)开发创建一个自定义日志库。在文章中,我们探讨了创建这样一个关键工具的动机:克服 MetaTrader 5 原生日志的局限性,为 MQL5 生态带来一个强大、可定制且功能丰富的解决方案。
回顾我们涵盖的主要内容,我们通过确立以下几个基本要求,为我们的库奠定了基础:
- 稳健的结构,采用单例模式,确保代码组件之间的一致性。
- 高级持久化,将日志存储在数据库中,为深度审计和分析提供可追溯的历史记录。
- 输出灵活性,允许日志以方便的方式存储或显示,无论是在控制台、文件、终端还是数据库中。
- 按日志级别分类,区分信息性消息与关键警报和错误。
- 输出格式自定义,以满足每个开发者或项目的独特需求。
有了这样坚实的基础,显而易见,我们正在开发的日志框架将远不止是一个简单的事件记录器;它将成为一个战略工具,用于实时理解、监控和优化 EA 的行为。
现在,在第二篇文章中,我们将深入探讨该库最相关的功能之一:日志格式化。毕竟,一个高效的日志不仅关乎记录了什么,更关乎信息如何呈现。想象一下,在重要的 EA 测试过程中,接收到混乱或格式不佳的消息。这不仅会使分析变得不必要地复杂,还可能导致有价值的信息丢失。我们希望每一条日志都具备必要的清晰度和细节,同时仍能根据开发者的需求进行定制。
什么是日志格式?
日志格式是一种结构,用于以清晰易懂的方式组织程序执行过程中记录的信息。它是一种标准,定义了每条记录将如何显示,并汇集诸如事件严重级别、发生日期和时间、日志来源以及相关的描述性消息等关键数据。
这种组织对于使日志可读且有用至关重要。没有格式,日志可能显得混乱且缺乏上下文,使分析变得困难。例如,想象一条非结构化的日志,它只是简单地说:
DEBUG: 订单发送成功,服务器响应时间为 32毫秒
现在,将其与遵循结构化格式的日志进行比较,后者不仅呈现相同的信息,还提供了上下文: [2025-01-02 12:35:27] DEBUG (CTradeManager): 订单发送成功,服务器响应时间为 32毫秒
这种微小的差异在诊断问题时可能会产生巨大的影响,尤其是在复杂系统中。格式将原始数据转化为连贯的叙述,引导开发者理解系统的行为。
灵活性也是日志格式的一个关键方面。每个项目都有特定的需求,能够调整格式可以实现有用的自定义,例如突出关键事件、跟踪来源或用上下文元数据丰富消息。
格式化器的基本结构
格式化器的基本结构基于“占位符”的概念,这些元素充当模板中的替换点。这些占位符定义了日志中的信息将如何以及在哪里插入到最终消息中。
您可以将格式化器想象为一台机器,它将日志事件中的原始数据转换为可读、可定制的格式。输入可以是一个包含严重级别、消息、时间和其他详细值的数据模型。然后,格式化器将这些值应用于用户提供的模板,从而生成一条格式化的日志。
例如,考虑这样一个模板:
{date_time} {levelname}: {msg}当相应的值替换占位符后,输出将类似于:
12/04/2025 00:25:45 DEBUG: IFR 指标已成功插入图表!
格式化器的强大之处在于其灵活性。以下是我们将添加到库中的一些占位符示例:
- {levelname}: 以人类可读的方式表示日志级别(例如 DEBUG、ERROR、FATAL)。
- {msg}:描述所记录事件的消息。
- {args}:为消息提供上下文的附加数据,通常用于捕获动态信息。
- {timestamp}:以毫秒为单位的时间戳,用于精确性分析。
- {date_time}:时间戳的人类可读版本,以人类标准格式显示日期和时间。
- {origin}:指示事件的来源,例如发出日志的模块或类。
- {filename}, {function} and {line}:标识生成日志的确切代码位置,使调试更高效。
格式化器背后的逻辑简单而强大。它允许您为每条日志消息创建自定义框架,为开发者提供一种针对每种情况呈现最相关信息的方式。这种方法在数据量可能庞大且需要快速分析的项目中尤其有用。
借助可定制的模板和全面的占位符,该库将提供一个高度模块化的工具。这种模块化将确保您能够创建适合应用程序特定需求的日志,从而提高解释和使用所记录数据的效率。
在 MQL5 中实现格式化器
既然我们已经了解了格式和占位符是什么,那么让我们直接进入代码,了解这将如何在库的当前阶段实现。首先,在 <Include/Logify> 内创建一个名为“Formatter”的新文件夹。在此文件夹中,创建一个名为 LogifyFormatter.mqh 的文件。最终,路径将是 <Include/Logify/Formatter/LogifyFormatter.mqh>。请记住,我在文章末尾附上了本文使用的最终文件,您只需下载并使用即可。文件资源管理器应如下所示:
//+------------------------------------------------------------------+ //| LogifyFormatter.mqh | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../LogifyModel.mqh" //+------------------------------------------------------------------+ //| class : CLogifyFormatter | //| | //| [PROPERTY] | //| Name : CLogifyFormatter | //| Heritage : No heritage | //| Description : Class responsible for formatting the log into a | //| string, replacing placeholders with their respective values. | //| | //+------------------------------------------------------------------+ class CLogifyFormatter { public: CLogifyFormatter(void); ~CLogifyFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::CLogifyFormatter(void) { } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::~CLogifyFormatter(void) { } //+------------------------------------------------------------------+
我们声明存储数据和日志格式的私有属性
class CLogifyFormatter { private: //--- Stores the formats string m_date_format; string m_log_format; public: //--- Format query methods string GetDateFormat(void); string GetLogFormat(void); }; //+------------------------------------------------------------------+ //| Get date format | //+------------------------------------------------------------------+ string CLogifyFormatter::GetDateFormat(void) { return(m_date_format); } //+------------------------------------------------------------------+ //| Get the log format | //+------------------------------------------------------------------+ string CLogifyFormatter::GetLogFormat(void) { return(m_log_format); } //+------------------------------------------------------------------+
这里我们定义:
- m_date_format:定义日期的格式(例如:“yyyy/MM/dd hh:mm:ss”)
- m_log_format:定义日志的标准格式(例如:“{timestamp} - {msg}”)
- 此外还有两个方法用于访问这些私有属性。
构造函数负责初始化日期格式和日志格式,并通过我们稍后会看到的 CheckLogFormat 方法对日志格式进行验证。为此,我为构造函数添加了两个参数,以便在创建类的实例时就能直接定义格式。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::CLogifyFormatter(string date_formate,string log_format) { m_date_format = date_formate; if(CheckLogFormat(log_format)) { m_log_format = log_format; } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyFormatter::~CLogifyFormatter(void) { } //+------------------------------------------------------------------+
析构函数(~CLogifyFormatter)不执行任何特定操作,但声明它是一个良好的编程习惯,因为在未来的场景中可能会派上用场。
关于 CheckLogFormat 方法,我们现在就来实现它。它在验证用于格式化日志的模板方面扮演着至关重要的角色。其目的是确保模板中的所有占位符在被处理之前都结构正确且闭合。这类验证对于避免意外错误、确保日志输出的可靠性至关重要。让我们来看一些无效格式的示例,并理解其原因:
- 未闭合的占位符:例如 {timestamp} - {{msg}。这里多了一个 { 字符,未能正确闭合。这类错误表明结构不完整,可能导致日志处理失败。未开头的占位符:在诸如 {timestamp} - {msg}} 的情况下,存在一个多余的 } 字符,没有与之匹配的 {。与前一示例类似,这会导致日志结构不一致。
- 空的占位符: 模板 {timestamp} - {msg} {} 中包含一个空的占位符 {},没有关联的键。每个占位符都必须填入一个有效的引用,以便在运行时被替换,而空的占位符破坏了这一预期。
- 格式为空: 该方法也将提供的字符串完全空白的情形视为无效。要成为有效格式,字符串必须至少包含一个字符,作为格式化日志的基础。
当该方法检测到任何上述异常时,它会返回 false,并向开发者打印详细的错误信息。这些信息有助于快速定位并修正模板中的问题。反之,如果模板结构正确且符合所有规则,该方法将返回 true,表示该模板已准备就绪,可以使用。除了确保准确性,该方法还倡导了良好的日志模板设计实践,鼓励开发者创建清晰、一致的结构。这种方式不仅提升了日志的可读性,也使其在后续的维护和分析中更加便捷。
//+------------------------------------------------------------------+ //| Validate format | //+------------------------------------------------------------------+ bool CLogifyFormatter::CheckLogFormat(string log_format) { //--- Variables to track the most recent '{' opening index and the number of '{' brace openings int openIndex = -1; // Index of last '{' found int openBraces = 0; // '{' counter int len = StringLen(log_format); // String length //--- Checks if string is empty if(len == 0) { //--- Prints error message and returns false Print("Erro de formatação: sequência inesperada encontrada. Verifique o padrão de placeholders usado."); return(false); } //--- Iterate through each character of the string for(int i=0;i<len;i++) { //--- Gets the current character ushort character = StringGetCharacter(log_format,i); //--- Checks if the character is an opening '{' if(character == '{') { openBraces++; // Increments the opening counter '{' openIndex = i; // Updates the index of the last opening } //--- Checks if the character is a closing '}' else if(character == '}') { //--- If there is no matching '{' if(openBraces == 0) { //--- Prints error message and returns false Print("Erro de formatação: o caractere '}' na posição ",i," não possui um '{' correspondente."); return(false); } //--- Decreases the open count because a matching '{' was found openBraces--; //--- Extracts the contents of the placeholder (between '{' and '}') string placeHolder = StringSubstr(log_format, openIndex + 1, i - openIndex - 1); //--- Checks if placeholder is empty if(StringLen(placeHolder) == 0) { //--- Prints error message and returns false Print("Erro de formatação: placeholder vazio detectado na posição ",i,". Um nome é esperado dentro de '{...}'."); return(false); } } } //--- After traversing the entire string, check if there are still any unmatched '{'}' if(openBraces > 0) { //--- Prints error message indicating the index of the last opened '{' and returns false Print("Erro de formatação: o placeholder '{' na posição ",openIndex," não possui um '}' correspondente."); return(false); } //--- Format is correct return(true); } //+------------------------------------------------------------------+
现在我们来看类中负责格式化日志的两个主要方法:
- FormatDate():用于处理日期格式
- FormatLog():用于创建日志本身的格式
这两个方法在定制库所记录的数据方面发挥着核心作用。为了使这些方法具有可扩展性和灵活性,它们被声明为virtual。由于这些方法被声明为虚方法,FormatDate 可以在派生类中轻松重写,以适应特定场景。例如,自定义实现可以调整日期格式以包含时区或其他附加信息。这种灵活的架构使库能够随项目需求的发展而演进,确保其在各种场景下的适用性。
FormatDate 方法负责将一个 datetime 对象转换为格式化字符串,以适配 m_date_format 中定义的标准。该模式使用占位符系统,这些占位符会被动态替换为日期的相应元素,如年、月、日、时间等。
这种方法非常灵活,允许使用高度自定义的格式,以适应多种场景。例如,你可以选择只显示日和月,或者包含完整信息,如星期和时间。以下是可用的占位符:
- 年份
- yy→ 两位年份(例如 “25”)。
- yyyy → 四位年份(例如 “2025”)。
- 月份
- M → 不带前导零的月份(例如一月表示为 “1”)。
- MM → 两位月份(例如一月表示为 “01”)。
- MMM → 月份缩写(例如 “Jan”)。
- MMMM → 月份全称(例如 “January”)。
- 日
- d → 不带前导零的日(例如 “1”)。
- dd → 两位日(例如 “01”)。
- 一年中的第几天
- D → 不带前导零的年中日(例如 2 月 1 日为 “32”)。DDD → 三位数的年中日(例如 “032”)。
- 星期几
- E → 星期几的缩写(例如 “Mon”)。
- EEEE → 星期几的全称(例如 “Monday”)。
- 24 小时制小时
- H → 不带前导零的小时(例如 “9”)。
- HH → 两位小时(例如 “09”)。
- 12 小时制小时
- h → 不带前导零的小时(例如 “9”)。
- hh → 两位小时(例如 “09”)。
- 分钟
- m → 不带前导零的分钟(例如 “5”)。
- mm → 两位分钟(例如 “05”)。
- 秒
- s → 不带前导零的秒(例如 “9”)。
- ss → 两位秒(例如 “09”)。
- AM/PM
- tt → 返回小写字母(am/pm)
- TT → 返回大写字母(AM/PM)
这一逻辑使得根据需求自定义日期显示极为方便。例如,使用 “yyyy-MM-dd HH:mm:ss” 格式,输出结果类似于 “2025-01-02 14:30:00”。使用 “EEEE, MMM dd, yyyy” 格式,输出结果为 “Tuesday, Jul 30, 2019”。这种适应性对于提供既信息丰富又视觉清晰的日志至关重要。
FormatLog 背后的逻辑基于 MQL5 原生函数 StringReplace()。该函数执行直接的字符串替换,将所有特定子串的出现替换为另一个子串。在 FormatLog 方法的上下文中,诸如 {timestamp} 或 {message} 的占位符会被日志模型中的真实值替换。这将诸如 MqlLogifyModel 的模型实例转换为已准备好可视化的结构化数据。
以下是该类中的实现代码,我添加了多处注释以使代码尽可能具有教学意义:
//+------------------------------------------------------------------+ //| class : CLogifyFormatter | //| | //| [PROPERTY] | //| Name : CLogifyFormatter | //| Heritage : No heritage | //| Description : Class responsible for formatting the log into a | //| string, replacing placeholders with their respective values. | //| | //+------------------------------------------------------------------+ class CLogifyFormatter { //--- Date and log formatting methods virtual string FormatDate(datetime date); virtual string FormatLog(MqlLogifyModel &data); }; //+------------------------------------------------------------------+ //| Formats dates | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatDate(datetime date) { string formated = m_date_format; //--- Date and time structure MqlDateTime time; TimeToStruct(date, time); //--- Array with months and days of the week in string string months_abbr[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; string months_full[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; string day_of_week_abbr[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; string day_of_week_full[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; //--- Replace year StringReplace(formated, "yyyy", IntegerToString(time.year)); StringReplace(formated, "yy", IntegerToString(time.year % 100, 2, '0')); //--- Replace month if(StringFind(formated,"MMM") < 0 && StringFind(formated,"MMMM") < 0) { StringReplace(formated, "MM", IntegerToString(time.mon, 2, '0')); StringReplace(formated, "M", IntegerToString(time.mon)); } //--- Replace day StringReplace(formated, "dd", IntegerToString(time.day, 2, '0')); StringReplace(formated, "d", IntegerToString(time.day)); //--- Replace day of year StringReplace(formated, "DDD", IntegerToString(time.day_of_year, 3, '0')); StringReplace(formated, "D", IntegerToString(time.day_of_year)); //--- Replace Replace hours (24h and 12h) StringReplace(formated, "HH", IntegerToString(time.hour, 2, '0')); StringReplace(formated, "H", IntegerToString(time.hour)); int hour_12 = time.hour % 12; if (hour_12 == 0) hour_12 = 12; StringReplace(formated, "hh", IntegerToString(hour_12, 2, '0')); StringReplace(formated, "h", IntegerToString(hour_12)); //--- Replace minutes and seconds StringReplace(formated, "mm", IntegerToString(time.min, 2, '0')); StringReplace(formated, "m", IntegerToString(time.min)); StringReplace(formated, "ss", IntegerToString(time.sec, 2, '0')); StringReplace(formated, "s", IntegerToString(time.sec)); //--- Replace AM/PM bool is_am = (time.hour < 12); StringReplace(formated, "tt", is_am? "am" : "pm"); StringReplace(formated, "TT", is_am? "AM" : "PM"); //--- Replace month StringReplace(formated, "MMMM", months_full[time.mon - 1]); StringReplace(formated, "MMM", months_abbr[time.mon - 1]); //--- Replace day of week StringReplace(formated, "EEEE", day_of_week_full[time.day_of_week]); StringReplace(formated, "E", day_of_week_abbr[time.day_of_week]); return(formated); } //+------------------------------------------------------------------+ //| Format logs | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatLog(MqlLogifyModel &data) { string formated = m_log_format; //--- Replace placeholders StringReplace(formated,"{timestamp}",IntegerToString(data.timestamp)); StringReplace(formated,"{level}",IntegerToString(data.level)); StringReplace(formated,"{origin}",data.origin); StringReplace(formated,"{message}",data.message); StringReplace(formated,"{metadata}",data.metadata); return(formated); } //+------------------------------------------------------------------+
至此,类的构建已经完成,现在我们来对 MqlLogifyModel 进行一些更新。
为 MqlLogifyModel 添加更多数据
MqlLogifyModel 是我们日志库的核心元素之一,它代表用于存储和操作每个日志事件相关数据的基础结构。在当前状态下,该结构定义如下:struct MqlLogifyModel { ulong timestamp; // Date and time of the event ENUM_LOG_LEVEL level; // Severity level string origin; // Log source string message; // Log message string metadata; // Additional information in JSON or text MqlLogifyModel::MqlLogifyModel(void) { timestamp = 0; level = LOG_LEVEL_DEBUG; origin = ""; message = ""; metadata = ""; } MqlLogifyModel::MqlLogifyModel(ulong _timestamp,ENUM_LOG_LEVEL _level,string _origin,string _message,string _metadata) { timestamp = _timestamp; level = _level; origin = _origin; message = _message; metadata = _metadata; } };
这个初始版本在简单场景中表现良好,但我们可以通过添加更多信息并优化属性名称来显著改进它,使其更易于使用,并使模型符合最佳实践。下面,我们将讨论计划的改进内容。
属性名称简化- message 字段将被重命名为 msg。这一改动虽然细微,但在访问该属性时减少了字符数,使代码更易于编写和阅读。
- metadata 将被替换为 args,因为新名称更准确地反映了该属性的功能:存储日志生成时与之关联的上下文数据。
新增字段
为了丰富日志内容并支持更详细的分析,将添加以下字段:
- formatted:根据指定格式格式化后的日志消息。它将用于存储日志的最终版本,即已将占位符替换为真实值后的内容。此属性为只读。
- levelname:日志级别的文本名称(例如 “DEBUG”、“INFO”)。当格式中需要描述严重性级别时,此字段非常有用。
- date_time:以 datetime 格式表示事件的日期和时间,是 timestamp 的替代形式。
- filename:生成日志的文件名,对于追踪确切来源至关重要。
- function:调用日志的函数名称,提供有关事件来源的更多上下文。
- line:生成日志的源文件中的行号。这在调试场景中尤其有用。
这些新字段使 MqlLogifyModel 更加健壮,能够满足不同的日志记录需求,例如详细调试或与外部监控工具集成。修改后,代码如下所示:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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 MqlLogifyModel::MqlLogifyModel(void) { formated = ""; levelname = ""; msg = ""; args = ""; timestamp = 0; date_time = 0; level = LOG_LEVEL_DEBUG; origin = ""; filename = ""; function = ""; line = 0; } 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) { formated = _formated; levelname = _levelname; msg = _msg; args = _args; timestamp = _timestamp; date_time = _date_time; level = _level; origin = _origin; filename = _filename; function = _function; line = _line; } }; //+------------------------------------------------------------------+下一步,我们将更新 CLogifyFormatter 类中的 FormatLog 方法,以支持与新增属性对应的占位符。以下是该方法的更新版本,现已兼容所有新的模型属性:
//+------------------------------------------------------------------+ //| 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)); return(formated); } //+------------------------------------------------------------------+
在 FormatLog 方法中,{date_time} 的值会被 FormatDate 的格式化返回值替换,因此该占位符将被替换为之前传入的日期格式。
将格式化器应用于日志
现在我们来确保 CLogify 类能够使用格式化器来格式化日志消息。我们来导入负责此功能的类,并添加一个属性来存储它:
#include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh"
下一步是向 CLogify 类中添加 m_formatter 属性,用于存储格式化器实例,并创建配置和访问它的方法。这将使我们能够在系统的不同位置复用该格式化器:
//+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifyFormatter *m_formatter; public: //--- Get/Set object formatter void SetFormatter(CLogifyFormatter *format); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogify::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogify::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
现有的日志方法仅包含一组有限的参数,例如时间戳、日志级别、消息、来源和元数据。我们将通过添加描述日志上下文的新参数来改进这一方法,例如:
- filename:日志发生的文件名。
- function:日志发生的函数名。
- line:生成日志的代码行号。
此外,为使方法签名更加直观,我们还将调整参数名称。以下是主要方法(Append)被修改后的示例:
bool CLogify::Append(ulong timestamp, ENUM_LOG_LEVEL level, string message, string origin = "", string metadata = "");
调整后的方法
bool CLogify::Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0);
至此,我们的新实现如下:
//+------------------------------------------------------------------+ //| 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); //--- Printing the formatted log Print(m_formatter.FormatLog(data)); return(true); } //+------------------------------------------------------------------+
请注意,我正在打印 FormatLog 函数的返回值,即返回的数据对象。
现在,我们已经拥有了一个可使用详细参数的基础 Append 方法,接下来可以调整或为每个日志级别(Debug、Info、Alert、Error 和 Fatal)添加其他专门方法。这些方法是“快捷方式”,它们会自动设置日志级别,同时允许使用所有其他参数。
//+------------------------------------------------------------------+ //| Debug level message | //+------------------------------------------------------------------+ bool CLogify::Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_DEBUG,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Infor level message | //+------------------------------------------------------------------+ bool CLogify::Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_INFOR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Alert level message | //+------------------------------------------------------------------+ bool CLogify::Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ALERT,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Error level message | //+------------------------------------------------------------------+ bool CLogify::Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ERROR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Fatal level message | //+------------------------------------------------------------------+ bool CLogify::Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_FATAL,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+
实际例子
我将演示如何使用配置了自定义日志格式的 CLogify 类。我们将从基本示例开始,并逐步增加复杂性,以展示该解决方案的灵活性。
1. 日志的基本配置
在本示例中,我们将使用在第一篇文章中创建的测试文件 LogifyTest.mq5。初始配置包括创建一个 CLogify 实例,并定义日志消息的基本格式。代码如下:
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} => {msg}")); //--- Log a simple message logify.Debug("Application initialized successfully."); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
当代码执行时,生成的日志格式将如下所示:
[DEBUG] 07:25:32 => Application initialized successfully.
请注意,我们使用的是简化时间格式(hh:mm:ss),并且消息结构中显示了日志级别和当前时间。这是使用 CLogify 的最基本示例。
2. 添加来源标识的详细信息
现在,我们扩展示例,加入日志来源和额外参数等信息。这对于更复杂的系统非常有用,因为日志需要指明是系统的哪一部分生成了该消息。
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} {args}")); //--- Log a simple message logify.Debug("Connection established with the server.", "Network"); logify.Alert("Configuration file not found!", "Config", "Attempt 1 of 3"); return(INIT_SUCCEEDED); }此代码将生成如下输出:
[DEBUG] 07:26:18 (Network) => Connection established with the server. [ALERT] 07:26:19 (Config) => Configuration file not found!尝试次数:13次
在这里,我们添加了 origin 参数和上下文参数,以丰富日志消息的内容。
3. 使用高级元数据
在更健壮的系统中,通常需要识别生成日志的文件、函数和行号。让我们调整示例以包含这些信息:
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} (File: {filename}, Line: {line})")); //--- Log a simple message logify.Error("Error accessing database.", "Database", "", __FILE__, __FUNCTION__, __LINE__); return(INIT_SUCCEEDED); }执行上述代码后,我们得到:
[ERROR] 07:27:15 (Database) => Error accessing database. (File: LogifyTest.mq5, Line: 25)
现在我们拥有了详细信息,可以精确定位错误在代码中发生的位置。
4. 自定义格式与集成到大型系统中
作为最后一个示例,我们将展示如何自定义格式,并在模拟大型系统执行的循环中生成不同类型的日志消息:
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("yyyy.MM.dd hh:mm:ss","{date_time} [{levelname}] {msg} - {origin} ({filename}:{line})")); //--- Cycle simulating various system operations for(int i=0; i<3; i++) { logify.Debug("Operation in progress...", "TaskProcessor", "", __FILE__, __FUNCTION__, __LINE__); if(i == 1) { logify.Alert("Possible inconsistency detected.", "TaskValidator", "", __FILE__, __FUNCTION__, __LINE__); } if(i == 2) { logify.Fatal("Critical error, purchase order not executed correctly!", "Core", "", __FILE__, __FUNCTION__, __LINE__); } } return(INIT_SUCCEEDED); }执行后输出结果如下:
2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [ALERT] Possible inconsistency detected. - TaskValidator (LogifyTest.mq5:28) 2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [FATAL] Critical error, purchase order not executed correctly! - Core (LogifyTest.mq5:32)
此示例模拟了现实世界中的操作流程,消息会根据情况的严重性逐步升级。日志格式非常详细,展示了完整的时间、日志级别、消息以及代码中的位置。
结论
在本文中,我们全面探讨了如何利用 Logify 库的强大功能,在 MQL5 中自定义和应用日志格式。我们从概念入手,介绍了什么是日志格式,以及这一实践在应用程序监控与调试中的重要性。
接着,我们介绍了格式化器的基本结构,这是日志自定义的核心,并探讨了其在 MQL5 中的应用如何使日志更加有意义且易于访问。在此基础上,我们展示了如何实现这些格式化器,重点突出了在 MqlLogifyModel 模型中进行自定义和添加额外数据的可能性。
随后,我们研究了为日志添加更多上下文数据以提升信息量的过程,例如识别消息的确切来源(文件、函数和行号)。我们还讨论了如何配置和应用这些格式化器,以确保日志输出满足任何项目的特定需求。
最后,我们通过一个实际示例总结全文,展示了如何在日志中实现不同复杂度的层次,从基础配置到具有高级详细日志的健壮系统。该示例巩固了所学知识,将理论与 MQL5 系统开发中的实际应用相结合。
以下是当前阶段库的结构图:
本文中使用的所有代码已附在下方。以下是各库文件的说明表格:
文件名 | 说明 |
---|---|
Experts/Logify/LogiftTest.mq5 | 用于测试库功能的文件,包含一个实际示例 |
Include/Logify/Formatter/LogifyFormatter.mqh | 负责格式化日志记录的类,将占位符替换为具体值 |
Include/Logify/Logify.mqh | 日志管理的核心类,集成了级别、模型和格式化功能 |
Include/Logify/LogifyLevel.mqh | 定义 Logify 库日志级别的文件,支持精细控制 |
Include/Logify/LogifyModel.mqh | 用于建模日志记录的结构,包含级别、消息、时间戳和上下文等详细信息 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16833
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



