
来自专业程序员的提示(第三部分):日志。 连接到 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

