
来自专业程序员的提示(第三部分):日志。 连接到 Seq 日志收集和分析系统
内容目录
概述
Logging 是用于分析应用程序操作的消息输出。 MQL5 的 Print 和 PrintFormat 函数将输出消息保存到智能系统栏流水账。 智能系统栏流水账是 Unicode 格式的文本文件。 每天都会创建新的 MQL5/Logs/yyyymmdd.log 文件,从而避免日志超载。
所有打开图表上的所有脚本和智能交易系统将“日志写入”一个文件。 一部分日志仍保留在磁盘缓存当中。 换句话说,如果从资源管理器中打开日志文件,不会看到其中的最新信息,因为它还位于缓存中。 若要强制终端将缓存保存到文件中,您应该关闭终端,或者使用“智能系统”选项卡的关联菜单,并选择其中的“打开”项。 这将打开一个包含日志文件的目录。
分析这些日志并不轻松,尤其是在终端中。 但这种分析非常重要。 在提示的第一部分中,我展示了简化终端日志中信息搜索、选择和查看的一种方式。 在本文中,我将向您展示如何:
- 统一日志输出(Logger 类)
- 把日志连接到 Seq 日志收集和分析系统
- Seq 中在线查看消息(事件)
- 把常规 MetaTrader 5 日志导入 Seq(Python 软件包)
Seq:日志收集和分析系统
Seq 是一个实时搜索和分析应用程序日志的服务器。 其设计优良的用户界面、JSON 格式的事件存储、和 SQL 查询语言支持,令其成为识别和诊断复杂应用程序和微服务中问题的有效平台。
若要向 Seq 发送消息,您需要:
- 在我们的计算机上安装 Seq
安装后,Seq UI 将于以下位置提供:
http://localhost:5341/#/events - 在 c:/windows/system32/drivers/etc/hosts 文件里添加以下行:
127.0.0.1 seqlocal.net
为了能够在 MetaTrader 5 终端设置里添加 URL - 禁用 Seq 中的时区,“按原样”显示消息时间
- 进入 UI Seq
- 进入 admin/Preferences/Preferences
- 启用 "Show timestamps in Coordinated Universal Time (UTC)" - 将以下地址添加到 MT5/工具/选项/智能系统
http://seqlocal.net
允许 WebRequest 函数访问该 URL
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++ 编辑器中查看消息
在 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


是否缺少报价功能?