English Русский Español Deutsch 日本語 Português
preview
精通日志记录(第四部分):将日志保存到文件

精通日志记录(第四部分):将日志保存到文件

MetaTrader 5示例 |
295 1
joaopedrodev
joaopedrodev

引言

在本系列的第一篇文章《精通日志记录(第一部分):MQL5 中的基础概念与初步实践》中,我们开始为智能交易系统(EA)开发创建一个自定义日志库。在文章中,我们探讨了创建这样一个关键工具的动机:克服 MetaTrader 5 原生日志的局限性,为 MQL5 生态带来一个强大、可定制且功能丰富的解决方案。

回顾我们涵盖的主要内容,我们通过确立以下几个基本要求,为我们的库奠定了基础:

  1. 稳健的结构,采用单例模式,确保代码组件之间的一致性。
  2. 高级持久化,将日志存储在数据库中,为深度审计和分析提供可追溯的历史记录。
  3. 输出灵活性,允许日志以方便的方式存储或显示,无论是在控制台、文件、终端还是数据库中。
  4. 按日志级别分类,区分信息性消息与关键警报和错误。
  5. 输出格式自定义,以满足每个开发者或项目的独特需求。

有了这样坚实的基础,显而易见,我们正在开发的日志框架将远不止是一个简单的事件记录器;它将成为一个战略工具,用于实时理解、监控和优化 EA 的行为。

到目前为止,我们已经探索了日志的基础知识,学习了如何格式化日志,并理解了处理器如何控制消息的目的地。但是,我们应该将这些日志存储在哪里以供将来参考呢?现在,在第四篇文章中,我们将更深入地研究将日志保存到文件的过程。让我们开始吧!


为何要将日志保存到文件?

将日志保存到文件是任何重视健壮性和高效维护的系统的基本能力。想象一下以下场景:您的智能交易顾问已经运行了数天,突然发生了一个意外错误。您要如何理解发生了什么?如果没有永久记录,那就像试图在没有所有拼图块的情况下解决一个谜题。

日志文件不仅仅是存储消息的一种方式。它们代表了系统的记忆。以下是采用它们的主要原因:

  1. 持久性与历史记录

    保存到文件的日志在程序运行结束后仍然可用。这允许进行历史查询,以检查性能、了解过去的行为并识别随时间变化的模式。

  2. 审计与透明度

    在关键项目中,例如在金融市场,保留详细的历史记录对于审计或为自动化决策提供合理解释至关重要。在被质疑时,一个妥善存储的日志可能是您最有力的辩护。

  3. 诊断与调试

    通过日志文件,您可以跟踪特定错误、监控关键事件,并分析系统执行的每一步。

  4. 访问灵活性

    与显示在控制台或终端上的日志不同,文件可以被远程访问或与团队共享,从而生成对重要事件的共享且详细的视图。

  5. 自动化与集成

    文件可以被自动化工具读取和分析,针对关键问题发送警报,或创建关于性能的详细报告。

通过将日志保存到文件,您将一个简单的资源转变为一个用于管理、跟踪和改进的工具。在此,我无需赘述将此数据保存到文件的重要性;让我们在下一个主题中直奔主题,了解如何在我们的库中高效地实现这一功能。

在直接进入代码之前,定义文件处理器应提供的功能非常重要。下面,我详细说明了每个必要的需求:

  • 目录、名称和文件类型的自定义

    允许用户配置:

    • 日志将要存储的目录。
    • 日志文件的名称,以确保更好的控制和组织。
    • 输出文件格式,支持 .log、.txt 和 .json。
  • 编码配置

    支持日志文件的不同编码类型,例如:

    • UTF-8(推荐标准)。
    • UTF-7、ANSI 代码页 (ACP) 或其他,根据需要。
  • 错误报告库

    库必须包含一个用于识别和报告其自身执行错误的系统:

    • 直接在终端控制台中显示的错误消息。
    • 清晰的信息,以方便诊断和解决问题。


在 MQL5 中处理文件

在 MQL5 中,处理文件需要对该语言如何处理这些操作有基本的了解。如果您想真正深入研究读取、写入和使用标志的复杂操作,我强烈推荐阅读由Dmitry Fedoseev撰写的文章MQL5 编程基础:文件。它对该主题提供了完整而详细的概述,能将复杂性变得清晰,同时又不失深度。

但我们在这里寻求的是一种更直接、更客观的东西。我们不会迷失在细枝末节中,因为我的任务是教您核心内容:以简单实用的方式打开、操作和关闭文件。

  1. 理解 MQL5 中的文件目录,所有由标准函数处理的文件都会自动存储在 MQL5/Files 文件夹中,该文件夹位于终端安装目录内。这意味着,在 MQL5 中处理文件时,您只需要指定从此基础文件夹开始的相对路径,而无需包含完整路径。例如,当保存到 logs/expert.log 时,完整路径将是:

    <terminal folder>/MQL5/Files/logs/expert.log

  1. 创建和打开文件。用于打开或创建文件的函数是FileOpen。它需要一个必填参数,即文件路径(在 MQL5/Files 之后),以及一些用于确定文件处理方式的标志。我们将使用的标志如下:

    • FILE_READ:允许以读取模式打开文件。
    • FILE_WRITE:允许以写入模式打开文件。
    • FILE_ANSI:指定内容将以 ANSI 格式的字符串写入(每个字符占用一个字节)。

    MQL5 的一个有用特性是,当同时使用 FILE_READ 和 FILE_WRITE 时,如果文件不存在,它会自动创建该文件。这样就无需手动检查文件是否存在。

  2. 关闭文件。最后,当您完成对文件的操作后,请使用 FileClose() 函数来关闭文件处理进程。

以下是一个在MQL5中打开(或创建)及关闭文件的实际例子:

int OnInit()
  {
   //--- Open the file and store the handler
   int handle_file = FileOpen("logs\\expert.log", FILE_READ|FILE_WRITE|FILE_ANSI, '\t', CP_UTF8);
   
   //--- If opening fails, display an error in the terminal log
   if(handle_file == INVALID_HANDLE)
     {
      Print("[ERROR] Unable to open log file. 确保文件路径存在并可写。(Code: "+IntegerToString(GetLastError())+")");
      return(INIT_FAILED);
     }
   
   //--- Close file
   FileClose(handle_file);
   
   return(INIT_SUCCEEDED);
  }

既然我们已经打开了文件,现在就来学习如何向其中写入数据。

  1. 定位写入指针:在写入之前,我们需要定义数据将要插入的位置。我们使用 FileSeek() 函数将写入指针定位到文件末尾。这样可以避免覆盖现有内容。
  2. 写入数据:FileWrite() 方法将字符串写入文件。无需使用 “\n” 来换行。使用此方法时,下次写入数据会自动换到新的一行,以确保更好的组织性。

以下是实际操作:

int OnInit()
  {
   //--- Open the file and store the handler
   int handle_file = FileOpen("logs\\expert.log", FILE_READ|FILE_WRITE|FILE_ANSI, '\t', CP_UTF8);
   
   //--- If opening fails, display an error in the terminal log
   if(handle_file == INVALID_HANDLE)
     {
      Print("[ERROR] Unable to open log file. 确保文件路径存在并可写。(Code: "+IntegerToString(GetLastError())+")");
      return(INIT_FAILED);
     }
   
   //--- Move the writing pointer
   FileSeek(handle_file, 0, SEEK_END);
   
   //--- Writes the content inside the file
   FileWrite(handle_file, "[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms");
   
   //--- Close file
   FileClose(handle_file);
   
   return(INIT_SUCCEEDED);
  }

运行代码后,您将在 Files 文件夹中看到一个创建的文件。完整路径将类似于:

<Terminal folder>/MQL5/Files/logs/expert.log

如果您打开该文件,将看到我们写入的内容:

[2025-01-02 12:35:27] DEBUG (CTradeManager): 订单发送成功,服务器响应时间为 32毫秒

现在我们已经学会了如何在 MQL5 中以非常简单的方式处理文件,接下来让我们将这项工作添加到负责保存文件的处理器类 CLogifyHandlerFile 中。


创建 CLogifyHandlerFile 类的配置

现在,让我们详细说明如何配置此类,以高效地处理我在需求部分提到的日志轮换。但“日志轮换”究竟是什么意思呢?让我更详细地解释一下。轮换是一项必不可少的实践,它可以避免单个日志文件无限增长而导致的那种混乱场景,防止数据在单个日志文件中过度累积,这会使后续分析变得困难,将日志变成一个真正缓慢、难以处理且几乎无法解读的噩梦。

设想这样一个场景:一个EA运行了数周或数月,将每个事件、错误或通知都记录在同一个文件中。很快,该日志就开始达到相当大的规模,使得读取和解释信息变得相当复杂。这时轮换就派上用场了。它允许我们将这些信息分割成更小、更有组织的部分,使一切更易于阅读和分析。

两种最常见的方式是:

  1. 按大小:您为日志文件设置一个大小限制,通常以兆字节(MB)为单位。当达到此限制时,会自动创建一个新文件,并重新开始循环。当重点在于控制日志增长,而不必遵循日历时,这种方法非常实用。一旦当前文件达到大小限制(以 MB 为单位),就会发生以下流程:当前的日志文件被重命名,并获得一个索引,例如 “log1.log”。目录中的现有文件也会被重新编号,例如 “log1.log” 变成 “log2.log”。如果文件数量达到最大允许值,最旧的文件将被删除。这种方法对于限制日志占用的空间和保存的文件数量都很有用。
  2. 按日期:在这种情况下,每天都会创建一个新的日志文件。每个文件的名称中都包含其创建日期,例如 log_2025-01-19.log ,这解决了日志组织的大部分难题。当您需要查看特定某一天的情况,而不想迷失在一个巨大的文件中时,这种方法非常完美。这是我在保存智能顾问日志时最常用的方法,一切都更干净、更直接、更易于浏览。

此外,您还可以限制存储的日志文件数量。此控制非常重要,可以防止不必要的旧日志累积。想象一下,如果您设置为保留最近 30 个文件,当第 31 个文件出现时,系统会自动丢弃最旧的那个,这可以防止磁盘上累积非常旧的日志,同时保留最新的日志。

另一个关键细节是缓存的使用。它不是在每条消息到达时都直接写入文件,而是将消息临时存储在缓存中。当缓存达到设定的限制时,它会一次性将所有内容转储到文件中。这会减少对磁盘的读写操作,提高性能,并延长您的存储设备的使用寿命。

既然我们理解了日志轮换的概念,那么让我们创建一个名为 MqlLogifyHandleFileConfig 的结构体来存储 CLogifyHandlerFile 类的所有配置。该结构体将负责保存定义日志管理方式的参数。

该结构体的第一部分将涉及为轮换类型和要使用的文件扩展名定义枚举

//+------------------------------------------------------------------+
//| ENUMS for log rotation and file extension                        |
//+------------------------------------------------------------------+
enum ENUM_LOG_ROTATION_MODE
  {
   LOG_ROTATION_MODE_NONE = 0,       // No rotation
   LOG_ROTATION_MODE_DATE,           // Rotate based on date
   LOG_ROTATION_MODE_SIZE,           // Rotate based on file size
  };
enum ENUM_LOG_FILE_EXTENSION
  {
   LOG_FILE_EXTENSION_TXT = 0,       // .txt file
   LOG_FILE_EXTENSION_LOG,           // .log file
   LOG_FILE_EXTENSION_JSON,          // .json file
  };

MqlLogifyHandleFileConfig 结构体本身将包含以下参数:

  • directory: 日志文件将要存储的目录。
  • base_filename: 基础文件名,不含扩展名。
  • file_extension: 日志文件扩展名类型(如 .txt、.log 或 .json)。
  • rotation_mode: 文件轮换模式。
  • messages_per_flush: 在将日志消息写入文件前需要缓存的条数。
  • codepage: 日志文件使用的编码(如 UTF-8 或 ANSI)。
  • max_file_size_mb: 如果轮换基于大小,则为每个日志文件的最大大小(以 MB 为单位)。
  • max_file_count: 在删除最旧的文件之前,要保留的最大日志文件数量。

除了构造函数和析构函数外,我还将在该结构体中添加辅助方法来配置每种轮换模式,旨在使配置过程更实用,最重要的是,更可靠。这些方法不仅仅是为了代码的优雅,它们的存在是为了确保在配置过程中不会遗漏任何关键细节。

例如,如果轮换模式设置为按日期(LOG_ROTATION_MODE_DATE),那么尝试配置 max_file_size_mb 属性就毫无意义,毕竟,这个参数只在按大小模式(LOG_ROTATION_MODE_SIZE)下才有意义。这些方法的作用就是避免此类不一致性,保护系统免受无效配置的影响。

如果偶然有某个必要参数未被指定,系统将采取行动。它可以自动填充一个默认值,并向开发者发出警告,从而确保流程的健壮性,不留任何令人不快的意外。

我们将要实现的辅助方法有:

  • CreateNoRotationConfig(): 不进行文件轮换的配置(所有日志都写入同一个文件,不进行轮换)。
  • CreateDateRotationConfig(): 基于日期进行轮换的配置。
  • CreateSizeRotationConfig(): 基于文件大小进行轮换的配置。
  • ValidateConfig(): 验证所有配置是否正确并可供使用的方法。(这是一个将由类自动使用的方法,而不是由使用该库的开发者调用)

以下是该结构体的完整实现:

//+------------------------------------------------------------------+
//| Struct: MqlLogifyHandleFileConfig                                |
//+------------------------------------------------------------------+
struct MqlLogifyHandleFileConfig
  {
   string directory;                         // Directory for log files
   string base_filename;                     // Base file name
   ENUM_LOG_FILE_EXTENSION file_extension;   // File extension type
   ENUM_LOG_ROTATION_MODE rotation_mode;     // Rotation mode
   int messages_per_flush;                   // Messages before flushing
   uint codepage;                            // Encoding (e.g., UTF-8, ANSI)
   ulong max_file_size_mb;                   // Max file size in MB for rotation
   int max_file_count;                       // Max number of files before deletion
   
   //--- Default constructor
   MqlLogifyHandleFileConfig(void)
     {
      directory = "logs";                    // Default directory
      base_filename = "expert";              // Default base name
      file_extension = LOG_FILE_EXTENSION_LOG;// Default to .log extension
      rotation_mode = LOG_ROTATION_MODE_SIZE;// Default size-based rotation
      messages_per_flush = 100;              // Default flush threshold
      codepage = CP_UTF8;                    // Default UTF-8 encoding
      max_file_size_mb = 5;                  // Default max file size in MB
      max_file_count = 10;                   // Default max file count
     }

   //--- Destructor
   ~MqlLogifyHandleFileConfig(void)
     {
     }

   //--- Create configuration for no rotation
   void CreateNoRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, int msg_per_flush=100, uint cp=CP_UTF8)
     {
      directory = dir;
      base_filename = base_name;
      file_extension = extension;
      rotation_mode = LOG_ROTATION_MODE_NONE;
      messages_per_flush = msg_per_flush;
      codepage = cp;
     }

   //--- Create configuration for date-based rotation
   void CreateDateRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, int max_files=10, int msg_per_flush=100, uint cp=CP_UTF8)
     {
      directory = dir;
      base_filename = base_name;
      file_extension = extension;
      rotation_mode = LOG_ROTATION_MODE_DATE;
      messages_per_flush = msg_per_flush;
      codepage = cp;
      max_file_count = max_files;
     }

   //--- Create configuration for size-based rotation
   void CreateSizeRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, ulong max_size=5, int max_files=10, int msg_per_flush=100, uint cp=CP_UTF8)
     {
      directory = dir;
      base_filename = base_name;
      file_extension = extension;
      rotation_mode = LOG_ROTATION_MODE_SIZE;
      messages_per_flush = msg_per_flush;
      codepage = cp;
      max_file_size_mb = max_size;
      max_file_count = max_files;
     }
   
   //--- Validate configuration
   bool ValidateConfig(string &error_message)
     {
      //--- Saves the return value
      bool is_valid = true;
      
      //--- Check if the directory is not empty
      if(directory == "")
        {
         directory = "logs";
         error_message = "The directory cannot be empty.";
         is_valid = false;
        }
      
      //--- 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;
        }
      
      //--- Check if the number of messages per flush is positive
      if(messages_per_flush <= 0)
        {
         messages_per_flush = 100;
         error_message = "The number of messages per flush must be greater than zero.";
         is_valid = false;
        }
      
      //--- Check if the codepage is valid (verify against expected values)
      if(codepage != CP_ACP
      && codepage != CP_MACCP
      && codepage != CP_OEMCP
      && codepage != CP_SYMBOL
      && codepage != CP_THREAD_ACP
      && codepage != CP_UTF7
      && codepage != CP_UTF8)
        {
         codepage = CP_UTF8;
         error_message = "The specified codepage is invalid.";
         is_valid = false;
        }
      
      //--- Validate limits for size-based rotation
      if(rotation_mode == LOG_ROTATION_MODE_SIZE)
        {
         if(max_file_size_mb <= 0)
           {
            max_file_size_mb = 5;
            error_message = "The maximum file size (in MB) must be greater than zero.";
            is_valid = false;
           }
         if(max_file_count <= 0)
           {
            max_file_count = 10;
            error_message = "The maximum number of files must be greater than zero.";
            is_valid = false;
           }
        }
      
      //--- Validate limits for date-based rotation
      if(rotation_mode == LOG_ROTATION_MODE_DATE)
        {
         if(max_file_count <= 0)
           {
            max_file_count = 10;
            error_message = "The maximum number of files for date-based rotation must be greater than zero.";
            is_valid = false;
           }
        }
   
      //--- No errors found
      error_message = "";
      return(is_valid);
     }
  };
//+------------------------------------------------------------------+

我想在此强调一个有趣的细节,那就是 ValidateConfig() 函数的工作方式。在分析这个函数时,你会注意到一个有趣的现象:当它检测到某个配置值存在错误时,它并不会立即返回 false 来表示出错了。它会先采取行动,采取纠正措施来自动解决问题,所有这些都在它返回最终结果之前完成。

首先,它会重置无效值,将其恢复为默认值。这在某种程度上临时“修复”了配置,不让错误阻碍程序的正常流程。然后,为了不让情况不明不白,该函数会分配一条详细消息,明确指出错误发生的位置以及需要调整的内容。最后,该函数会将名为 is_valid 的变量标记为 false,表示有错误发生。只有在采取了所有这些措施之后,它才会返回这个带有最终状态的变量,该变量将表明配置是否通过验证且有效。

但更有趣的是该函数如何处理多个错误。如果同时存在不止一个不正确的值,它不会只专注于纠正第一个出现的错误,而把其他的留到以后处理。恰恰相反,它会一次性处理所有错误,同时纠正所有问题。最后,该函数会返回一条消息,解释最后一个被纠正的错误,确保没有任何遗漏。

这种方法非常宝贵,有助于开发者的工作。在系统开发过程中,一些值被错误地或无意中定义是很常见的。这里的妙处在于,该函数有额外的安全层,能自动纠正错误,而无需等待程序员逐一发现它们。毕竟,小错误如果放任不管,可能会导致更大的故障——例如,日志记录保存失败。我所创建的这种错误处理自动化,最终可以防止小故障中断系统的运行,有助于保持一切正常运转。


实现 CLogifyHandlerFile 类

我们有在上一篇文章中已经创建的类,现在我们只需对其进行修改,使其具备功能。在这里,我将详细说明所做的每一项调整,以确保您理解所有工作原理。

在类的私有作用域中,我们添加了一些变量和一些重要的辅助方法:

  1. 配置:我们创建了一个 MqlLogifyHandleFileConfig 类型的变量 m_config,用于存储与日志系统相关的设置。
  2. 我还实现了 SetConfig() 和 GetConfig() 方法,用于定义和访问类的设置。

以下是该类的初始结构,包含基本定义和方法:

//+------------------------------------------------------------------+
//| class : CLogifyHandlerFile                                       |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : CLogifyHandlerFile                                 |
//| Heritage    : CLogifyHandler                                     |
//| Description : Log handler, inserts data into file, supports      |
//| rotation modes.                                                  |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandlerFile : public CLogifyHandler
  {
private:
   //--- Config
   MqlLogifyHandleFileConfig m_config;
   
public:
   //--- Configuration management
   void              SetConfig(MqlLogifyHandleFileConfig &config);
   MqlLogifyHandleFileConfig GetConfig(void);
  };
//+------------------------------------------------------------------+
//| Set configuration                                                |
//+------------------------------------------------------------------+
void CLogifyHandlerFile::SetConfig(MqlLogifyHandleFileConfig &config)
  {
   m_config = config;
   
   //--- Validade
   string err_msg = "";
   if(!m_config.ValidateConfig(err_msg))
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg);
     }
  }
//+------------------------------------------------------------------+
//| Get configuration                                                |
//+------------------------------------------------------------------+
MqlLogifyHandleFileConfig CLogifyHandlerFile::GetConfig(void)
  {
   return(m_config);
  }
//+------------------------------------------------------------------+

我将列出这些辅助方法,并更详细地解释它们的工作原理。我实现了三个有用的方法,它们将用于文件管理:

  1. LogFileExtensionToStr():将 ENUM_LOG_FILE_EXTENSION 枚举的值转换为表示文件扩展名的字符串。该枚举定义了文件类型的可能值,例如 .log、.txt 和 .json。

    //+------------------------------------------------------------------+
    //| Convert log file extension enum to string                        |
    //+------------------------------------------------------------------+
    string CLogifyHandlerFile::LogFileExtensionToStr(ENUM_LOG_FILE_EXTENSION file_extension)
      {
       switch(file_extension)
         {
          case LOG_FILE_EXTENSION_LOG:
            return(".log");
          case LOG_FILE_EXTENSION_TXT:
            return(".txt");
          case LOG_FILE_EXTENSION_JSON:
            return(".json");
         }
       //--- Default return
       return(".txt");
      }
    //+------------------------------------------------------------------+

  2. LogPath():此函数负责根据当前类的设置生成日志文件的完整路径。首先,它使用 LogFileExtensionToStr() 函数转换已配置的文件扩展名。然后,它检查已配置的轮换类型。如果轮换是基于文件大小或没有轮换,它仅返回已配置目录中的文件名。如果轮换是基于日期,它会在文件名中包含当前日期(YYYY-MM-DD 格式)作为前缀。

    //+------------------------------------------------------------------+
    //| Generate log file path based on config                           |
    //+------------------------------------------------------------------+
    string CLogifyHandlerFile::LogPath(void)
      {
       string file_extension = this.LogFileExtensionToStr(m_config.file_extension);
       string base_name = m_config.base_filename + file_extension;
       
       if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE || m_config.rotation_mode == LOG_ROTATION_MODE_NONE)
         {
          return(m_config.directory + "\\\\" + base_name);
         }
       else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE)
         {
          MqlDateTime date;
          TimeCurrent(date);
          string date_str = IntegerToString(date.year) + "-" + IntegerToString(date.mon, 2, '0') + "-" + IntegerToString(date.day, 2, '0');
          base_name = date_str + (m_config.base_filename != "" ? "-" + m_config.base_filename : "") + file_extension;
          return(m_config.directory + "\\\\" + base_name);
         }
       
       //--- Default return
       return(base_name);
      }
    //+------------------------------------------------------------------+

Emit()方法负责将日志消息记录到文件中。在当前代码中,它只是在终端控制台中显示日志。让我们来改进它,使其能够打开日志文件,添加一行包含格式化数据的新内容,并在写入后关闭文件。如果无法打开文件,将在控制台中显示一条错误消息。

void CLogifyHandlerFile::Emit(MqlLogifyModel &data)
  {
   //--- Checks if the configured level allows
   if(data.level >= this.GetLevel())
     {
      //--- Get the full path of the file
      string log_path = this.LogPath();
      
      //--- Open file
      ResetLastError();
                  int handle_file = FileOpen(log_path, FILE_READ|FILE_WRITE|FILE_ANSI, '\t', m_config.codepage);
                  if(handle_file == INVALID_HANDLE)
                    {
                     Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. 确保文件路径存在并可写。(Code: "+IntegerToString(GetLastError())+")");
                     return;
                    }
      
      //--- Write
      FileSeek(handle_file, 0, SEEK_END);
      FileWrite(handle_file, data.formated);
      
      //--- Close file
      FileClose(handle_file);
     }
  }

因此,我们已经有了将日志添加到文件的类中最简单的版本,让我们执行一些简单的测试来验证基本功能是否正常工作。


首次文件测试

我们将使用在前面示例中已经用过的测试文件 LogifyTest.mqh。目标是配置日志系统,使其使用 CLogify 基类和我们刚刚实现的文件处理器来将记录保存到文件中。

  1. 我们创建一个 MqlLogifyHandleFileConfig 类型的变量来存储文件处理器的特定设置。
  2. 我们配置处理器以使用所需的格式和规则,例如按大小进行文件轮换。
  3. 我们将此处理器添加到 CLogify 基类中。
  4. 我们配置一个格式化器,以确定每条记录在文件中的显示方式。

完整代码如下:

//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleFileConfig m_config;
   m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,10);
   
   //--- Handler File
   CLogifyHandlerFile *handler_file = new CLogifyHandlerFile();
   handler_file.SetConfig(m_config);
   handler_file.SetLevel(LOG_LEVEL_DEBUG);
   
   //--- Add handler in base class
   logify.AddHandler(handler_file);
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Using logs
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678");
   logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance");
   logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

当您运行上述代码时,将在配置的目录(logs)中创建一个新的日志文件。您可以在文件浏览器中查看它。

当在记事本或任何文本编辑器中打开该文件时,我们将看到由消息测试生成的内容:

在进行改进之前,我将进行一次性能测试,以便了解这能在多大程度上提升性能,这样我们就有了一个可供日后比较的参考。在 OnTick() 函数内部,我将向日志添加一条记录,这样每当有新的 tick 到来时,日志文件就会被打开、写入和关闭。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Logs
   logify.Debug("Debug Message");
  }
//+------------------------------------------------------------------+

我将使用策略测试器来执行此测试,即使在回测中,文件创建系统也能正常工作,但文件会保存在另一个文件夹中,稍后我将展示如何访问它。测试将使用以下设置进行:

考虑到“OHLC for 1 minute”(1分钟OHLC)建模,在 EURUSD 品种上进行 7 天的测试,考虑到在每个 tick 都会生成一条新的日志记录并立即保存到文件,测试耗时 5 分 11 秒完成。


使用 JSON 文件进行测试

最后,我想展示一下 JSON 日志文件的实际应用,因为它们在某些特定场景下可能很有用。要保存为 JSON,只需更改设置中的文件类型,并为 JSON 格式定义一个有效的格式化器,这是一个实现示例:

//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleFileConfig m_config;
   m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,5,5,10);
   
   //--- Handler File
   CLogifyHandlerFile *handler_file = new CLogifyHandlerFile();
   handler_file.SetConfig(m_config);
   handler_file.SetLevel(LOG_LEVEL_DEBUG);
   
   //--- Add handler in base class
   logify.AddHandler(handler_file);
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\\"datetime\\":\\"{date_time}\\", \\"level\\":\\"{levelname}\\", \\"msg\\":\\"{msg}\\"}"));
   
   //--- Using logs
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678");
   logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance");
   logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

使用相同的日志消息,这是在图表上运行EA后生成的文件结果:

{"datetime":"08:24:10", "level":"DEBUG", "msg":"RSI indicator value calculated: 72.56"}
{"datetime":"08:24:10", "level":"INFOR", "msg":"Buy order sent successfully"}
{"datetime":"08:24:10", "level":"ALERT", "msg":"Stop Loss adjusted to breakeven level"}
{"datetime":"08:24:10", "level":"ERROR", "msg":"Failed to send sell order"}
{"datetime":"08:24:10", "level":"FATAL", "msg":"Failed to initialize EA: Invalid settings"}


结论

在本文中,我们提供了一份实用且详细的指南,介绍了如何执行基本的文件操作:以简单的方式打开、操作内容,并最终关闭文件。我还讨论了配置“处理器”结构的重要性,通过这种配置,可以调整多个特性,例如要使用的文件类型(如文本、日志甚至 json)以及文件将要保存的目录,这使得该库非常灵活。

此外,我们对名为 CLogifyHandlerFile 的类进行了特定的改进。这些改动使得将每条消息直接记录到日志文件中成为可能。在完成此实现后,作为研究的一部分,我还进行了一项性能测试来衡量该解决方案的效率。我们使用了一个特定场景,在该场景中,系统模拟了在 EURUSD 资产上执行一周的交易策略。在此测试期间,为每个新的市场“tick”(报价)生成了一个日志记录。这个过程是极其密集的,因为资产价格的每一次变化都需要在文件中保存一个新行。

最终结果被记录下来:整个过程耗时 5 分 11 秒。这个结果将作为下一篇文章的参考基准,届时我们将实现一个缓存系统(临时内存)。缓存的目的是临时存储记录,从而消除持续访问文件的需要,以提升整体性能。

请继续关注下一篇文章,届时我们将探索更先进的技术来提高系统的效率和性能。期待与您再见!

文件名
说明
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/16986

附加的文件 |
Logify0Part40.zip (14.21 KB)
最近评论 | 前往讨论 (1)
Alpha Dolcy
Alpha Dolcy | 29 1月 2025 在 13:29
听起来是个值得追求的目标,尤其是在回溯测试和优化方面。
从基础到中级:定义(二) 从基础到中级:定义(二)
在本文中,我们将继续了解 #define 指令,但这次我们将重点关注它的第二种使用形式,即创建宏。由于这个主题可能有点复杂,我们决定使用我们已经研究了一段时间的应用程序。希望您喜欢今天的文章。
从基础到中级:定义(一) 从基础到中级:定义(一)
在这篇文章中,我们将做一些许多人会觉得奇怪和完全脱离上下文的事情,但如果使用得当,这将使你的学习更加有趣:我们将能够根据这里显示的内容构建非常有趣的东西。这将使您更好地理解 MQL5 语言的语法。此处提供的材料仅用于教育目的。它不应以任何方式被视为最终应用程序。其目的不是探索所提出的概念。
逆公允价值缺口(IFVG)交易策略 逆公允价值缺口(IFVG)交易策略
当价格回到先前确定的公允价值缺口位置,且未表现出预期的支撑或阻力反应,而是无视该缺口时,便出现了逆公允价值缺口(IFVG)。这种“无视”现象可能预示着市场方向的潜在转变,并为反向交易提供优势。在本文中,我将介绍自己开发的量化方法,以及如何将IFVG作为一种策略,应用于MetaTrader 5智能交易系统(EA)中。
黑洞算法(BHA) 黑洞算法(BHA)
黑洞算法(BHA)利用黑洞引力原理来优化解。在本文中,我们将考察 BHA 如何在避免局部极端情况的同时,吸引最佳解,以及为什么该算法已成为解决复杂问题的强大工具。学习简单的思路如何在优化世界带来令人印象深刻的结果。