
Meistern der Log-Einträge (Teil 2): Formatieren der Logs
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 der nativen Protokolle des MetaTrader 5 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 Singleton-Musters, 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.
In diesem zweiten Artikel werden wir uns nun mit einer der wichtigsten Funktionen dieser Bibliothek befassen: der Protokollformatierung. Schließlich geht es bei einem effizienten Protokoll nicht nur darum, was aufgezeichnet wird, sondern auch darum, wie die Informationen dargestellt werden. Stellen Sie sich vor, Sie erhalten mitten in einem wichtigen EA-Test verwirrende oder schlecht formatierte Nachrichten. Dies kann die Analyse nicht nur unnötig komplex machen, sondern auch zum Verlust wertvoller Informationen führen. Wir möchten, dass jedes Protokoll die nötige Klarheit und Ausführlichkeit aufweist, aber dennoch an die Bedürfnisse des Entwicklers angepasst werden kann.
Was ist ein Protokollformat?
Ein Protokollformat ist die Struktur, die die während der Ausführung eines Programms aufgezeichneten Informationen in einer klaren und verständlichen Weise organisiert. Dabei handelt es sich um einen Standard, der festlegt, wie jeder Datensatz angezeigt wird, wobei Schlüsseldaten wie der Schweregrad des Ereignisses, das Datum und die Uhrzeit des Auftretens, die Quelle des Protokolls und die zugehörige beschreibende Meldung erfasst werden.
Diese Organisation ist für die Lesbarkeit und Nützlichkeit von Protokollen unerlässlich. Ohne ein Format können die Protokolle verwirrend erscheinen und es fehlt ihnen an Kontext, was die Analyse erschwert. Stellen Sie sich zum Beispiel ein unstrukturiertes Protokoll vor, in dem einfach steht:
DEBUG: Order sent successfully, server responded in 32msVergleichen wir dies nun mit einem strukturierten Protokoll, das nicht nur dieselben Informationen enthält, sondern auch einen Kontext bietet:
[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms
Dieser kleine Unterschied kann bei der Diagnose von Problemen, insbesondere bei komplexen Systemen, einen großen Unterschied ausmachen. Das Format verwandelt Rohdaten in eine zusammenhängende Erzählung, die dem Entwickler hilft, das Verhalten des Systems zu verstehen.
Flexibilität ist auch ein wichtiger Aspekt der Protokollformate. Jedes Projekt hat spezifische Anforderungen, und die Möglichkeit, das Format anzupassen, ermöglicht nützliche Anpassungen, wie z. B. die Hervorhebung kritischer Ereignisse, die Verfolgung von Quellen oder die Anreicherung von Nachrichten mit kontextbezogenen Metadaten.
Grundstruktur eines Formatierers
Die Grundstruktur eines Formatierers basiert auf dem Konzept der „Platzhalter", d. h. auf Elementen, die als Ersetzungspunkte in einer Vorlage dienen. Diese Platzhalter legen fest, wo und wie Informationen aus einem Protokoll in eine endgültige Meldung eingefügt werden.
Stellen Sie sich einen Formatierer als eine Maschine vor, die Rohdaten aus einem Protokollereignis in ein lesbares, anpassbares Format umwandelt. Die Eingabe kann ein Datenmodell sein, das Werte wie Schweregrad, Nachricht, Zeit und andere Details enthält. Der Formatierer wendet diese Werte dann auf die vom Nutzer bereitgestellte Vorlage an, wodurch ein formatiertes Protokoll entsteht.
Nehmen wir zum Beispiel eine Vorlage wie diese:
{date_time} {levelname}: {msg}Wenn die Werte durch die entsprechenden Platzhalter ersetzt werden, sieht die Ausgabe etwa so aus:
12/04/2025 00:25:45 DEBUG: IFR indicator successfully inserted into the chart!
Die Stärke eines Formatierers liegt in seiner Flexibilität. Hier sind einige Beispiele für Platzhalter, die wir der Bibliothek hinzufügen werden:
- {levelname} : Stellt die Protokollstufe (z.B. DEBUG, ERROR, FATAL) in einer menschenfreundlichen Weise dar.
- {msg} : Die Nachricht, die das protokollierte Ereignis beschreibt.
- {args} : Zusätzliche Daten, die die Nachricht kontextualisieren und häufig zur Erfassung dynamischer Informationen verwendet werden.
- {timestamp} : Der Zeitstempel in Millisekunden, nützlich für die Genauigkeitsanalyse.
- {date_time} : Die für Menschen lesbare Version des Zeitstempels, die Datum und Uhrzeit im Standardformat anzeigt.
- {origin} : Gibt den Ursprung des Ereignisses an, z. B. das Modul oder die Klasse, in der das Protokoll erstellt wurde.
- {filename} , {function} und {line} : Sie identifizieren die genaue Stelle im Code, an der das Protokoll erstellt wurde, um die Fehlersuche effizienter zu gestalten.
Die Logik hinter einem Formatierer ist einfach, aber wirkungsvoll. Sie ermöglicht die Erstellung eines nutzerdefinierten Rahmens für jede Protokollmeldung und gibt dem Entwickler die Möglichkeit, die wichtigsten Informationen für jede Situation zu präsentieren. Dieser Ansatz ist besonders nützlich bei Projekten, bei denen die Datenmenge sehr groß sein kann und eine schnelle Analyse unerlässlich ist.
Mit anpassbaren Vorlagen und umfassenden Platzhaltern wird die Bibliothek ein hochgradig modulares Werkzeug darstellen. Diese Modularität gewährleistet, dass Sie Protokolle erstellen können, die auf die spezifischen Anforderungen Ihrer Anwendung zugeschnitten sind, und erhöht die Effizienz bei der Interpretation und Nutzung der protokollierten Daten.
Implementierung von Formatierern in MQL5
Da wir nun wissen, was Formate und Platzhalter sind, wollen wir uns direkt dem Code zuwenden, um zu verstehen, wie dies in der aktuellen Phase der Bibliothek umgesetzt wird. Zunächst erstellen wir einen neuen Ordner innerhalb von <Include/Logify> mit dem Namen „Formatter". In diesem Ordner erstellen wir die Datei namens LogifyFormatter.mqh. Am Ende wird der Pfad <Include/Logify/Formatter/LogifyFormatter.mqh> lauten. Denken Sie daran, dass ich die endgültigen Dateien, die in dem Artikel verwendet werden, am Ende des Artikels angehängt habe, laden Sie sie einfach herunter und verwenden Sie sie. So sollte der Datei-Explorer aussehen:
//+------------------------------------------------------------------+ //| LogifyFormatter.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" //+------------------------------------------------------------------+ //| class : CLogifyFormatter | //| | //| [PROPERTY] | //| Name : CLogifyFormatter | //| Heritage : No heritage | //| Description : Class responsible for formatting the log into a | //| string, replacing placeholders with their respective values. | //| | //+------------------------------------------------------------------+ class CLogifyFormatter { public: CLogifyFormatter(void); ~CLogifyFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::CLogifyFormatter(void) { } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::~CLogifyFormatter(void) { } //+------------------------------------------------------------------+
Wir deklarieren die privaten Attribute, die das Datum und die Protokollformate speichern:
class CLogifyFormatter { private: //--- Stores the formats string m_date_format; string m_log_format; public: //--- Format query methods string GetDateFormat(void); string GetLogFormat(void); }; //+------------------------------------------------------------------+ //| Get date format | //+------------------------------------------------------------------+ string CLogifyFormatter::GetDateFormat(void) { return(m_date_format); } //+------------------------------------------------------------------+ //| Get the log format | //+------------------------------------------------------------------+ string CLogifyFormatter::GetLogFormat(void) { return(m_log_format); } //+------------------------------------------------------------------+
Hier definieren wir:
- m_date_format : Legt fest, wie die Datumsangaben formatiert werden (z. B. "yyyy/MM/dd hh:mm:ss")
- m_log_format : Legt den Protokollstandard fest (z. B. "{timestamp} - {msg}")
- Und zwei weitere Methoden für den Zugriff auf die privaten Attribute
Der Konstruktor initialisiert die Datums- und Protokollformate und validiert das Protokollformat mit der Methode CheckLogFormat, die wir gleich sehen werden. Zu diesem Zweck habe ich dem Konstruktor zwei Parameter hinzugefügt, um die Definition des Formats gleich bei der Erstellung einer Instanz der Klasse zu erleichtern.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyFormatter::CLogifyFormatter(string date_formate,string log_format) { m_date_format = date_formate; if(CheckLogFormat(log_format)) { m_log_format = log_format; } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyFormatter::~CLogifyFormatter(void) { } //+------------------------------------------------------------------+
Der Destruktor ( ~CLogifyFormatter ) führt keine spezifische Aktion aus, aber es ist eine gute Praxis, ihn zu deklarieren, da er in zukünftigen Situationen nützlich sein kann.
Die Methode CheckLogFormat wird jetzt implementiert. Sie spielt eine wesentliche Rolle bei der Validierung der Vorlagen, die zur Formatierung der Protokolle verwendet werden. Sie soll sicherstellen, dass alle Platzhalter innerhalb der Vorlage ordnungsgemäß strukturiert und geschlossen sind, bevor sie verarbeitet werden. Diese Art der Validierung ist wichtig, um unerwartete Fehler zu vermeiden und die Zuverlässigkeit der Protokollausgaben zu gewährleisten. Sehen wir uns einige Beispiele für ungültige Formate an und verstehen wir die Gründe dafür:
- Nicht geschlossene Platzhalter: Ein Beispiel ist {timestamp} - {{msg} . Hier haben wir ein zusätzliches {-Zeichen, das nicht richtig geschlossen wurde. Diese Art von Fehler deutet auf eine unvollständige Struktur hin, was zu Fehlern bei der Protokollverarbeitung führen kann. - Fehlender öffnende Platzhalter: In Fällen wie {timestamp} - {msg}} gibt es ein überzähliges }-Zeichen, für das es kein { gibt. Wie im vorherigen Beispiel führt dies zu Inkonsistenzen in der Protokollstruktur.
- Leere Platzhalter: Die Vorlage {Zeitstempel} - {msg} {} enthält einen leeren Platzhalter {}, der keinen zugehörigen Schlüssel hat. Jeder Platzhalter muss mit einem gültigen Verweis gefüllt werden, der dynamisch ersetzt wird, und leere Vorlagen brechen diese Erwartung.
- Leeres Format: Die Methode berücksichtigt auch ungültige Fälle, in denen die angegebene Zeichenkette völlig leer ist. Um gültig zu sein, muss die Zeichenfolge mindestens ein Zeichen enthalten, das als Grundlage für das formatierte Protokoll dient.
Wenn die Methode eine dieser Unregelmäßigkeiten feststellt, gibt sie false zurück und gibt detaillierte Fehlermeldungen an den Entwickler aus. Diese Meldungen helfen dabei, Probleme in der bereitgestellten Vorlage schnell zu finden und zu beheben. Ist die Vorlage hingegen korrekt strukturiert und entspricht sie allen Regeln, gibt die Methode true zurück und signalisiert damit, dass sie verwendet werden kann. Die Methode gewährleistet nicht nur Genauigkeit, sondern fördert auch gute Praktiken bei der Gestaltung von Protokollvorlagen, indem sie Entwickler dazu anregt, klare und konsistente Strukturen zu schaffen. Dieser Ansatz verbessert nicht nur die Lesbarkeit der Protokolle, sondern erleichtert auch deren Pflege und Analyse im Laufe der Zeit.
//+------------------------------------------------------------------+ //| Validate format | //+------------------------------------------------------------------+ bool CLogifyFormatter::CheckLogFormat(string log_format) { //--- Variables to track the most recent '{' opening index and the number of '{' brace openings int openIndex = -1; // Index of last '{' found int openBraces = 0; // '{' counter int len = StringLen(log_format); // String length //--- Checks if string is empty if(len == 0) { //--- Prints error message and returns false Print("Erro de formatação: sequência inesperada encontrada. Verifique o padrão de placeholders usado."); return(false); } //--- Iterate through each character of the string for(int i=0;i<len;i++) { //--- Gets the current character ushort character = StringGetCharacter(log_format,i); //--- Checks if the character is an opening '{' if(character == '{') { openBraces++; // Increments the opening counter '{' openIndex = i; // Updates the index of the last opening } //--- Checks if the character is a closing '}' else if(character == '}') { //--- If there is no matching '{' if(openBraces == 0) { //--- Prints error message and returns false Print("Erro de formatação: o caractere '}' na posição ",i," não possui um '{' correspondente."); return(false); } //--- Decreases the open count because a matching '{' was found openBraces--; //--- Extracts the contents of the placeholder (between '{' and '}') string placeHolder = StringSubstr(log_format, openIndex + 1, i - openIndex - 1); //--- Checks if placeholder is empty if(StringLen(placeHolder) == 0) { //--- Prints error message and returns false Print("Erro de formatação: placeholder vazio detectado na posição ",i,". Um nome é esperado dentro de '{...}'."); return(false); } } } //--- After traversing the entire string, check if there are still any unmatched '{'}' if(openBraces > 0) { //--- Prints error message indicating the index of the last opened '{' and returns false Print("Erro de formatação: o placeholder '{' na posição ",openIndex," não possui um '}' correspondente."); return(false); } //--- Format is correct return(true); } //+------------------------------------------------------------------+
Kommen wir nun zu den beiden Hauptmethoden der Klasse, die für die Formatierung der Protokolle zuständig sind:
- FormatDate() : Zur Manipulation von Daten
- FormatLog() : Für die Erstellung des Protokollformats selbst
Beide spielen eine zentrale Rolle bei der Anpassung der von der Bibliothek protokollierten Daten. Um diese Methoden erweiterbar und flexibel zu machen, werden sie als virtuell deklariert. Da die Methoden als virtuell deklariert sind, kann FormatDate in abgeleiteten Klassen leicht überschrieben werden, um bestimmten Szenarien gerecht zu werden. Eine nutzerdefinierte Implementierung könnte beispielsweise das Datumsformat so anpassen, dass es die Zeitzone oder andere zusätzliche Informationen enthält. Diese flexible Architektur ermöglicht es der Bibliothek, sich mit den Anforderungen der Projekte weiterzuentwickeln und ihre Eignung für verschiedene Kontexte zu gewährleisten.
Die Methode FormatDate ist für die Umwandlung eines Datetime-Objekts in eine formatierte Zeichenkette zuständig, die an den in m_date_format definierten Standard angepasst ist. Dieses Muster verwendet ein System von Platzhaltern, die dynamisch durch die entsprechenden Elemente eines Datums, wie Jahr, Monat, Tag, Uhrzeit usw., ersetzt werden.
Dieser Ansatz ist unglaublich flexibel und ermöglicht hochgradig angepasste Formate, die für eine Vielzahl von Szenarien geeignet sind. Sie können z. B. wählen, ob nur der Tag und der Monat angezeigt werden sollen, oder ob Sie vollständige Informationen wie den Wochentag und die Uhrzeit angeben möchten. Nachstehend finden Sie die verfügbaren Platzhalter:
- Jahr
- yy→2-stellige Jahreszahl (z. B. "25").
- yyyy → 4-stelliges Jahr (z. B. "2025").
- Monat
- M → Monat ohne führende Null (z. B. "1" für Januar).
- MM → 2-stelliger Monat (z. B. "01" für Januar).
- MMM → Monatskürzel (z. B. "Jan").
- MMMM → Vollständiger Monatsname (z. B. "January").
- Tag
- d → Tag ohne führende Null (z. B. "1").
- dd → 2-stelliger Tag (z. B. "01").
- Tag des Jahres
- D → Tag des Jahres ohne führende Null (z. B. "32" für den 1. Februar). - DDD → Dreistelliger Tag des Jahres (z. B. "032").
- Tag der Woche
- E → abgekürzter Name des Wochentags (z. B. "Mo").
- EEEE → Vollständiger Name des Wochentags (z. B. "Monday").
- Stunden im 24-Stunden-Format
- H → Stunde ohne führende Null (z. B. "9").
- HH → Zweistellige Stunde (z. B. "09").
- Stunden im 12-Stunden-Format
- h → Stunde ohne führende Null (z. B. "9").
- hh → Zweistellige Stunde (z.B. "09").
- Minute
- m → Minute ohne führende Null (z. B. "5").
- mm → Zweistellige Minute (z. B. "05").
- Sekunde
- s → Sekunde ohne führende Null (z. B. "9").
- ss → Sekunde mit zwei Ziffern (z. B. "09").
- AM/PM
- tt → Rückgabe in Kleinbuchstaben (am/pm)
- TT → Rückgabe in Großbuchstaben (AM/PM)
Diese Logik macht es äußerst bequem, die Datumsanzeige nach Ihren Bedürfnissen anzupassen. Bei Verwendung des Formats "yyyy-MM-dd HH:mm:ss" würde die Ausgabe zum Beispiel so aussehen: "2025-01-02 14:30:00" . Bei Verwendung von "EEEE, MMM tt, jjjj" würde die Ausgabe „Tuesday, Jul 30, 2019" lauten. Diese Anpassungsfähigkeit ist unerlässlich, um sowohl informative als auch visuell klare Protokolle zu erstellen.
Die Logik hinter FormatLog basiert auf der nativen MQL5-Funktion StringReplace(). Diese Funktion führt direkte Ersetzungen von Zeichenketten durch, wobei alle Vorkommen einer bestimmten Teilzeichenkette durch eine andere ersetzt werden. Im Rahmen der Methode FormatLog werden Platzhalter wie {timestamp} oder {message} durch echte Werte aus dem Protokollmodell ersetzt. Dadurch werden Instanzen eines Modells wie MqlLogifyModel in organisierte Daten umgewandelt, die dann visualisiert werden können.
Hier ist der Code für die Implementierung in der Klasse, ich habe mehrere Kommentare hinzugefügt, um den Code so didaktisch wie möglich zu gestalten:
//+------------------------------------------------------------------+ //| class : CLogifyFormatter | //| | //| [PROPERTY] | //| Name : CLogifyFormatter | //| Heritage : No heritage | //| Description : Class responsible for formatting the log into a | //| string, replacing placeholders with their respective values. | //| | //+------------------------------------------------------------------+ class CLogifyFormatter { //--- Date and log formatting methods virtual string FormatDate(datetime date); virtual string FormatLog(MqlLogifyModel &data); }; //+------------------------------------------------------------------+ //| Formats dates | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatDate(datetime date) { string formated = m_date_format; //--- Date and time structure MqlDateTime time; TimeToStruct(date, time); //--- Array with months and days of the week in string string months_abbr[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; string months_full[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; string day_of_week_abbr[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; string day_of_week_full[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; //--- Replace year StringReplace(formated, "yyyy", IntegerToString(time.year)); StringReplace(formated, "yy", IntegerToString(time.year % 100, 2, '0')); //--- Replace month if(StringFind(formated,"MMM") < 0 && StringFind(formated,"MMMM") < 0) { StringReplace(formated, "MM", IntegerToString(time.mon, 2, '0')); StringReplace(formated, "M", IntegerToString(time.mon)); } //--- Replace day StringReplace(formated, "dd", IntegerToString(time.day, 2, '0')); StringReplace(formated, "d", IntegerToString(time.day)); //--- Replace day of year StringReplace(formated, "DDD", IntegerToString(time.day_of_year, 3, '0')); StringReplace(formated, "D", IntegerToString(time.day_of_year)); //--- Replace Replace hours (24h and 12h) StringReplace(formated, "HH", IntegerToString(time.hour, 2, '0')); StringReplace(formated, "H", IntegerToString(time.hour)); int hour_12 = time.hour % 12; if (hour_12 == 0) hour_12 = 12; StringReplace(formated, "hh", IntegerToString(hour_12, 2, '0')); StringReplace(formated, "h", IntegerToString(hour_12)); //--- Replace minutes and seconds StringReplace(formated, "mm", IntegerToString(time.min, 2, '0')); StringReplace(formated, "m", IntegerToString(time.min)); StringReplace(formated, "ss", IntegerToString(time.sec, 2, '0')); StringReplace(formated, "s", IntegerToString(time.sec)); //--- Replace AM/PM bool is_am = (time.hour < 12); StringReplace(formated, "tt", is_am? "am" : "pm"); StringReplace(formated, "TT", is_am? "AM" : "PM"); //--- Replace month StringReplace(formated, "MMMM", months_full[time.mon - 1]); StringReplace(formated, "MMM", months_abbr[time.mon - 1]); //--- Replace day of week StringReplace(formated, "EEEE", day_of_week_full[time.day_of_week]); StringReplace(formated, "E", day_of_week_abbr[time.day_of_week]); return(formated); } //+------------------------------------------------------------------+ //| Format logs | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatLog(MqlLogifyModel &data) { string formated = m_log_format; //--- Replace placeholders StringReplace(formated,"{timestamp}",IntegerToString(data.timestamp)); StringReplace(formated,"{level}",IntegerToString(data.level)); StringReplace(formated,"{origin}",data.origin); StringReplace(formated,"{message}",data.message); StringReplace(formated,"{metadata}",data.metadata); return(formated); } //+------------------------------------------------------------------+
Damit ist die Konstruktion der Klasse abgeschlossen, jetzt wollen wir einige Aktualisierungen an MqlLogifyModel vornehmen.
Hinzufügen weiterer Daten zu MqlLogifyModel
Das MqlLogifyModel ist eines der Kernelemente unserer Logging-Bibliothek und stellt die Basisstruktur für die Speicherung und Bearbeitung der Daten zu jedem Log-Event dar. In ihrem derzeitigen Zustand ist die Struktur wie folgt definiert:struct MqlLogifyModel { ulong timestamp; // Date and time of the event ENUM_LOG_LEVEL level; // Severity level string origin; // Log source string message; // Log message string metadata; // Additional information in JSON or text MqlLogifyModel::MqlLogifyModel(void) { timestamp = 0; level = LOG_LEVEL_DEBUG; origin = ""; message = ""; metadata = ""; } MqlLogifyModel::MqlLogifyModel(ulong _timestamp,ENUM_LOG_LEVEL _level,string _origin,string _message,string _metadata) { timestamp = _timestamp; level = _level; origin = _origin; message = _message; metadata = _metadata; } };
Diese ursprüngliche Version funktioniert gut in einfachen Szenarien, aber wir können sie erheblich verbessern, indem wir mehr Informationen hinzufügen und die Eigenschaftsnamen verfeinern, um die Verwendung zu erleichtern und das Modell an bewährte Verfahren anzupassen. Im Folgenden werden wir die geplanten Verbesserungen erörtern.
Namesvereinfachung der Eigenschaft- Das Nachrichtenfeld wird in msg umbenannt. Diese Änderung ist zwar unauffällig, reduziert aber die Anzahl der Zeichen beim Zugriff auf die Eigenschaft und macht den Code einfacher zu schreiben und zu lesen.
- metadata wird durch args ersetzt, da der neue Name die Funktion der Eigenschaft besser widerspiegelt: die Speicherung der mit dem Protokoll verbundenen Kontextdaten zum Zeitpunkt seiner Erstellung.
Hinzufügen von neuen Feldern
Um die Protokolle anzureichern und eine detailliertere Analyse zu ermöglichen, werden die folgenden Felder aufgenommen:
- formatted : Die Protokollnachricht wird entsprechend dem angegebenen Format formatiert. Es wird verwendet, um die endgültige Version des Protokolls zu speichern, wobei Platzhalter bereits durch echte Werte ersetzt werden. Diese Eigenschaft ist schreibgeschützt.
- levelname : Bezeichnung der Protokollstufe in Textform (z. B. „DEBUG“, „INFO“). Dies ist nützlich, wenn das Format eine Beschreibung des Schweregrads erfordert.
- date_time : Stellt das Datum und die Uhrzeit des Ereignisses im Datetime-Format dar, eine Alternative zu timestamp .
- filename : Der Name der Datei, in der das Protokoll erstellt wurde, ist wichtig, um die genaue Quelle zu ermitteln.
- function : Name der Funktion, in der das Protokoll aufgerufen wurde, um mehr Informationen über den Ursprung des Ereignisses zu erhalten.
- line : Zeilennummer in der Quelldatei, in der das Protokoll erstellt wurde. Dies kann besonders bei der Fehlersuche nützlich sein.
Mit diesen neuen Feldern wird das MqlLogifyModel robuster und ist auf unterschiedliche Protokollierungsanforderungen vorbereitet, wie z. B. detaillierte Fehlersuche oder Integration mit externen Überwachungswerkzeugen. Nach den Änderungen sieht der Code wie folgt aus:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ struct MqlLogifyModel { string formated; // The log message formatted according to the specified format. string levelname; // Textual name of the log level (e.g., "DEBUG", "INFO") string msg; // Main content of the log message string args; // Additional arguments associated with the log message ulong timestamp; // Timestamp of the log event, represented in seconds since the start of the Unix epoch datetime date_time; // Date and time of the log event, in datetime format ENUM_LOG_LEVEL level; // Enumeration representing the severity level of the log string origin; // Source or context of the log message (e.g., class or module) string filename; // Name of the source file where the log message was generated string function; // Name of the function where the log was called ulong line; // Line number in the source file where the log was generated MqlLogifyModel::MqlLogifyModel(void) { formated = ""; levelname = ""; msg = ""; args = ""; timestamp = 0; date_time = 0; level = LOG_LEVEL_DEBUG; origin = ""; filename = ""; function = ""; line = 0; } MqlLogifyModel::MqlLogifyModel(string _formated,string _levelname,string _msg,string _args,ulong _timestamp,datetime _date_time,ENUM_LOG_LEVEL _level,string _origin,string _filename,string _function,ulong _line) { formated = _formated; levelname = _levelname; msg = _msg; args = _args; timestamp = _timestamp; date_time = _date_time; level = _level; origin = _origin; filename = _filename; function = _function; line = _line; } }; //+------------------------------------------------------------------+Als nächsten Schritt werden wir die Methode FormatLog der Klasse CLogifyFormatter aktualisieren, um die Unterstützung für die den neuen Eigenschaften entsprechenden Platzhalter aufzunehmen. Nachstehend finden Sie die aktualisierte Version der Methode, die jetzt mit allen neuen Modelleigenschaften kompatibel ist:
//+------------------------------------------------------------------+ //| Format logs | //+------------------------------------------------------------------+ string CLogifyFormatter::FormatLog(MqlLogifyModel &data) { string formated = m_log_format; StringReplace(formated,"{levelname}",data.levelname); StringReplace(formated,"{msg}",data.msg); StringReplace(formated,"{args}",data.args); StringReplace(formated,"{timestamp}",IntegerToString(data.timestamp)); StringReplace(formated,"{date_time}",this.FormatDate(data.date_time)); StringReplace(formated,"{level}",IntegerToString(data.level)); StringReplace(formated,"{origin}",data.origin); StringReplace(formated,"{filename}",data.filename); StringReplace(formated,"{function}",data.function); StringReplace(formated,"{line}",IntegerToString(data.line)); return(formated); } //+------------------------------------------------------------------+
In der Methode FormatLog wird der Wert von {date_time} durch die formatierte Rückgabe von FormatDate ersetzt, sodass dieser Platzhalter durch das zuvor übergebene Datumsformat ersetzt wird.
Anwendung der Formatierung auf die Protokolle
Stellen wir sicher, dass unsere CLogify-Klasse einen Formatierer zum Formatieren von Protokollnachrichten verwenden kann. Wir importieren die Klasse, die dafür zuständig ist, und fügen ein Attribut hinzu, um es zu speichern:
#include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh"
Der nächste Schritt besteht darin, der Klasse CLogify das Attribut m_formatter hinzuzufügen, um die Formatierungsinstanz zu speichern und die Methoden zur Konfiguration und zum Zugriff darauf zu erstellen. So können wir den Formatierer an verschiedenen Stellen im System wiederverwenden:
//+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifyFormatter *m_formatter; public: //--- Get/Set object formatter void SetFormatter(CLogifyFormatter *format); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogify::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogify::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
Die vorhandenen Protokollierungsmethoden haben eine begrenzte Anzahl von Parametern, wie z. B. Zeitstempel, Protokollstufe, Nachricht, Quelle und Metadaten. Wir werden diesen Ansatz verbessern, indem wir neue Parameter hinzufügen, die den Kontext des Protokolls beschreiben, wie z. B.:
- filename : Name der Datei, in der das Protokoll aufgetreten ist.
- function : Name der Funktion, in der das Protokoll aufgetreten ist.
- line : Codezeile, die das Protokoll erzeugt hat.
Um die Signatur der Methode intuitiver zu gestalten, werden wir außerdem die Parameternamen anpassen. Hier ein Beispiel, wie die Hauptmethode ( Append ) geändert wurde:
bool CLogify::Append(ulong timestamp, ENUM_LOG_LEVEL level, string message, string origin = "", string metadata = "");
Angepasste Methode
bool CLogify::Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0);
Damit ist unsere neue Implementierung:
//+------------------------------------------------------------------+ //| 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); //--- Printing the formatted log Print(m_formatter.FormatLog(data)); return(true); } //+------------------------------------------------------------------+
Beachten Sie, dass ich den Rückgabewert der Funktion FormatLog ausdrucke, die das Datenobjekt zurückgibt.
Nachdem die grundlegende Append-Methode nun mit detaillierten Parametern funktioniert, können wir weitere spezialisierte Methoden für jede Protokollstufe (Debug, Infor, Alert, Error und Fatal) anpassen oder hinzufügen. Bei diesen Methoden handelt es sich um „shortcuts", die den Protokollierungsgrad automatisch festlegen und gleichzeitig die Verwendung aller anderen Parameter ermöglichen.
//+------------------------------------------------------------------+ //| 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)); } //+------------------------------------------------------------------+
Praktisches Beispiel
Ich werde zeigen, wie man die Klasse CLogify mit einem nutzerdefinierten Format für Protokolle konfiguriert. Wir werden mit einfachen Beispielen beginnen und nach und nach die Komplexität erhöhen, um die Flexibilität dieser Lösung zu verdeutlichen.
1. Grundlegende Konfiguration eines Protokolls
Für dieses Beispiel verwenden wir die Testdatei LogifyTest.mq5 , die im ersten Artikel erstellt wurde. Die anfängliche Konfiguration umfasst die Erstellung einer Instanz von CLogify und die Definition des grundlegenden Formats für die Protokollmeldungen. Hier ist der Code:
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} => {msg}")); //--- Log a simple message logify.Debug("Application initialized successfully."); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Wenn der Code ausgeführt wird, wird das Format des erzeugten Protokolls etwa so aussehen:
[DEBUG] 07:25:32 => Application initialized successfully.
Beachten Sie, dass wir ein vereinfachtes Zeitformat ( hh:mm:ss ) verwenden und die Meldung so strukturiert ist, dass sie die Protokollstufe und die aktuelle Zeit anzeigt. Dies ist das einfachste Beispiel für die Verwendung von CLogify .
2. Hinzufügen von Details mit Quellennachweis
Erweitern wir nun das Beispiel, um Informationen wie die Protokollquelle und zusätzliche Parameter aufzunehmen. Dies ist nützlich für komplexere Systeme, bei denen die Protokolle angeben müssen, welcher Teil des Systems die Meldung erzeugt.
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} {args}")); //--- Log a simple message logify.Debug("Connection established with the server.", "Network"); logify.Alert("Configuration file not found!", "Config", "Attempt 1 of 3"); return(INIT_SUCCEEDED); }Dieser Code ergibt die folgende Ausgabe:
[DEBUG] 07:26:18 (Network) => Connection established with the server. [ALERT] 07:26:19 (Config) => Configuration file not found! Attempt 1 of 3
Hier fügen wir Ursprungsparameter und Kontextargumente hinzu, um die Protokollmeldung zu bereichern.
3. Erweiterte Metadaten verwenden
In robusteren Systemen ist es oft notwendig, die Datei, Funktion und Zeile zu identifizieren, die das Protokoll erzeugt hat. Passen wir das Beispiel an, um diese Informationen einzubeziehen:
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","[{levelname}] {date_time} ({origin}) => {msg} (File: {filename}, Line: {line})")); //--- Log a simple message logify.Error("Error accessing database.", "Database", "", __FILE__, __FUNCTION__, __LINE__); return(INIT_SUCCEEDED); }Bei der Ausführung des obigen Codes ergibt sich folgendes Bild:
[ERROR] 07:27:15 (Database) => Error accessing database. (File: LogifyTest.mq5, Line: 25)
Jetzt haben wir detaillierte Informationen, die es uns ermöglichen, genau zu verfolgen, wo der Fehler im Code aufgetreten ist.
4. Anpassung der Formate und Integration in größere Systeme
Als letztes Beispiel zeigen wir, wie man Formate anpasst und verschiedene Arten von Protokollmeldungen innerhalb einer Schleife erzeugt, die die Ausführung eines größeren Systems simuliert:
int OnInit() { //--- Configure log format logify.SetFormatter(new CLogifyFormatter("yyyy.MM.dd hh:mm:ss","{date_time} [{levelname}] {msg} - {origin} ({filename}:{line})")); //--- Cycle simulating various system operations for(int i=0; i<3; i++) { logify.Debug("Operation in progress...", "TaskProcessor", "", __FILE__, __FUNCTION__, __LINE__); if(i == 1) { logify.Alert("Possible inconsistency detected.", "TaskValidator", "", __FILE__, __FUNCTION__, __LINE__); } if(i == 2) { logify.Fatal("Critical error, purchase order not executed correctly!", "Core", "", __FILE__, __FUNCTION__, __LINE__); } } return(INIT_SUCCEEDED); }Die Ausführung führt zu der folgenden Ausgabe:
2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [ALERT] Possible inconsistency detected. - TaskValidator (LogifyTest.mq5:28) 2025.01.03 07:25:32 [DEBUG] Operation in progress... - TaskProcessor (LogifyTest.mq5:25) 2025.01.03 07:25:32 [FATAL] Critical error, purchase order not executed correctly! - Core (LogifyTest.mq5:32)
Dieses Beispiel simuliert einen realen Arbeitsablauf, wobei die Meldungen je nach Schweregrad der Situation eskaliert werden. Das Protokollformat ist sehr detailliert und zeigt die komplette Zeit, die Protokollstufe, die Nachricht und die Stelle im Code an.
Schlussfolgerung
In diesem Artikel erfahren Sie, wie Sie die Log-Formate in MQL5 mit Hilfe der Bibliothek Logify anpassen und anwenden können. Wir beginnen mit einer Einführung, in der wir erläutern, was ein Protokollformat ist und wie wichtig dieses Verfahren für die Überwachung und Fehlersuche in Anwendungen ist.
Anschließend behandeln wir die Grundstruktur eines Formatierers, der das Herzstück der Protokollanpassung ist, und wie seine Anwendung in MQL5 die Protokolle aussagekräftiger und zugänglicher machen kann. Darauf aufbauend zeigen wir, wie diese Formatierer implementiert werden können, wobei wir die Möglichkeiten der Anpassung und der Aufnahme zusätzlicher Daten in das MqlLogifyModel-Modell hervorheben.
Im weiteren Verlauf werden wir uns damit befassen, wie wir den Protokollen mehr Kontextdaten hinzufügen können, um sie informativer zu machen, z. B. die genaue Quelle einer Meldung (Datei, Funktion und Zeile) zu identifizieren. Außerdem wird erläutert, wie diese Formatierer konfiguriert und auf Protokolle angewandt werden können, um eine Protokollausgabe zu gewährleisten, die den spezifischen Anforderungen eines jeden Projekts entspricht.
Abschließend wird anhand eines praktischen Beispiels gezeigt, wie verschiedene Komplexitätsstufen in Protokollen implementiert werden können, von einfachen Konfigurationen bis hin zu robusten Systemen mit erweiterten und detaillierten Protokollen. Dieses Beispiel konsolidiert die gewonnenen Erkenntnisse, indem es die Theorie mit der realen Praxis der in MQL5 entwickelten Systeme verbindet.
Nachfolgend sehen Sie das Diagramm mit der Bibliothek in ihrem derzeitigen Zustand:
Der gesamte in diesem Artikel verwendete Code ist unten angefügt. Hier ist eine Tabelle mit der Beschreibung der einzelnen Bibliotheksdateien:
Datei Name | Beschreibung |
---|---|
Experts/Logify/LogiftTest.mq5 | Datei, in der wir die Funktionen der Bibliothek testen, mit einem praktischen Beispiel |
Include/Logify/Formatter/LogifyFormatter.mqh | Klasse, die für die Formatierung von Protokolldatensätzen zuständig ist, indem sie Platzhalter durch bestimmte Werte ersetzt |
Include/Logify/Logify.mqh | Kernklasse für die Protokollverwaltung, die Ebenen, Modelle und Formatierung integriert |
Include/Logify/LogifyLevel.mqh | Datei, die die Log-Ebenen der Logify-Bibliothek definiert und eine detaillierte Kontrolle ermöglicht |
Include/Logify/LogifyModel.mqh | 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/16833





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