English
preview
ログレコードをマスターする(第2回):ログのフォーマット処理

ログレコードをマスターする(第2回):ログのフォーマット処理

MetaTrader 5 | 22 4月 2025, 10:14
22 0
joaopedrodev
joaopedrodev

はじめに

本連載最初の記事、「ログレコードをマスターする(第1回):MQL5の基本概念と最初のステップ」では、エキスパートアドバイザー(EA)開発向けのカスタムログライブラリの構築に着手しました。そこでは、MetaTrader 5の標準ログ機能の制限を克服し、MQL5の世界において柔軟性・拡張性に優れた強力なソリューションを提供するという目的を明らかにしました。

前回の記事で取り上げた主なポイントを振り返ると、私たちは次のような基本要件をもとにライブラリの基盤を構築しました。

  1. シングルトンによる堅牢な構造で、コードコンポーネント間の一貫性を確保
  2. データベースへの永続的な保存により、詳細な監査や分析を可能にする追跡可能な履歴の実現
  3. 柔軟な出力形式に対応し、コンソール、ファイル、ターミナル、データベースなど多様な出力先をサポート
  4. ログレベルによる分類で、情報メッセージと重大なアラート・エラーを明確に区別
  5. 出力フォーマットのカスタマイズにより、開発者やプロジェクトごとの個別ニーズに対応

このような強固な基盤を構築したことで、私たちのログフレームワークは単なるイベントログではなく、EAの動作をリアルタイムで把握・監視・最適化するための戦略的ツールとしての価値を持つことが明らかになりました。

そして今回の第2回では、ライブラリにおける最も重要な機能のひとつであるログのフォーマット処理に焦点を当てます。というのも、効果的なログとは「が記録されるか」だけでなく、「その情報がどのように提示されるか」が極めて重要だからです。たとえば、重要なEAテストの最中に、読みにくく整理されていないメッセージを受け取ったとしたらどうでしょうか。それは分析を不必要に複雑にするだけでなく、貴重な情報を見落とすリスクにもつながります。私たちの目標は、ログが常に明確で必要な情報を正確に伝えると同時に、開発者のニーズに合わせて柔軟にカスタマイズ可能であることです。


ログフォーマットとは

ログフォーマットとは、プログラムの実行中に記録される情報を、明確で理解しやすい形に整理するための構造です。ログの各エントリがどのように表示されるかを定義する「標準」として機能し、イベントの重大度レベル、発生日時、ログの出所(ソース)、および関連する説明メッセージなど、重要なデータを一つにまとめます。

このような整理がされていることで、ログは読みやすく、そして実際に使えるものになります。逆に、フォーマットが存在しないと、ログは雑然としたものになり、文脈が不足し、分析が非常に困難になる可能性があります。たとえば、以下のような非構造的なログを想像してみてください。

DEBUG: Order sent successfully, server responded in 32ms
では、同じ情報に加えてコンテキストも提供する、構造化されたフォーマットに従ったログと比較してみましょう。
[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms

このわずかな違いが、特に複雑なシステムにおいては、問題の診断に大きな影響を与えることがあります。フォーマットによって、生のデータが一貫性のあるストーリーに変わり、開発者がシステムの挙動を理解する手助けとなるのです。

ログフォーマットの柔軟性もまた、重要な側面です。各プロジェクトにはそれぞれ特有のニーズがあり、フォーマットを適応させることで、重大なイベントを目立たせたり、ソースを追跡したり、コンテキストに関するメタデータを加えてメッセージを強化したりといった有用なカスタマイズが可能になります。


フォーマッターの基本構造

フォーマッターの基本構造は、「プレースホルダー」と呼ばれる要素の概念に基づいています。プレースホルダーは、テンプレート内の置換ポイントとして機能し、ログの情報が最終的なメッセージにどのように挿入されるかを定義します。

フォーマッターを、ログイベントから得られた生データを読みやすくカスタマイズ可能なフォーマットに変換するマシンだと考えてください。入力としては、重大度レベル、メッセージ、時間、その他の詳細を含むデータモデルを使用できます。その後、フォーマッターはユーザーが指定したテンプレートにこれらの値を適用し、フォーマットされたログを生成します。

たとえば、次のようなテンプレートを考えてみましょう。

{date_time} {levelname}: {msg}
値が対応するプレースホルダーに置き換えられると、出力は次のようになります。
12/04/2025 00:25:45 DEBUG: IFR indicator successfully inserted into the chart!

フォーマッターの強みはその柔軟性にあります。以下は、ライブラリに追加するプレースホルダーのいくつかの例です。

  • {levelname}:ログレベル(DEBUG、ERROR、FATALなど)を人間にわかりやすい方法で表します。
  • {msg}:ログに記録されたイベントの説明メッセージ
  • {args}:メッセージのコンテキストを補足する追加データで、動的な情報をキャプチャする際に使用されます。
  • {timestamp}:精度分析に役立つミリ秒単位のタイムスタンプ
  • {date_time}:タイムスタンプの人間に優しい形式で表示された日付と時刻
  • {origin}:イベントが発生したモジュールやクラスなど、ログの発信元を示します。
  • {filename}{function}{line}:ログが生成されたコード内の正確な位置を特定し、デバッグ作業を効率化します。

フォーマッターの背後にあるロジックはシンプルですが非常に強力です。これにより、開発者は各ログメッセージにカスタムフレームを作成し、状況に最も関連性の高い情報を表示できるようになります。このアプローチは、特にデータ量が多く、高速な分析が求められるプロジェクトにおいて非常に有用です。

カスタマイズ可能なテンプレートと多彩なプレースホルダーを備えたこのライブラリは、非常にモジュール化されたツールを提供します。このモジュール性によって、アプリケーションのニーズに合わせたログの作成が可能となり、記録されたデータの解釈と利用が効率的になります。


MQL5でのフォーマッターの実装

フォーマットとプレースホルダーが何であるかを理解したので、コードに直接進み、ライブラリの現在の段階でこれがどのように実装されるかを理解しましょう。まず、<Include/Logify>内に「Formatter」という新しいフォルダを作成しましょう。このフォルダー内に、LogifyFormatter.mqhというファイルを作成します。最終的に、パスは<Include/Logify/Formatter/LogifyFormatter.mqh>になります。この記事で使用した最終的なファイルを記事の最後に添付したので、ダウンロードして使用してください。ファイルエクスプローラーは次のようになります。

CLogifyFormatterは主に、開発者が定義したテンプレートに基づいてログメッセージをフォーマット処理することを目的としています。また、メッセージ内のカスタム日付フォーマットとプレースホルダーもサポートします。まず、クラスを宣言し、依存関係を追加します。
//+------------------------------------------------------------------+
//|                                              LogifyFormatter.mqh |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/ja/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/ja/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)
  {
  }
//+------------------------------------------------------------------+

日付とログのフォーマットを保存するprivate属性を宣言します。

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);
  }
//+------------------------------------------------------------------+

ここでは以下を定義します。

  • m_date_format:日付のフォーマット処理方法(例:「yyyy/MM/dd hh:mm:ss」)
  • m_log_format:ログの標準フォーマット(例:「{timestamp} - {msg}」)
  • private属性にアクセスするための他の2つのメソッド

コンストラクタは日付とログのフォーマットを初期化し、後ほど説明するCheckLogFormatメソッドを使用してログのフォーマットを検証します。これを実現するために、コンストラクタに2つのパラメータを追加しました。これにより、クラスのインスタンスを作成する際にフォーマットを定義しやすくなります。

//+------------------------------------------------------------------+
//| 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)
  {
  }
//+------------------------------------------------------------------+

デストラクタ(~CLogifyFormatter)は特定のアクションを実行しませんが、将来の状況で役立つ可能性があるため、宣言することをお勧めします。

CheckLogFormatメソッドに関しては、これから実装します。これは、ログのフォーマット処理に使用されるテンプレートの検証において重要な役割を果たします。その目的は、テンプレート内のすべてのプレースホルダーが処理される前に適切に構造化され、閉じられていることを確認することです。このタイプの検証は、予期しないエラーを回避し、ログ出力の信頼性を確保するために不可欠です。無効なフォーマットの例をいくつか見て、その理由を理解しましょう。

  • 閉じていないプレースホルダー:例は「{timestamp} - {{msg}」です。ここでは、適切に閉じられていない「{」が追加されています。このタイプのエラーは不完全な構造を示しており、ログ処理の失敗につながる可能性があります。 -開かないプレースホルダー:「{timestamp} - {msg}}」のような場合、どの「{」とも一致しない余分な「}」が存在します。前の例と同様に、これによりログ構造に不整合が生じます。
  • 空のプレースホルダー:テンプレート「{timestamp} - {msg} {}」には、関連付けられたキーを持たない空のプレースホルダー「{}」が含まれています。各プレースホルダーは、動的に置き換えられる有効な参照で埋められる必要があり、空のテンプレートはこの期待を破ります。
  • 空のフォーマット:メソッドは、提供された文字列が完全に空である場合も無効と見なします。文字列が有効であるためには、少なくとも1文字が含まれている必要があり、それがフォーマット処理されたログの基礎となります。

メメソッドはこれらの不規則性を検出すると、falseを返し、開発者に詳細なエラーメッセージを出力します。これらのメッセージは、提供されたテンプレート内の問題を迅速に特定し、修正するのに役立ちます。一方、テンプレートが適切に構造化され、すべてのルールを満たしている場合、メソッドはtrueを返し、使用可能であることを示します。この方法は、正確性を確保するだけでなく、ログテンプレートの設計における良いプラクティスを促進し、開発者が明確で一貫性のある構造を作成することを奨励します。このアプローチにより、ログの可読性が向上し、また長期的なログの保守や分析も容易になります。

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

次に、ログのフォーマットを担当するクラスの2つの主要なメソッドに移りましょう。

  • FormatDate():日付を操作する
  • FormatLog():ログフォーマット自体を作成する

これらは両方とも、ライブラリによって記録されるデータのカスタマイズにおいて中心的な役割を果たします。これらのメソッドを拡張可能かつ柔軟にするために、これらはvirtualとして宣言されます。メソッドが仮想として宣言されているため、派生クラスでFormatDateを簡単にオーバーライドして、特定のシナリオに適した実装をおこなうことができます。たとえば、カスタム実装では、日付フォーマットを調整してタイムゾーンやその他の追加情報を含めることができます。この柔軟なアーキテクチャにより、ライブラリはプロジェクトの要求に応じて進化し、さまざまなコンテキストに適応できるようになります。

FormatDateメソッドは、datetimeオブジェクトをm_date_formatで定義された標準に基づいてフォーマットされた文字列に変換する役割を担います。このパターンでは、年、月、日、時刻などの日付の各要素を動的に置き換えるプレースホルダーシステムを使用します。

このアプローチは非常に柔軟で、さまざまなシナリオに合わせた高度にカスタマイズされたフォーマットを実現できます。例えば、日付と月のみを表示することも、曜日や時刻などの完全な情報を含めることも選択できます。使用可能なプレースホルダーは次のとおりです。

  • Year
    • yy→2桁の年(例:「25」)
    • yyyy→4桁の年(例:「2025」)
  • Month
    • M→先頭にゼロのない月(例:1月は「1」)
    • MM→2桁の月(例:1月は「01」)
    • MMM→月の略語(例:「Jan」)
    • MMMM→月の完全な名前(例:「January」)
  • Day
    • d→先頭にゼロのない日(例:「1」)
    • dd→2桁の日(例:「01」)
  • 年間通算日
    • D → 先頭にゼロのない年間通算日(例:2月1日は「32」)。- DDD → 3桁の年間通算日(例:「032」)
  • 曜日
    • E→曜日の略称(例:「Mon」)
    • EEEE→曜日のフルネーム(例:「Monday」)
  • 24時間表記の時間
    • H→先頭にゼロのない時間(例:「9」)
    • HH→2桁の時間(例:「09」)
  • 12時間表記の時間
    • h→先頭にゼロのない時間(例:「9」)
    • hh→2桁の時間(例:「09」)
    • m→先頭にゼロのない分(例:「5」)
    • mm→2桁の分(例:「05」)
    • s→先頭にゼロを除いた秒(例:「9」)
    • ss→2桁の秒(例:「09」)
  • 午前/午後
    • tt→小文字(am/pm)
    • TT→大文字(AM/PM)

このロジックにより、ニーズに応じて日付表示をカスタマイズすることが非常に便利になります。たとえば、「yyyy-MM-ddHH:mm:ss」フォーマットを使用すると、出力は「2025-01-0214:30:00」のようになります。「EEEE, MMM dd, yyyy」を使用すると、出力は「Tuesday, Jul 30, 2019」のようになります。この柔軟性は、情報量が豊富で視認性にも優れたログを提供するために不可欠です。

FormatLogの背後にあるロジックは、MQL5ネイティブ関数のStringReplace()に基づいています。この関数は直接的な文字列置換を実行し、特定の部分文字列のすべての出現を別の部分文字列に置き換えます。FormatLogメソッドのコンテキストでは、{timestamp}や{message}などのプレースホルダーは、ログモデルの実際の値に置き換えられます。これにより、MqlLogifyModelなどのモデルのインスタンスが、視覚化できる整理されたデータに変換されます。

以下はクラスでの実装のコードです。コードをできるだけわかりやすくするために、いくつかコメントを追加しました。

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

これでクラスの構築は完了です。次に、MqlLogifyModelにいくつか更新を加えてみましょう。


MqlLogifyModelにデータを追加する

MqlLogifyModelは、ログライブラリのコア要素の1つであり、各ログイベントに関連するデータを保存および操作するための基本構造を表します。現状では、この構造体は次のように定義されています。
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;
     }
  };

この初期バージョンは単純なシナリオでは十分に機能しますが、さらなる情報の追加やプロパティ名の見直しを行うことで、モデルの使いやすさを向上させ、ベストプラクティスに沿った形に大幅に改善することが可能です。以下では、計画されている改善について説明します。

プロパティ名の簡素化
  • messageフィールドはmsgに名前変更されます。この変更は些細ではありますが、プロパティにアクセスする際の文字数が減り、コードの記述と読み取りが容易になります。
  • metadataはargsに置き換えられます。新しい名前は、プロパティの機能(ログが生成された時点でログに関連付けられたコンテキストデータを保存する)をより正確に反映するからです。

新しいフィールドの追加

ログを充実させ、より詳細な分析を可能にするために、次のフィールドが追加されます。

  • formatted:指定されたフォーマットに従ってフォーマット処理されたログメッセージ。これは、プレースホルダーを実際の値に置き換えた、ログの最終バージョンを保存するために使用されます。このプロパティは読み取り専用になります。
  • levelname:ログレベルのテキスト名(例:「DEBUG」、「INFO」)。これは、フォーマットで重大度レベルの説明が必要な場合に役立ちます。
  • date_time:イベントの日時を、timestampの代替としてdatetimeフォーマットで表します。
  • filename:ログが生成されたファイルの名前。正確なソースを追跡するために不可欠です。
  • function:ログが呼び出された関数の名前。イベントの発生元に関する詳細なコンテキストを提供します。
  • line:ログが生成されたソースファイル内の行番号。これは、デバッグのシナリオで特に役立ちます。

これらの新しいフィールドにより、MqlLogifyModelはより堅牢になり、詳細なデバッグや外部監視ツールとの統合など、さまざまなログ記録要件を満たす準備が整います。変更後、コードは次のようになります。

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
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;
     }
  };
//+------------------------------------------------------------------+
次のステップとして、CLogifyFormatterクラスのFormatLogメソッドを更新して、新しいプロパティに対応するプレースホルダーのサポートを追加します。以下は、すべての新しいモデルプロパティと互換性があるメソッドの更新バージョンです。
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

FormatLogメソッドでは、{date_time}の値はFormatDateのフォーマットされた戻り値に置き換えられるため、このプレースホルダーは以前に渡された日付フォーマットに置き換えられます。


ログにフォーマッターを適用する

CLogifyクラスがフォーマッターを使用してログメッセージをフォーマットできることを確認しましょう。これを担当するクラスをインポートし、それを保存するための属性を追加します。

#include "LogifyModel.mqh"
#include "Formatter/LogifyFormatter.mqh"

次のステップでは、CLogifyフォーマッタークラスにフォーマッターm_formatterフォーマッター属性を追加してフォーマッターインスタンスを保存し、それを構成およびアクセスするためのメソッドを作成します。これにより、システム内のさまざまなポイントでフォーマッターを再利用できるようになります。

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

既存のログ記録方法には、タイムスタンプ、ログレベル、メッセージ、ソース、メタデータなどのパラメータのセットが限られています。次のようなログのコンテキストを記述する新しいパラメータを追加することで、このアプローチを改善します。

  • filename:ログが発生したファイルの名前
  • function:ログが発生した関数の名前
  • line:ログを生成したコード行

さらに、メソッドシグネチャをより直感的にするために、パラメータ名を調整します。メインメソッド「Append」がどのように変更されたかの例を次に示します。

bool CLogify::Append(ulong timestamp, ENUM_LOG_LEVEL level, string message, string origin = "", string metadata = "");

変更後:

bool CLogify::Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0);

新しい実装は次のようになります。

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

FormatLog関数の戻り値を出力し、データオブジェクトを返していることに注意してください。

詳細なパラメータを使用して基底Appendメソッドが機能するようになったので、各ログレベル(Debug、Infor、Alert、Error、Fatal)に合わせて他の特殊なメソッドを調整または追加することができます。これらのメソッドは、他のすべてのパラメータを使用できるようにしながら、ログレベルを自動的に設定する「ショートカット」です。

//+------------------------------------------------------------------+
//| 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));
  }
//+------------------------------------------------------------------+


実例

ログにカスタムフォーマットが設定されたCLogifyクラスの使用方法を説明します。まずは基本的な例から始めて、徐々に複雑な例を追加し、このソリューションの柔軟性を示していきます。

1.ログの基本構成

この例では、最初の記事で作成したテストファイル「LogifyTest.mq5」を使用します。初期設定では、CLogifyのインスタンスを作成し、ログメッセージの基本フォーマットを定義します。次がコードです。

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

コードを実行すると、生成されるログのフォーマットは次のようになります。

[DEBUG] 07:25:32 => Application initialized successfully.

簡略化された時刻フォーマット(hh:mm:ss)が使用されており、メッセージはログレベルと現在の時刻を表示するように構成されていることに注意してください。これはCLogifyを使用する最も基本的な例です。

2 ソース識別による詳細の追加

ここで、ログソースや追加のパラメータなどの情報を含めるように例を拡張してみましょう。これは、システムのどの部分がメッセージを生成しているかをログで示す必要がある、より複雑なシステムに役立ちます。

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);
  }
このコードは次の出力を生成します。
[DEBUG] 07:26:18 (Network) => Connection established with the server.
[ALERT] 07:26:19 (Config) => Configuration file not found! Attempt 1 of 3

ここでは、ログメッセージを充実させるために、元のパラメータとコンテキスト引数を追加します。

3.高度なメタデータの使用

より堅牢なシステムでは、ログを生成したファイル、関数、行を識別することが必要になることがよくあります。この情報を含めるように例を調整してみましょう。

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);
  }
上記のコードを実行すると、次のようになります。
[ERROR] 07:27:15 (Database) => Error accessing database. (File: LogifyTest.mq5, Line: 25)

詳細な情報が得られるようになったため、コード内のどこでエラーが発生したかを正確に追跡できるようになりました。

4.フォーマットのカスタマイズと大規模システムへの統合

最後の例として、大規模なシステムの実行をシミュレートするループ内でフォーマットをカスタマイズし、さまざまなタイプのログメッセージを生成する方法を示します。

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);
  }
実行すると次の出力が生成されます。

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)

この例では、状況の重大度に応じてメッセージがエスカレートされる、実際の操作フローをシミュレートします。ログフォーマットは非常に詳細で、完全な時間、ログレベル、メッセージ、コード内の場所が表示されます。


結論

この記事では、Logifyライブラリの機能を活用して、MQL5におけるログフォーマットのカスタマイズおよび適用方法について包括的に解説しました。まず初めに、ログフォーマットとは何か、そしてアプリケーションの監視やデバッグにおいてその実践がいかに重要であるかを概念的に紹介しました。

続いて、ログカスタマイズの中核を成すフォーマッターの基本構造と、そのMQL5における活用がログの可読性や有用性をどのように高めるかを解説しました。その上で、実際にフォーマッターを実装する方法を示し、MqlLogifyModelモデルに対してカスタマイズや追加データの組み込みが可能である点を強調しました。

さらに、ファイル名、関数名、行番号といったメッセージの発生源を特定するためのコンテキスト情報の追加によって、ログの情報価値を高めるプロセスについても取り上げました。加えて、これらのフォーマッターをどのように構成・適用し、プロジェクトごとのニーズに合ったログ出力を実現するかについても詳しく説明しました。

そして最後に、基本的な構成から、高度かつ詳細なログを備えた堅牢なシステムまで、さまざまなレベルの複雑さに対応したログ実装の実用例を紹介しました。この例を通じて、理論的な知識とMQL5での実践を結びつけ、学んだ内容を確かな形で定着させました。

以下に、現在の段階におけるライブラリの構成図を示します。

この記事で使用したすべてのコードは以下に添付されています。各ライブラリファイルの説明を記載した表を以下に示します。

ファイル名
詳細
Experts/Logify/LogiftTest.mq5
ライブラリの機能をテストするファイル。実用的な例が含まれています。
Include/Logify/Formatter/LogifyFormatter.mqh
ログレコードのフォーマット、プレースホルダーを特定の値に置き換えるクラス
Include/Logify/Logify.mqh
ログ管理、レベル、モデル、フォーマットの統合のためのコアクラス
Include/Logify/LogifyLevel.mqh
Logifyライブラリのログレベルを定義するファイル。詳細な制御が可能
Include/Logify/LogifyModel.mqh
レベル、メッセージ、タイムスタンプ、コンテキストなどの詳細を含むログレコードをモデル化する構造

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16833

添付されたファイル |
LogifycPart2v.zip (7.87 KB)
Candlestick Trend Constraintモデルの構築(第10回):戦略的ゴールデンクロスとデスクロス(EA) Candlestick Trend Constraintモデルの構築(第10回):戦略的ゴールデンクロスとデスクロス(EA)
移動平均線のクロスオーバーに基づくゴールデンクロスおよびデッドクロス戦略は、長期的な市場トレンドを見極める上で最も信頼性の高い指標の一つであることをご存知でしょうか。ゴールデンクロスは、短期移動平均線が長期移動平均線を上回るときに強気トレンドの到来を示します。一方、デッドクロスは、短期移動平均線が長期線を下回ることで弱気トレンドの兆候を示します。これらの戦略は非常にシンプルでありながら効果的ですが、手動で運用すると機会の逸失やエントリーの遅れが発生しやすいという課題があります。しかし、MQL5を活用してTrend Constraintエキスパートアドバイザー(EA)内で自動化することで、これらの戦略は独立して機能し、市場の反転に迅速かつ効率的に対応できるようになります。また、制約付きの戦略と組み合わせることで、広範なトレンドと整合性を保つことができます。このアプローチにより、反転戦略とトレンドフォロー戦略のシームレスな統合が実現され、精密なエントリーと一貫したパフォーマンス向上をもたらします。
古典的な戦略を再構築する(第13回):移動平均線のクロスオーバーにおける遅延の最小化 古典的な戦略を再構築する(第13回):移動平均線のクロスオーバーにおける遅延の最小化
移動平均クロスオーバーは、私たちのコミュニティにおけるトレーダーの間で広く知られている戦略ですが、その基本的な仕組みは誕生以来ほとんど変化していません。本稿では、この戦略に存在する“遅延”を最小限に抑えることを目的とした、わずかながらも重要な改良について紹介します。元の戦略を愛用しているトレーダーの方々にも、今回ご紹介する洞察をもとに、戦略の見直しを検討していただければ幸いです。同一の期間を持つ2つの移動平均を使用することで、戦略の根本的な原則を損なうことなく、遅延を大幅に削減することが可能になります。
MQL5取引ツールキット(第5回):ポジション関数による履歴管理EX5ライブラリの拡張 MQL5取引ツールキット(第5回):ポジション関数による履歴管理EX5ライブラリの拡張
エクスポート可能なEX5関数を作成して、過去のポジションデータを効率的にクエリおよび保存する方法を解説します。このステップバイステップのガイドでは、直近にクローズされたポジションの主要なプロパティを取得するモジュールを開発し、HistoryManagement EX5ライブラリを拡張していきます。対象となるプロパティには、純利益、取引時間、ピップ単位でのストップロスやテイクプロフィット、利益値、その他多くの重要な情報が含まれます。
MQL5での取引戦略の自動化(第3回):ダイナミック取引管理のためのZone Recovery RSIシステム MQL5での取引戦略の自動化(第3回):ダイナミック取引管理のためのZone Recovery RSIシステム
この記事では、MQL5を使ってZone Recovery RSI EAシステムを構築し、RSIシグナルによって取引を開始し、損失を管理するためのリカバリーストラテジーを実装します。取引エントリー、リカバリーロジック、ポジション管理を自動化するために、ZoneRecoveryクラスを作成します。この記事の最後では、EAのパフォーマンスを最適化し、その有効性を高めるためのバックテストの洞察を紹介します。