English 日本語
preview
Meistern der Log-Einträge (Teil 5): Optimierungen mit Cache und Rotation

Meistern der Log-Einträge (Teil 5): Optimierungen mit Cache und Rotation

MetaTrader 5Beispiele | 17 Juni 2025, 07:46
57 0
joaopedrodev
joaopedrodev

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:

  1. Robuste Struktur unter Verwendung des Musters Singleton, das die Konsistenz zwischen den Code-Komponenten gewährleistet.
  2. Erweiterte Persistenz für die Speicherung von Protokollen in Datenbanken, die eine nachvollziehbare Historie für eingehende Audits und Analysen bieten.
  3. 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.
  4. Klassifizierung nach Protokollebenen, wobei informative Meldungen von kritischen Warnungen und Fehlern unterschieden werden.
  5. 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:

  1. 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.
  2. 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

Beigefügte Dateien |
Logify2Part5c.zip (17.06 KB)
Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 14): Parabolischer Stopp und Umkehr-Tool Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 14): Parabolischer Stopp und Umkehr-Tool
Der Einsatz von technischen Indikatoren in der Preisaktionsanalyse ist ein wirkungsvoller Ansatz. Diese Indikatoren weisen häufig auf wichtige Umkehr- und Rücksetzpunkte hin und bieten wertvolle Einblicke in die Marktdynamik. In diesem Artikel zeigen wir, wie wir ein automatisiertes Tool entwickelt haben, das mit Hilfe des Parabolic-SAR-Indikators Signale erzeugt.
Erstellen eines Handelsadministrator-Panels in MQL5 (Teil IX): Code Organisation (II): Modularisierung Erstellen eines Handelsadministrator-Panels in MQL5 (Teil IX): Code Organisation (II): Modularisierung
In dieser Diskussion gehen wir einen Schritt weiter, indem wir unser MQL5-Programm in kleinere, besser handhabbare Module aufteilen. Diese modularen Komponenten werden dann in das Hauptprogramm integriert, um dessen Organisation und Wartbarkeit zu verbessern. Dieser Ansatz vereinfacht die Struktur unseres Hauptprogramms und macht die einzelnen Komponenten in anderen Expert Advisors (EAs) und Indikatorentwicklungen wiederverwendbar. Durch diesen modularen Aufbau schaffen wir eine solide Grundlage für künftige Erweiterungen, von denen sowohl unser Projekt als auch die breitere Entwicklergemeinschaft profitiert.
Ein neuer Ansatz für nutzerdefinierte Kriterien in den Optimierungen (Teil 1): Beispiele für Aktivierungsfunktionen Ein neuer Ansatz für nutzerdefinierte Kriterien in den Optimierungen (Teil 1): Beispiele für Aktivierungsfunktionen
Der erste einer Reihe von Artikeln, die sich mit der Mathematik der nutzerdefinierten Kriterien befassen, mit besonderem Schwerpunkt auf nichtlinearen Funktionen, die in neuronalen Netzen verwendet werden, MQL5-Code für die Implementierung und die Verwendung von gezielten und korrigierenden Offsets.
Datenwissenschaft und ML (Teil 33): Pandas Dataframe in MQL5, Vereinfachung der Datensammlung für ML-Nutzung Datenwissenschaft und ML (Teil 33): Pandas Dataframe in MQL5, Vereinfachung der Datensammlung für ML-Nutzung
Bei der Arbeit mit maschinellen Lernmodellen ist es wichtig, die Konsistenz der für Training, Validierung und Tests verwendeten Daten sicherzustellen. In diesem Artikel werden wir unsere eigene Version der Pandas-Bibliothek in MQL5 erstellen, um einen einheitlichen Ansatz für den Umgang mit maschinellen Lerndaten zu gewährleisten und sicherzustellen, dass innerhalb und außerhalb von MQL5, wo der Großteil des Trainings stattfindet, dieselben Daten verwendet werden.