English Русский Español Deutsch 日本語 Português
preview
来自专业程序员的提示(第三部分):日志。 连接到 Seq 日志收集和分析系统

来自专业程序员的提示(第三部分):日志。 连接到 Seq 日志收集和分析系统

MetaTrader 5统计分析 | 9 五月 2022, 10:13
962 0
Malik Arykov
Malik Arykov

内容目录


概述

Logging 是用于分析应用程序操作的消息输出。 MQL5 的 Print 和 PrintFormat 函数将输出消息保存到智能系统栏流水账。 智能系统栏流水账是 Unicode 格式的文本文件。 每天都会创建新的 MQL5/Logs/yyyymmdd.log 文件,从而避免日志超载。

所有打开图表上的所有脚本和智能交易系统将“日志写入”一个文件。 一部分日志仍保留在磁盘缓存当中。 换句话说,如果从资源管理器中打开日志文件,不会看到其中的最新信息,因为它还位于缓存中。 若要强制终端将缓存保存到文件中,您应该关闭终端,或者使用“智能系统”选项卡的关联菜单,并选择其中的“打开”项。 这将打开一个包含日志文件的目录。

分析这些日志并不轻松,尤其是在终端中。 但这种分析非常重要。 在提示的第一部分中,我展示了简化终端日志中信息搜索、选择和查看的一种方式。 在本文中,我将向您展示如何:

  • 统一日志输出(Logger 类)
  • 把日志连接到 Seq 日志收集和分析系统
  • Seq 中在线查看消息(事件)
  • 把常规 MetaTrader 5 日志导入 Seq(Python 软件包)


Seq:日志收集和分析系统

Seq 是一个实时搜索和分析应用程序日志的服务器。 其设计优良的用户界面、JSON 格式的事件存储、和 SQL 查询语言支持,令其成为识别和诊断复杂应用程序和微服务中问题的有效平台。

若要向 Seq 发送消息,您需要:

  1. 在我们的计算机上安装 Seq
    安装后,Seq UI 将于以下位置提供:
    http://localhost:5341/#/events
  2. c:/windows/system32/drivers/etc/hosts 文件里添加以下行:
    127.0.0.1 seqlocal.net
    为了能够在 MetaTrader 5 终端设置里添加 URL
  3. 禁用 Seq 中的时区,“按原样”显示消息时间
    - 进入 UI Seq
    - 进入 admin/Preferences/Preferences
    - 启用 "Show timestamps in Coordinated Universal Time (UTC)"
  4. 将以下地址添加到 MT5/工具/选项/智能系统
    http://seqlocal.net
    允许 WebRequest 函数访问该 URL
若要在线观看消息(事件),您需要通过点击UI Seq 的 Tail 按钮,启用在线模式。


Logger 类

思路很简单:为了获得统一和结构化的信息,应该以相同的方式形成和显示。为此目的,我们将使用完全自主的 Logger 类。 即,它没有任何额外依赖的 # 包含文件项。 正因如此,这个类可以“开箱即用”。

// Message levels
#define LEV_DEBUG "DBG"   // debugging (for service use)
#define LEV_INFO "INF"    // information (to track the functions)
#define LEV_WARNING "WRN" // warning (attention)
#define LEV_ERROR "ERR"   // a non-critical error (check the log, work can be continued)
#define LEV_FATAL "FTL"   // fatal error (work cannot be continued)

消息级别可大致给出消息的严重性和紧迫性。 为了让这些级别在智能系统流水账中具有良好的可读性,把它们高亮显示并对齐,我用了三个字母的前缀:DBG、INF、WRN、ERR、FTL。 

  • DEBUG 是为程序员设计的,在许多日志系统中,它不会输出到控制台,而是保存到文件中。 DEBUG 消息的显示频率高于其它消息,通常包含带有参数的函数名和/或它们的调用结果。
  • INFO 是为用户设计的。 这些消息的出现频率低于 DEBUG 消息。 它们包含有关应用程序操作的信息。 例如,这些可以是用户操作,例如单击菜单项、业务结果等,即用户能够理解的一切。
  • WARNING 表示该信息应加以注意。 例如:成交开仓、或平仓、挂单触发、等等。 
  • ERROR 意味着这些是非严重错误,之后应用程序将继续运行。 例如,导致订单被拒绝或无法执行的无效价格、或止损价位。
  • FATAL 表示严重错误,之后应用程序无法保证在正常模式下能进一步操作。 您需要紧急停止应用程序、并修复错误。

为了可读性和减少代码,消息可通过以下宏替换输出

// Message output macros
#define LOG_SENDER gLog.SetSender(__FILE__, __FUNCTION__)
#define LOG_INFO(message) LOG_SENDER; gLog.Info(message)
#define LOG_DEBUG(message) LOG_SENDER; gLog.Debug(message)
#define LOG_WARNING(message) LOG_SENDER; gLog.Warning(message)
#define LOG_ERROR(message) LOG_SENDER; gLog.Error(message)
#define LOG_FATAL(message) LOG_SENDER; gLog.Fatal(message)

因此,每条消息都会显示文件或模块的名称、函数的名称、还有消息本身。 若要形成消息,我建议使用 PrintFormat 函数。 最好用子字符串 “/” 分隔每个数值。 这项技术会令所有消息统一且结构化。

操作符示例

LOG_INFO(m_result);
LOG_INFO(StringFormat("%s / %s / %s", StringSubstr(EnumToString(m_type), 3), 
    TimeToString(m_time0Bar), m_result));

操作员数据输出到智能系统日志

时间                     来源              消息
---------------------------------------------------------------------------------------------------------------------
2022.02.16 13:00:06.079  Cayman (GBPUSD,H1)  INF: AnalyserRollback::Run Rollback, H1, 12:00, R1, D1, RO, order 275667165
2022.02.16 13:00:06.080  Cayman (GBPUSD,H1)  INF: Analyser::SaveParameters Rollback / 2022.02.16 12:00 / Rollback, H1, 12:00, R1, D1, RO, order 275667165

MetaTrader 5 中打印的消息的具体特征是,时间列指定 TimeLocal,其信息实际上属于服务器时间 TimeCurrent,因此,如果需要强调时间,则应在消息本身中指定时间。 示例显示在第二条消息中,其中 13:00 是本地时间,12:00 是服务器时间(真实柱线的开盘时间)。

Logger 类具有以下结构

class Logger {
private:
    string  m_module;  // module or file name
    string  m_sender;  // function name
    string  m_level;   // message level
    string  m_message; // message text
    string  m_urlSeq;  // url of the Seq message service
    string  m_appName; // application name for Seq
    // private methods
    void Log(string level, string message);
    string TimeToStr(datetime value);
    string PeriodToStr(ENUM_TIMEFRAMES value);
    string Quote(string value);
    string Level();
    void SendToSeq();
public:
    Logger(string appName, string urlSeq);
    void SetSender(string module, string sender);
    void Debug(string message) { Log(LEV_DEBUG, message); };
    void Info(string message) { Log(LEV_INFO, message); };
    void Warning(string message) { Log(LEV_WARNING, message); };
    void Error(string message) { Log(LEV_ERROR, message); };
    void Fatal(string message) { Log(LEV_FATAL, message); };
};

extern Logger *gLog; // logger instance

所有内容都简明易懂,不包含任何不必要的细节。 请注意 gLog logger 实例的声明。 在同一项目的不同源文件中,可能存在具有相同类型和标识符,且声明为 “extern” 的变量。 外部变量只能被初始化一次。 因此,在任何项目文件中创建 logger 实例后,gLog 变量会指向同一个对象。

// -----------------------------------------------------------------------------
// Constructor
// -----------------------------------------------------------------------------
Logger::Logger(string appName, string urlSeq = "") {
    m_appName = appName;
    m_urlSeq = urlSeq;
}

logger 的构造函数接收两个参数:

  • appName - 调用 Seq 的应用程序名称。 Seq 系统能够以在线模式接收来自不同应用程序的日志。 appName 用于过滤消息。
  • urlSeq - Seq 服务的 URL。 它可以是监听特定端口的本地站点 (http://localhost:5341/#/events)。

urlSeq 参数是可选的。 如果未指定,则消息仅会输出到智能系统日志。 如果定义了 urlSeq,事件将通过 WebRequest 另行发往 Seq 服务。

// -----------------------------------------------------------------------------
// Set the message sender          
// -----------------------------------------------------------------------------
void Logger::SetSender(string module, string sender) { 
    m_module = module; // module or file name
    m_sender = sender; // function name
    StringReplace(m_module, ".mq5", "");
}

SetSender 函数获取两个必需的参数,并设置消息的发送者。 将从模块名中删除 “.mq5” 文件扩展名。 如果在类方法中用到日志操作符 LOG_LEVEL,那么类名将被添加到函数名称里,例如 TestClass::TestFunc。

// -----------------------------------------------------------------------------
// Convert time to the ISO8601 format for Seq
// -----------------------------------------------------------------------------
string Logger::TimeToStr(datetime value) {
    MqlDateTime mdt;
    TimeToStruct(value, mdt);
    ulong msec = GetTickCount64() % 1000; // for comparison
    return StringFormat("%4i-%02i-%02iT%02i:%02i:%02i.%03iZ", 
        mdt.year, mdt.mon, mdt.day, mdt.hour, mdt.min, mdt.sec, msec);
}

Seq 的时间类型必须为 ISO8601 格式 (YYYY-MM-DDThh:mm:ss[.SSS])。 MQL5 中的 datetime 类型在计算时最多精确到秒。 Seq 中的时间最多可精确到毫秒。 因此,自从系统启动(GetTickCount64)以来所经历的毫秒数会被强制添加到指定的时间。 此方法允许您比较消息相对于彼此的时间。

// -----------------------------------------------------------------------------
// Convert period to string
// -----------------------------------------------------------------------------
string Logger::PeriodToStr(ENUM_TIMEFRAMES value) {
    return StringSubstr(EnumToString(value), 7);
}

周期则以符号形式传递给 Seq。 任何周期的符号表示均以 “PERIOD_” 作为前缀。 因此,当把周期转换为字符串时,前缀会被简单地截断。 例如,PERIOD_H1 被转换为 “H1”。

SendToSeq 函数用于向 Seq 发送消息 (注册事件)

// -----------------------------------------------------------------------------
// Send message to Seq via http
// -----------------------------------------------------------------------------
void Logger::SendToSeq() {

    // replace illegal characters
    StringReplace(m_message, "\n", " ");
    StringReplace(m_message, "\t", " ");
    
    // prepare a string in the CLEF (Compact Logging Event Format) format
    string speriod = PeriodToStr(_Period);
    string extended_message = StringFormat("%s, %s / %s / %s / %s",
        _Symbol, speriod, m_module, m_sender, m_message);
    string clef = "{" +
        "\"@t\":" + Quote(TimeToStr(TimeCurrent())) + // event time
        ",\"AppName\":" + Quote(m_appName) +          // application name (Cayman)
        ",\"Symbol\":" + Quote(_Symbol) +             // symbol (EURUSD)
        ",\"Period\":" + Quote(speriod) +             // period (H4)
        ",\"Module\":" + Quote(m_module) +            // module name (__FILE__)
        ",\"Sender\":" + Quote(m_sender) +            // sender name (__FUNCTION__)
        ",\"Level\":" + Quote(m_level) +              // level abbreviation (INF)
        ",\"@l\":" + Quote(Level()) +                 // level details (Information)
        ",\"Message\":" + Quote(m_message) +          // message without additional info
        ",\"@m\":" + Quote(extended_message) +        // message with additional info
    "}";

    // prepare data for POST request
    char data[]; // HTTP message body data array
    char result[]; // Web service response data array
    string answer; // Web service response headers
    string headers = "Content-Type: application/vnd.serilog.clef\r\n";
    ArrayResize(data, StringToCharArray(clef, data, 0, WHOLE_ARRAY, CP_UTF8) - 1);

    // send message to Seq via http
    ResetLastError();
    int rcode = WebRequest("POST", m_urlSeq, headers, 3000, data, result, answer);
    if (rcode > 201) {
        PrintFormat("%s / rcode=%i / url=%s / answer=%s / %s", __FUNCTION__, 
            rcode, m_urlSeq, answer, CharArrayToString(result));
    }
}

首先,换行符和制表符被替换为空格。 然后形成一个 JSON 记录,其中消息参数为 “关键字”:“数值” 对。 带有“@前缀”的参数是必需的(服务),其余参数则是用户定义的。 名称和其编号则由程序员判定。 参数及其数值可用 SQL 进行查询。

请注意消息时间 @t = TimeCurrent()。 与终端相较,它固定为服务器时间,并非本地时间(TimeLocal())。 接下来,形成请求主体,然后通过 WebRequest 发送至 Seq 服务。

// -----------------------------------------------------------------------------
// Write a message to log
// -----------------------------------------------------------------------------
void Logger::Log(string level, string message) {
    
    m_level = level;
    m_message = message;
    
    // output a message to the expert log (Toolbox/Experts)
    PrintFormat("%s: %s %s", m_level, m_sender, m_message);
    
    // if a URL is defined, then send a message to Seq via http
    if (m_urlSeq != "") SendToSeq();
}

该函数有两个必需参数:消息严重性级别,和消息字符串。 该条消息被被打印到智能系统流水帐里。 级别后面跟着一个冒号字符。 这是专门为 Notepad++ 做的,用于高亮显示行(WRN:- 黄底黑字,ERR:- 红底黄字)。


测试 Logger 类

TestLogger.mq5 脚本用于测试该类。 Logging 宏定义用在各种函数里。 

#include <Cayman/Logger.mqh>

class TestClass {
    int m_id;
public:
    TestClass(int id) { 
        m_id = id;
        LOG_DEBUG(StringFormat("create object with id = %i", id));
    };
};

void TestFunc() {
    LOG_INFO("info message from inner function");
}

void OnStart() {

    string urlSeq = "http://seqlocal.net:5341/api/events/raw?clef";
    gLog = new Logger("TestLogger", urlSeq);
    
    LOG_DEBUG("debug message");
    LOG_INFO("info message");
    LOG_WARNING("warning message");
    LOG_ERROR("error message");
    LOG_FATAL("fatal message");
    
    // call function
    TestFunc();
    
    // create object
    TestClass *testObj = new TestClass(101);

    // free memory
    delete testObj;
    delete gLog;
}

查看智能系统日志中的消息。 消息清楚地显示级别和消息发送者(所有者)。

2022.02.16 20:17:21.048 TestLogger (USDJPY,H1)  DBG: OnStart debug message
2022.02.16 20:17:21.291 TestLogger (USDJPY,H1)  INF: OnStart info message
2022.02.16 20:17:21.299 TestLogger (USDJPY,H1)  WRN: OnStart warning message
2022.02.16 20:17:21.303 TestLogger (USDJPY,H1)  ERR: OnStart error message
2022.02.16 20:17:21.323 TestLogger (USDJPY,H1)  FTL: OnStart fatal message
2022.02.16 20:17:21.328 TestLogger (USDJPY,H1)  INF: TestFunc info message from inner function
2022.02.16 20:17:21.332 TestLogger (USDJPY,H1)  DBG: TestClass::TestClass create object with id = 101

在 Notepad++ 编辑器中查看消息

在 Notepad++ 中查看消息


在 Seq 中查看消息

在 Seq 中查看消息


把 MetaTrader 5 日志导入 Seq

为了将日志导入 Seq,我用 Python 创建了 seq2log 软件包。 我不会在本文中讲述它。 该软件包里包含 README.md 文件。 代码包含详细的注释。 seq2log 软件包从智能系统流水账 MQL5/Logs/yyyymmdd.log 导入任意日志。 没有重要性级别的消息会被指定为 INF 级别:

seq2log 可用于何处? 例如,如果您是一名自由职业开发者,您可以要求您的客户发送一份智能系统日志。 可以在文本编辑器中分析日志,但在 Seq 中使用 SQL 查询更便捷。 最常用或最复杂的查询可以存储在 Seq 中,只需在查询名称上单击即可执行。

    Run: py log2seq appName pathLog
    where log2seq - package name
        appName - application name to identify events in Seq
        pathLog - MetaTrader 5 log path
    Example: py log2seq Cayman d:/Project/MQL5/Logs/20211028.log


结束语

本文介绍了 Logger 类,以及如何使用它:

  • 记录具有严重性级别的结构化消息
  • 在 Seq 日志收集和分析系统中注册消息(事件)

Logger 类及其测试的源代码见附件。 此外,该附件还包含 log2seq 软件包的 Python 源代码,该软件包可将现有的 MetaTrader 5 日志导入 Seq。

Seq 服务能够以专业级别来分析日志。 它提供了强大的数据采样和可视化功能。 此外,Logger 类源代码允许向日志消息中添加专门为可视化而设计的数据 — 用于在 Seq 中绘制图表。 这可能会鼓励您查看应用程序日志中的调试信息。 尝试在实践中应用它。 祝好运!


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/10475

附加的文件 |
log2seq.zip (8.32 KB)
Logger.mqh (15.58 KB)
TestLogger.mq5 (2.83 KB)
DoEasy 函数库中的图形(第九十八部分):移动扩展的标准图形对象的轴点 DoEasy 函数库中的图形(第九十八部分):移动扩展的标准图形对象的轴点
在本文中,我将继续扩展的标准图形对象的开发,创建移动复合图形对象轴点的功能,通过控制点来管理图形对象轴点坐标。
在一张图表上的多个指标(第 03 部分):为用户开发定义 在一张图表上的多个指标(第 03 部分):为用户开发定义
今天,我们将首次更新指标系统的功能。 在“一张图表上的多个指标”的前一篇文章中,我们研究了允许在图表子窗口中加载多个指标的基本代码。 但其所代表的只是一个更大系统的起点。
一张图表上多个指标(第 04 部分):晋升为一款智能交易系统 一张图表上多个指标(第 04 部分):晋升为一款智能交易系统
在我之前的文章里,我已经解释了如何创建拥有多个子窗口的指标,在使用自定义指标时如此这般会变得很有趣。 这次,我们将看到如何为智能交易系统添加多个窗口。
在一张图表上的多个指标(第 02 部分):首次实验 在一张图表上的多个指标(第 02 部分):首次实验
在前一篇文章“在一张图表上的多个指标”中,我介绍了如何在一张图表上加载多个指标的概念和基本知识。 在本文中,我将提供源代码,并对其进行详解。