
Meistern der Log-Einträge (Teil 5): Optimierungen mit Cache und Rotation
Einführung
Im ersten Artikel dieser Serie, „Meistern der Log-Einträge (Teil 1): Grundlegende Konzepte und erste Schritte in MQL5“, haben wir mit der Erstellung einer nutzerdefinierten Protokollbibliothek für die Entwicklung von Expert Advisors (EA) begonnen. Darin haben wir die Motivation für die Entwicklung eines solchen wichtigen Tools untersucht: die Überwindung der Beschränkungen von MetaTrader 5's nativen Protokollen und die Bereitstellung einer robusten, anpassbaren und leistungsstarken Lösung für das MQL5-Universum.
Um die wichtigsten, behandelten Punkte zusammenzufassen, haben wir die Grundlage für unsere Bibliothek geschaffen, indem wir die folgenden grundlegenden Anforderungen festgelegt haben:
- Robuste Struktur unter Verwendung des Musters Singleton, das die Konsistenz zwischen den Code-Komponenten gewährleistet.
- Erweiterte Persistenz für die Speicherung von Protokollen in Datenbanken, die eine nachvollziehbare Historie für eingehende Audits und Analysen bieten.
- Flexibilität bei den Ausgaben, sodass die Protokolle bequem gespeichert oder angezeigt werden können, sei es in der Konsole, in Dateien, im Terminal oder in einer Datenbank.
- Klassifizierung nach Protokollebenen, wobei informative Meldungen von kritischen Warnungen und Fehlern unterschieden werden.
- Anpassung des Ausgabeformats an die individuellen Bedürfnisse der einzelnen Entwickler oder Projekte.
Mit dieser gut etablierten Grundlage wurde klar, dass das von uns entwickelte Logging-Framework weit mehr sein wird als ein einfaches Ereignisprotokoll; es wird ein strategisches Tool zum Verstehen, Überwachen und Optimieren des Verhaltens von EAs in Echtzeit sein.
Bisher haben wir uns mit den Grundlagen von Protokollen befasst, gelernt, wie man sie formatiert, und verstanden, wie Handler das Ziel von Nachrichten steuern. Im letzten Artikel haben wir gelernt, wie man Log-Einträge in einer Datei (.txt, .log oder .json) speichert. In diesem fünften Artikel werden wir den Prozess der Speicherung von Protokollen in Dateien durch die Implementierung von Caching und Dateirotation optimieren. Fangen wir an!
Hinzufügen eines Formatierers zu jedem Handler
Bislang verwaltet unsere Protokollierungsbibliothek die Nachrichtenformatierung über eine einzige Instanz der Klasse CFormatter, die in der Bibliotheksbasis (CLogify) zentralisiert ist. Dieser Ansatz eignet sich gut für einfache Szenarien, schränkt aber die Flexibilität der Handler ein.
Das Problem ist, dass durch die Verwendung eines einzigen globalen Formatierers alle Handler dasselbe Format verwenden, was nicht unbedingt ideal ist, wenn verschiedene Ziele unterschiedliche Formatierungen erfordern. Während beispielsweise ein Handler, der Protokolle in JSON schreibt, eine bestimmte Struktur benötigt, kann ein Handler, der Protokolle auf der Konsole ausgibt, ein besser lesbares Format benötigen. Die Lösung besteht darin, die Verantwortung für die Formatierung auf die Basisklasse des Handlers (CLogifyHandler) zu übertragen. Auf diese Weise kann jeder Handler seinen eigenen unabhängigen Formatierer haben, was eine bessere Kontrolle über die Formatierung der Protokollnachrichten ermöglicht. Lassen Sie uns diese Änderung umsetzen und sehen, wie sie die Flexibilität der Bibliothek verbessert.
Wir beginnen mit dem Code, indem wir eine Instanz des CFormatter innerhalb von CLogifyHandler hinzufügen. Da dies für diejenigen, die die vorherigen Artikel gelesen haben, eine einfache Aufgabe ist, werde ich nur den endgültigen Code hinzufügen, der hervorhebt, was hinzugefügt wurde:
//+------------------------------------------------------------------+ //| LogifyHandler.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" #include "../Formatter/LogifyFormatter.mqh" //+------------------------------------------------------------------+ //| class : CLogifyHandler | //| | //| [PROPERTY] | //| Name : CLogifyHandler | //| Heritage : No heritage | //| Description : Base class for all log handlers. | //| | //+------------------------------------------------------------------+ class CLogifyHandler { protected: string m_name; ENUM_LOG_LEVEL m_level; CLogifyFormatter *m_formatter; public: CLogifyHandler(void); ~CLogifyHandler(void); //--- Handler methods virtual void Emit(MqlLogifyModel &data); // Processes a log message and sends it to the specified destination virtual void Flush(void); // Clears or completes any pending operations virtual void Close(void); // Closes the handler and releases any resources //--- Set/Get void SetLevel(ENUM_LOG_LEVEL level); void SetFormatter(CLogifyFormatter *format); string GetName(void); ENUM_LOG_LEVEL GetLevel(void); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandler::CLogifyHandler(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandler::~CLogifyHandler(void) { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter ; } } //+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandler::Emit(MqlLogifyModel &data) { } //+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandler::Flush(void) { } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandler::Close(void) { } //+------------------------------------------------------------------+ //| Set level | //+------------------------------------------------------------------+ void CLogifyHandler::SetLevel(ENUM_LOG_LEVEL level) { m_level = level; } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogifyHandler::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get name | //+------------------------------------------------------------------+ string CLogifyHandler::GetName(void) { return(m_name); } //+------------------------------------------------------------------+ //| Get level | //+------------------------------------------------------------------+ ENUM_LOG_LEVEL CLogifyHandler::GetLevel(void) { return(m_level); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogifyHandler::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
Um mit den einfachsten Änderungen fortzufahren, haben wir die CFormatter-Instanz in CLogify entfernt. Die Teile, die aus der Klasse entfernt wurden, sind rot hervorgehoben, und die, die hinzugefügt wurden, sind grün hervorgehoben:
//+------------------------------------------------------------------+ //| Logify.mqh | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" #property version "1.00" #include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh" #include "Handlers/LogifyHandler.mqh" #include "Handlers/LogifyHandlerConsole.mqh" #include "Handlers/LogifyHandlerDatabase.mqh" #include "Handlers/LogifyHandlerFile.mqh" //+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifyFormatter *m_formatter; CLogifyHandler *m_handlers[]; public: CLogify(); ~CLogify(); //--- Handler void AddHandler(CLogifyHandler *handler); bool HasHandler(string name); CLogifyHandler *GetHandler(string name); CLogifyHandler *GetHandler(int index); int SizeHandlers(void); //--- Generic method for adding logs bool Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Specific methods for each log level bool Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Get/Set object formatter void SetFormatter(CLogifyFormatter *format); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogify::CLogify() { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogify::~CLogify() { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter; } //--- Delete handlers int size_handlers = ArraySize(m_handlers); for(int i=0;i<size_handlers;i++) { delete m_handlers[i]; } } //+------------------------------------------------------------------+ //| Add handler to handlers array | //+------------------------------------------------------------------+ void CLogify::AddHandler(CLogifyHandler *handler) { int size = ArraySize(m_handlers); ArrayResize(m_handlers,size+1); m_handlers[size] = GetPointer(handler); } //+------------------------------------------------------------------+ //| Checks if handler is already in the array by name | //+------------------------------------------------------------------+ bool CLogify::HasHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(true); } } return(false); } //+------------------------------------------------------------------+ //| Get handler by name | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(m_handlers[i]); } } return(NULL); } //+------------------------------------------------------------------+ //| Get handler by index | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(int index) { return(m_handlers[index]); } //+------------------------------------------------------------------+ //| Gets the total size of the handlers array | //+------------------------------------------------------------------+ int CLogify::SizeHandlers(void) { return(ArraySize(m_handlers)); } //+------------------------------------------------------------------+ //| 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); data.formated = m_formatter.FormatLog(data); //--- Call handlers int size = this.SizeHandlers(); for(int i=0;i<size;i++) { data.formated = m_handlers[i].GetFormatter().FormatLog(data); m_handlers[i].Emit(data); } return(true); } //+------------------------------------------------------------------+ //| 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)); } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogify::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogify::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
Der einzige Teil, der hinzugefügt wurde, betrifft die Formatierung der Nachricht. Vorher haben wir den Formatierer in der Klasse selbst verwendet. Mit den Änderungen verwenden wir in jedem Handler den vom Handler bereitgestellten Formatierer. Indem wir jedem Handler direkt einen Formatierer zuordnen, beseitigen wir die Beschränkung auf ein einziges Format und machen die Bibliothek anpassungsfähiger an unterschiedliche Bedürfnisse. Jetzt kann jedes Ziel einen spezifischen Protokollstil haben, der sicherstellt, dass die Ausgabe besser für den Kontext geeignet ist, in dem sie verwendet wird. Im nächsten Thema werden wir sehen, wie die Ausführung von Protokollen in geplanten Zyklen mit der Klasse CIntervalWatcher verwaltet werden kann, die eine Hilfsklasse für die Dateirotation sein wird.
Erstellen der Klasse CIntervalWatcher
Das Hauptziel von CIntervalWatcher ist es, zu prüfen, ob seit dem letzten Aufruf ein bestimmtes Zeitintervall vergangen ist. Dies ist wichtig für die Erstellung von Protokollen, die in bestimmten Zeitabständen überprüft werden müssen. Um eine Überlastung beim Schreiben zu vermeiden oder die Datensätze besser zu strukturieren, ist ein Kontrollmechanismus des Zyklus unerlässlich, der unnötige Verarbeitungen bei jedem Ticken vermeidet. Es ermöglicht uns die Konfiguration:
- Das zu überwachende Zeitintervall (in Sekunden).
- Die Zeitquelle (aktuelle Zeit, GMT, lokaler oder Handelsserver).
- Ob es bei der ersten Ausführung true zurückgeben soll.
Auf diese Weise ist die Klasse nützlich, um zu prüfen, wann eine periodische Aktion innerhalb der Bibliothek ausgeführt werden soll. Legen wir einen neuen Ordner mit dem Namen Utils an, der diese Datei enthalten wird. Am Ende sollte der Dateibrowser wie folgt aussehen:
Um die Klasse zu erstellen, erstellen wir zunächst eine Enumeration zur Unterstützung verschiedener Zeitquellen, die wir ENUM_TIME_ORIGIN nennen:
//+------------------------------------------------------------------+ //| Enum for different time sources | //+------------------------------------------------------------------+ enum ENUM_TIME_ORIGIN { TIME_ORIGIN_CURRENT = 0, // [0] Current Time TIME_ORIGIN_GMT, // [1] GMT Time TIME_ORIGIN_LOCAL, // [2] Local Time TIME_ORIGIN_TRADE_SERVER // [3] Server Time }; //+------------------------------------------------------------------+
Wir haben der Klasse private Variablen hinzugefügt, um den letzten aufgezeichneten Zeitpunkt (m_last_time), das gewünschte Zeitintervall (m_interval), den Zeitursprung (m_time_origin) und ein Flag (m_first_return) zur Steuerung der ersten Rückkehr zu speichern. Infolgedessen haben wir für jedes private Attribut ein Set (setzen) und ein Get (abrufen) erstellt. Um die Konfiguration der Intervalle, des Zeitursprungs und der ersten Rückkehr zu erleichtern, habe ich beschlossen, einige zusätzliche Konstruktoren für die Klasse zu erstellen, die Ihnen als Entwickler helfen. Nachfolgend finden Sie den Code mit Konstruktoren und Methoden für den Zugriff auf und den Erhalt von privaten Daten.
//+------------------------------------------------------------------+ //| class : CIntervalWatcher | //| | //| [PROPERTY] | //| Name : CIntervalWatcher | //| Type : Report | //| Heritage : No heredirary. | //| Description : Monitoring new time periods | //| | //+------------------------------------------------------------------+ class CIntervalWatcher { private: //--- Auxiliary attributes ulong m_last_time; ulong m_interval; ENUM_TIME_ORIGIN m_time_origin; bool m_first_return; public: CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(void); ~CIntervalWatcher(void); //--- Setters void SetInterval(ENUM_TIMEFRAMES interval); void SetInterval(ulong interval); void SetTimeOrigin(ENUM_TIME_ORIGIN time_origin); void SetFirstReturn(bool first_return); //--- Getters ulong GetInterval(void); ENUM_TIME_ORIGIN GetTimeOrigin(void); bool GetFirstReturn(void); ulong GetLastTime(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = PeriodSeconds(interval); m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = interval; m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(void) { m_interval = 10; // 10 seconds m_time_origin = TIME_ORIGIN_CURRENT; m_first_return = true; m_last_time = 0; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CIntervalWatcher::~CIntervalWatcher(void) { } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ENUM_TIMEFRAMES interval) { m_interval = PeriodSeconds(interval); } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ulong interval) { m_interval = interval; } //+------------------------------------------------------------------+ //| Set time origin | //+------------------------------------------------------------------+ void CIntervalWatcher::SetTimeOrigin(ENUM_TIME_ORIGIN time_origin) { m_time_origin = time_origin; } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ void CIntervalWatcher::SetFirstReturn(bool first_return) { m_first_return=first_return; } //+------------------------------------------------------------------+ //| Get interval | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetInterval(void) { return(m_interval); } //+------------------------------------------------------------------+ //| Get time origin | //+------------------------------------------------------------------+ ENUM_TIME_ORIGIN CIntervalWatcher::GetTimeOrigin(void) { return(m_time_origin); } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ bool CIntervalWatcher::GetFirstReturn(void) { return(m_first_return); } //+------------------------------------------------------------------+ //| Set last time | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetLastTime(void) { return(m_last_time); } //+------------------------------------------------------------------+
Um die Hauptmethode zu unterstützen, erstellen wir die Funktion GetTime, die die Zeit auf der Grundlage des definierten Ursprungs zurückgibt:
//+------------------------------------------------------------------+ //| Get time in miliseconds | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetTime(ENUM_TIME_ORIGIN time_origin) { switch(time_origin) { case(TIME_ORIGIN_CURRENT): return(TimeCurrent()); case(TIME_ORIGIN_GMT): return(TimeGMT()); case(TIME_ORIGIN_LOCAL): return(TimeLocal()); case(TIME_ORIGIN_TRADE_SERVER): return(TimeTradeServer()); } return(0); } //+------------------------------------------------------------------+
Die wichtigste Methode der Klasse ist Inspect(), die überprüft, ob das festgelegte Intervall erreicht wurde. Die Logik ist wie folgt: Beim ersten Aufruf wird geprüft, ob m_last_time Null ist (neu instanziierte Klasse), die Funktion speichert die aktuelle Zeit und gibt m_first_return zurück. Unterscheidet sich der gespeicherte Zeitstempel vom aktuellen Zeitstempel plus dem Intervall, bedeutet dies, dass das Intervall erreicht wurde, sodass m_last_time aktualisiert wird und die Funktion true zurückgibt. Wenn der Zeitstempel gleich ist, bedeutet dies, dass er noch nicht erreicht wurde, sodass die Funktion false zurückgibt.
//+------------------------------------------------------------------+ //| Check if there was an update | //+------------------------------------------------------------------+ bool CIntervalWatcher::Inspect(void) { //--- Get time ulong time_current = this.GetTime(m_time_origin); //--- First call, initial return if(m_last_time == 0) { m_last_time = time_current; return(m_first_return); } //--- Check interval if(time_current >= m_last_time + m_interval) { m_last_time = time_current; return(true); } return(false); } //+------------------------------------------------------------------+
Mit CIntervalWatcher haben wir eine feinere Kontrolle über die Protokollerstellung, was programmierbare Zyklen und eine höhere Verarbeitungseffizienz ermöglicht. Diese Art von Ansatz ist für eine Protokollierungsbibliothek, die eine periodische Ausführung von Aufgaben erfordert, unerlässlich. Nun, da die regelmäßige Ausführung von Protokollierungsaktionen konfiguriert ist, können wir uns auf die Optimierung des Aufzeichnungsprozesses und die Aufrechterhaltung der Systemleistung konzentrieren.
Optimierung der Protokollspeicherung: Zwischenspeicherung und Dateirotation
Obwohl die direkte Aufzeichnung von Protokollen in Dateien, die wir im letzten Artikel implementiert haben, eine funktionale Lösung ist, kann sie bei wachsendem Volumen der Protokolle ineffizient werden. Um negative Auswirkungen auf die Leistung zu vermeiden, muss dieser Prozess optimiert werden. In diesem Thema werden wir untersuchen, wie ein Caching- und Dateirotationssystem implementiert werden kann, um sicherzustellen, dass Protokolle effizient geschrieben werden, ohne den Speicher zu überlasten und die Datenintegrität zu wahren.
Im letzten Artikel haben wir die Funktionsweise und die Vorteile dieser Verbesserungen näher erläutert:
„Stellen Sie sich folgendes Szenario vor: Ein Expert Advisor läuft wochen- oder monatelang und zeichnet jedes Ereignis, jeden Fehler oder jede Benachrichtigung in derselben Datei auf. Schon bald erreicht das Protokoll beträchtliche Ausmaße, was das Lesen und Interpretieren von Informationen recht komplex macht. Hier kommt die Rotation ins Spiel. Sie ermöglicht es uns, diese Informationen in kleinere und geordnete Teile aufzuteilen, was das Lesen und Analysieren sehr viel einfacher macht.
Die beiden gängigsten Methoden hierfür sind:
- Nach Größe: Sie legen eine Größenbeschränkung für die Protokolldatei fest, normalerweise in Megabyte (MB). Wenn diese Grenze erreicht ist, wird automatisch eine neue Datei angelegt, und der Zyklus beginnt von neuem. Dieser Ansatz ist sehr praktisch, wenn der Schwerpunkt auf der Kontrolle des Stammwachstums liegt, ohne sich an einen Kalender halten zu müssen. Sobald die aktuelle Datei das Größenlimit (in Megabyte) erreicht, kommt es zu folgendem Ablauf: Die aktuelle Protokolldatei wird umbenannt und erhält einen Index, z. B. „log1.log“. Die vorhandenen Dateien im Verzeichnis werden ebenfalls umnummeriert, z. B. wird aus „log1.log“ „log2.log“. Wenn die Anzahl der Dateien die zulässige Höchstzahl erreicht, werden die ältesten Dateien gelöscht. Dieser Ansatz ist nützlich, um sowohl den von den Protokollen belegten Platz als auch die Anzahl der gespeicherten Dateien zu begrenzen.
- Nach Datum: In diesem Fall wird jeden Tag eine neue Protokolldatei erstellt. Jedes dieser Protokolle trägt in seinem Namen das Datum, an dem es erstellt wurde, z. B. log_2025-01-19.log, was bereits einen Großteil der Probleme bei der Organisation der Protokolle beseitigt. Dieser Ansatz ist ideal, wenn Sie sich einen bestimmten Tag ansehen möchten, ohne sich in einer einzigen riesigen Datei zu verlieren. Diese Methode verwende ich am häufigsten, wenn ich meine Expert Adivisors-Protokolle speichere. Alles ist sauberer, direkter und einfacher zu navigieren.
Darüber hinaus können Sie die Anzahl der gespeicherten Protokolldateien begrenzen. Diese Kontrolle ist sehr wichtig, um eine unnötige Anhäufung alter Daten zu verhindern. Stellen Sie sich vor, Sie konfigurieren es so, dass die 30 neuesten Dateien gespeichert werden. Wenn die 31. erscheint, verwirft das System automatisch das älteste Protokoll, sodass sich keine sehr alten Protokolle auf der Festplatte ansammeln, und die neuesten werden aufbewahrt.
Ein weiteres wichtiges Detail ist die Verwendung eines Caches. Anstatt jede Nachricht direkt in die Datei zu schreiben, sobald sie eintrifft, werden die Nachrichten im Cache zwischengespeichert. Wenn der Cache eine bestimmte Grenze erreicht, wird der gesamte Inhalt der Datei auf einmal gelöscht. Dies führt zu weniger Lese- und Schreibvorgängen auf der Festplatte, mehr Leistung und einer längeren Lebensdauer Ihrer Speichergeräte.
Um die Rotation von Protokolldateien zu implementieren, benötigen wir zunächst eine Hilfsmethode namens SearchForFilesInDirectory(). Diese Methode ist für die Suche nach allen Dateien in einem bestimmten Verzeichnis verantwortlich und gibt deren Namen in einem Array zurück. Sie verwendet die Funktion FileFindFirst(), um die Suche zu starten, und wenn sie Dateien findet, werden ihre Namen zu diesem Array hinzugefügt. Nachdem der Prozess abgeschlossen ist, schließt die Methode den Such-Handler mit FileFindClose().
Aber warum ist diese Methode so wichtig? Ganz einfach! Sie ermöglicht es uns, die vorhandenen Protokolldateien aufzulisten und sicherzustellen, dass die Klasse, die die Protokolle verwaltet, ältere Dateien bei Bedarf löscht.
class CLogifyHandlerFile : public CLogifyHandler { private: bool SearchForFilesInDirectory(string directory, string &file_names[]); }; //+------------------------------------------------------------------+ //| Returns an array with the names of all files in the directory | //+------------------------------------------------------------------+ bool CLogifyHandlerFile::SearchForFilesInDirectory(string directory,string &file_names[]) { //--- Search for all log files in the specified directory with the given file extension string file_name; long search_handle = FileFindFirst(directory,file_name); ArrayFree(file_names); bool is_found = false; if(search_handle != INVALID_HANDLE) { do { //--- Add each file name found to the array of file names int size_file = ArraySize(file_names); ArrayResize(file_names,size_file+1); file_names[size_file] = file_name; is_found = true; } while(FileFindNext(search_handle,file_name)); FileFindClose(search_handle); } return(is_found); } //+------------------------------------------------------------------+
Da wir nun die Funktion zum Abrufen der Dateien haben, können wir sie in die Hauptmethode Emit() integrieren, die für die Ausgabe der Protokolle zuständig ist. Je nach der gewählten Rotationskonfiguration wird die Logik angepasst.
Wenn die Protokollrotation so konfiguriert ist, dass sie auf der Grundlage der Dateigröße erfolgt, wird die Funktion:
- Überprüfen, ob die Dateigröße die konfigurierte Grenze ( m_config.max_file_size_mb ) überschritten hat.
- Alle Protokolldateien im Verzeichnis durchsuchen.
- Alte Dateien entfernen, die die maximal zulässige Anzahl überschreiten ( m_config.max_file_count ).
- Alte Dateien umbennen, wobei ihre Indizes numerisch erhöht werden ( log1.txt , log2.txt, usw.).
- Die aktuelle Protokolldatei in „log1“ umbenennen, um die Reihenfolge beizubehalten.
Wenn die Rotation auf dem Datum basiert, wird die Funktion:
- Alle Protokolldateien im Verzeichnis durchsuchen.
- Die ältesten Dateien löschen, die die maximal zulässige Anzahl (m_config.max_file_count) überschreiten.
Schauen wir uns nun die Implementierung der Methode Emit() mit beiden Rotationslogiken an:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ 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 = m_file.Open(log_path, FILE_READ | FILE_WRITE | FILE_ANSI); if(handle_file == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")"); return; } //--- Write m_file.Seek(0, SEEK_END); m_file.WriteString(data.formated + "\n"); //--- Size in megabytes ulong size_mb = m_file.Size() / (1024 * 1024); //--- Close file m_file.Close(); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } } //+------------------------------------------------------------------+
Blockweises Speichern für bessere Leistung
Gehen wir zu einer weiteren Verbesserung über und erstellen wir die Logik, die ich für die interessanteste in diesem Artikel halte: das Speichern von Datensätzen in Blöcken. Die zentrale Idee besteht darin, einen Cache (Zwischenspeicher) zu implementieren, in dem die Log-Einträge gespeichert werden, bis sie eine bestimmte Grenze erreichen. Wenn diese Grenze erreicht ist, werden alle Datensätze im Cache auf einmal in der Protokolldatei gespeichert.
Wir werden diese Logik in mehreren Schritten umsetzen. Zunächst wird die Cache-Struktur in der Klasse CLogifyHandlerFile angelegt. Im privaten Abschnitt der Klasse fügen wir ein Array vom Typ MqlLogifyModel hinzu, um die Protokolldatensätze vorübergehend zu speichern. Wir fügen auch eine Variable ein, um den aktuellen Index des letzten im Cache gespeicherten Wertes zu kontrollieren. Immer wenn ein neuer Datensatz hinzugefügt wird, wird dieser Index inkrementiert. Wir erstellen auch eine Instanz der Klasse CIntervalWatcher und legen im Konstruktor ein Intervall von einem Tag fest. Sehen Sie, wie es aussieht:
class CLogifyHandlerFile : public CLogifyHandler { private: //--- Update utilities CIntervalWatcher m_interval_watcher; //--- Cache data MqlLogifyModel m_cache[]; int m_index_cache; }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::CLogifyHandlerFile(void) { m_interval_watcher.SetInterval(PERIOD_D1); ArrayFree(m_cache); m_index_cache = 0; } //+------------------------------------------------------------------+
Nachdem die Cache- und Aktualisierungsstruktur erstellt wurde, folgt der nächste Schritt: die Änderung der Methode Emit() zur Verwendung des Cache.
Die Methode Emit() ist für die Verarbeitung einer Protokollnachricht und deren Versand an das konfigurierte Ziel (in diesem Fall eine Datei) zuständig. Wir werden sie so anpassen, dass sie die Daten nicht direkt in der Datei, sondern vorübergehend im Cache speichert. Wenn der Cache sein konfiguriertes Limit oder das festgelegte Intervall (ein Tag) erreicht, ruft die Methode die Funktion Flush() auf, die die gesammelten Datensätze in der Datei speichert. Dieses Intervall ist nützlich, denn wenn die Daten mehr als einen Tag zwischengespeichert wurden, stellt dieser Mechanismus sicher, dass die Daten weiterhin täglich gespeichert werden, und ermöglicht es außerdem, die Rotationsroutine jeden Tag auszuführen.
Hier ist der geänderte Code:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerFile::Emit(MqlLogifyModel &data) { //--- Checks if the configured level allows if(data.level >= this.GetLevel()) { //--- Resize cache if necessary int size = ArraySize(m_cache); if(size != m_config.messages_per_flush) { ArrayResize(m_cache, m_config.messages_per_flush); size = m_config.messages_per_flush; } //--- Add log to cache m_cache[m_index_cache++] = data; //--- Flush if cache limit is reached or update condition is met if(m_index_cache >= m_config.messages_per_flush || m_interval_watcher.Inspect()) { //--- Save cache Flush(); //--- Reset cache m_index_cache = 0; for(int i=0;i<size;i++) { m_cache[i].Reset(); } } } } //+------------------------------------------------------------------+
Die Funktion Flush() ist für die Speicherung der Cache-Daten in der Datei verantwortlich. Bei diesem Vorgang wird die Datei geöffnet, der Zeiger am Ende positioniert und alle im Cache gespeicherten Datensätze geschrieben.
//+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Flush(void) { //--- 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+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")"); return; } //--- Loop through all cached messages int size = ArraySize(m_cache); for(int i=0;i<size;i++) { if(m_cache[i].timestamp > 0) { //--- Point to the end of the file and write the message FileSeek(handle_file, 0, SEEK_END); FileWrite(handle_file, m_cache[i].formated); } } //--- Size in megabytes ulong size_mb = FileSize(handle_file) / (1024 * 1024); //--- Close file FileClose(handle_file); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } //+------------------------------------------------------------------+
Mit dieser Implementierung haben wir eine effiziente und skalierbare Protokollierungslösung geschaffen, die in der Lage ist, große Datenmengen zu verarbeiten, ohne die Leistung Ihres Experten zu beeinträchtigen. Schließlich müssen wir sicherstellen, dass beim Beenden des Programms alle zwischengespeicherten Daten in der Datei gespeichert werden. Rufen Sie dazu einfach die Methode Flush() in der Methode Close() auf, die bereits im Destruktor der Basisklasse CLogify aufgerufen wird.
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::~CLogifyHandlerFile(void) { this.Close(); } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Close(void) { //--- Save cache Flush(); } //+------------------------------------------------------------------+
Durch die Implementierung von Zwischenspeicherung und Dateirotation reduzieren wir die Anzahl der Schreibvorgänge auf die Festplatte und stellen sicher, dass unsere Protokolle effizienter gespeichert werden. Dies verleiht unserer Bibliothek Leistung und Skalierbarkeit und macht sie für reale Anwendungen robuster. Aber machen diese Optimierungen wirklich einen Unterschied? Testen wir es.
Leistungstests: Messung der Effizienz der Verbesserungen
Nun, da wir die Optimierungen umgesetzt haben, müssen wir ihre tatsächlichen Auswirkungen messen. Leistungstests helfen uns zu verstehen, ob der Cache die Schreiblast reduziert und ob die Dateirotation wie erwartet funktioniert. Dazu führen wir den gleichen Test wie im letzten Artikel durch, wobei wir die Originalversion der Bibliothek mit der optimierten Version vergleichen.
Für den Test verwenden wir dieselbe Datei, mit einigen Änderungen am Formatierer, da nun jeder Handler seinen eigenen Formatierer hat. Die Änderungen sind wie folgt hervorgehoben:
- Grün: Ergänzungen zum Code
- Rot: Gelöscht
- Gelb: Der Parameter, der die Größe des Cache definiert. Je größer der Cache, desto schneller die Verarbeitung.
//+------------------------------------------------------------------+ //| 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); handler_file.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- 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); } //+------------------------------------------------------------------+
Starten wir einen Test im Strategietester mit denselben Parametern für Datum und Symbol.
Bei Verwendung des Modells „OHLC für 1 Minute“ für das Symbol EURUSD und einen 7-Tage-Zeitrahmen betrug die Ausführungszeit 26 Sekunden. Es sei darauf hingewiesen, dass bei jedem Tick ein neuer Protokolleintrag erstellt wird und der Cache auf die Speicherung von 10 Meldungen eingestellt ist. Erhöhen wir nun den Cache auf 100 Nachrichten und beobachten wir den Unterschied in der Leistung:
Mit dieser Änderung konnten wir die Testzeit um 2 Sekunden verkürzen, wobei die gleichen Einstellungen für Modellierung, Datum und Symbole beibehalten wurden. Vergleicht man dies mit dem ersten Test im vorherigen Artikel, der 5 Minuten und 11 Sekunden dauerte, ist die Verbesserung beeindruckend!
Die Ergebnisse zeigen, dass kleine Optimierungen zu erheblichen Effizienzsteigerungen führen können. Die Kombination aus Caching und Dateirotation macht die Protokollverwaltung flexibler und zuverlässiger und bestätigt die bisher getroffenen Entscheidungen. Doch wie können diese Verbesserungen in der Praxis umgesetzt werden? Schauen wir uns einige Anwendungsbeispiele an.
Beispiele für die Verwendung der Protokollbibliothek
Nun, da wir unsere Protokollbibliothek verbessert haben, ist es an der Zeit, sie in die Tat umzusetzen! Lassen Sie uns anhand praktischer Beispiele untersuchen, wie Sie damit verschiedene Arten von Protokolldateien erstellen können, jede mit ihrer eigenen Formatierung und Schweregrad.
Beispiel 1: Logs in .log und .json Dateien aufteilen
Im ersten Szenario richten wir zwei Protokolldateien ein: eine im .log-Format und die andere im .json-Format. Jedes hat ein spezifisches Format und einen anderen Schweregrad, was die Verwaltung und Analyse der Protokolle erleichtert.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ALERT); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); //--- 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); } //+------------------------------------------------------------------+
Hier verwenden wir dieselbe Konfigurationsvariable m_config und ändern nur die Werte, die für die Definition beider Protokollformate erforderlich sind. Dies macht die Konfiguration einfacher und wiederverwendbar.
Beispiel 2: Speichern nur von Fehlern in einer JSON-Datei
Gehen wir nun einen Schritt weiter und konfigurieren ein spezielles Protokoll, das nur Fehlermeldungen speichert. Zu diesem Zweck erstellen wir einen separaten Ordner, in dem diese .json-Datei gespeichert wird. Außerdem fügen wir einen Konsolen-Handler hinzu, um Protokolle direkt im Terminal anzuzeigen.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs\\error",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ERROR); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Handler Console CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole(); handler_console.SetLevel(LOG_LEVEL_DEBUG); handler_console.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname} | {origin}] {msg}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); logify.AddHandler(handler_console); //--- 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); } //+------------------------------------------------------------------+
In diesem Beispiel verwenden wir drei Log-Handler:
- .log-Datei → Speichert Protokolle im herkömmlichen Format.
- .json-Datei → Speichert nur Fehlermeldungen in einem separaten Ordner.
- Konsole → Zeigt die Protokolle in einer für den Nutzer besser lesbaren Form an.
Die Verwendung eines „menschlicheren“ Formatierers in der Konsole trägt dazu bei, die Ausgabe verständlicher zu machen, während das JSON der Fehler die spätere Analyse erleichtert.
Anhand dieser Beispiele wird deutlich, wie unsere Protokollierungsbibliothek in realen Projekten eingesetzt werden kann. Die Flexibilität, verschiedene Formate und Schweregrade zu erstellen, ermöglicht ein gutes Management und hilft, Probleme leichter zu erkennen und zu beheben. Der modulare Aufbau macht es zudem einfach, das Protokollierungssystem nach Bedarf zu erweitern.
Jetzt müssen Sie nur noch diese Implementierung an Ihre Bedürfnisse anpassen und sicherstellen, dass Ihre Protokolle immer gut organisiert und zugänglich sind!
Schlussfolgerung
In diesem Artikel haben wir unsere Protokollierungsbibliothek weiterentwickelt und sie effizienter, skalierbarer und anpassungsfähiger gemacht. Wir haben die Formatierung verfeinert, sodass jeder Handler seinen eigenen Formatierer haben kann, was die Nachrichten übersichtlicher und flexibler für verschiedene Bedürfnisse macht, wie z.B. lokales Debugging und Auditing.
Wir haben die Klasse CIntervalWatcher implementiert, die die Ausführungszyklen steuert und sicherstellt, dass die Protokolle in genau definierten Intervallen geschrieben und rotiert werden. Außerdem haben wir das Schreiben durch Zwischenspeicherung optimiert, sodass weniger Festplattenoperationen erforderlich sind und das Wachstum von Dateien besser verwaltet werden kann. Wir haben diese Verbesserungen mit Leistungstests validiert und die Lösung weiter verfeinert, um hohe Belastungen zu unterstützen. Darüber hinaus haben wir praktische Beispiele vorgestellt, um die Anwendung der Bibliothek zu erleichtern.
Wenn es eine Hauptlektion gibt, die man aus diesem Artikel ziehen kann, dann ist es die Wichtigkeit, die Protokollierung als einen wesentlichen Aspekt der Softwareentwicklung zu behandeln. Ein gut durchdachtes Protokollierungssystem erleichtert nicht nur die Fehlersuche und die anschließende Prüfung, sondern trägt auch zur Sicherheit, Nachvollziehbarkeit und Zuverlässigkeit eines Expert Advisors bei. Die Implementierung guter Protokollierungspraktiken in der frühen Entwicklungsphase kann Ihnen Kopfschmerzen ersparen, die Wartung erleichtern und die Fehlerbehebung effizienter machen. Im nächsten Artikel werden wir untersuchen, wie man Protokolle in einer Datenbank für erweiterte Analysen speichert. Wir sehen uns!
Datei Name | Beschreibung |
---|---|
Experts/Logify/LogiftTest.mq5 | die Datei, in der wir die Funktionen der Bibliothek testen, mit einem praktischen Beispiel. |
Include/Logify/Formatter/LogifyFormatter.mqh | die Klasse, die für die Formatierung von Protokolldatensätzen zuständig ist, indem sie Platzhalter durch bestimmte Werte ersetzt. |
Include/Logify/Handlers/LogifyHandler.mqh | die Basisklasse für die Verwaltung von Log-Handlern, einschließlich der Einstellung des Levels und des Versands von Logs. |
Include/Logify/Handlers/LogifyHandlerConsole.mqh | der Handler der Logs, der formatierte Logs direkt an die Terminal-Konsole im MetaTrader sendet. |
Include/Logify/Handlers/LogifyHandlerDatabase.mqh | der Log-Handler, der formatierte Logs an eine Datenbank sendet (derzeit enthält er nur einen Ausdruck, aber bald werden wir ihn in einer echten Sqlite-Datenbank speichern). |
Include/Logify/Handlers/LogifyHandlerFile.mqh | der Log-Handler, der formatierte Logs in eine Datei sendet. |
Include/Logify/Utils/IntervalWatcher.mqh | prüft, ob ein Zeitintervall verstrichen ist, sodass Sie Routinen in der Bibliothek erstellen können. |
Include/Logify/Logify.mqh | die Kernklasse für die Protokollverwaltung, die Ebenen, Modelle und Formatierung integriert. |
Include/Logify/LogifyLevel.mqh | die Datei, die die Log-Ebenen der Logify-Bibliothek definiert und eine detaillierte Kontrolle ermöglicht. |
Include/Logify/LogifyModel.mqh | die Struktur, die die Protokolleinträge modelliert, einschließlich Details wie Ebene, Nachricht, Zeitstempel und Kontext. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17137





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.