ログレコードをマスターする(第10回):抑制機能を実装してログの再表示を防ぐ
はじめに
本記事は、Logifyライブラリのユーザーからの直接の要望を受けて作成されました。ユーザーから指摘があった問題は、多くの人が実際に直面する課題です。ログの量が増えすぎると、繰り返しや無関係なメッセージが履歴を汚染し、本当に重要な情報を見つけにくくなってしまうということです。もし他にご意見や質問、挑戦してほしい課題がありましたら、記事の最後にコメントを残してください。この場所は皆さんのためにあり、いただいた声を反映することでライブラリは進化していきます。
まず、「ログ抑制」が何を意味するのかを理解することが重要です。簡単に言うと、抑制とは記録されるログメッセージを制御するプロセスであり、過剰、冗長、または情報の汚染を避けることを目的としています。システムが生成するすべての情報をただ出力するのではなく、有用で関連性のあるメッセージのみを、適切なタイミングでログに残すことができるのです。
本記事では、柔軟かつ効率的に設計された、Logify向けの実用的なログ抑制システムの実装例を紹介します。連続した同一メッセージの回避、同じログの出現頻度の制限、最大繰り返し回数の管理や、ログの発生元やファイルによるフィルタリングなど、さまざまな制御方法の組み合わせ方を紹介します。これらはすべて、ビット単位のモードに基づくインテリジェントなシステムによって実現されており、複数のルールを同時に簡単に有効化できます。
この記事を読み進めることで、ログを効率的かつスリムに保つ堅牢なソリューションの作成方法を習得できます。過剰なログを自動的に抑制するだけでなく、分析を容易にしノイズを削減する明確なルールを適用する方法も理解できます。これは特に本番環境で有用であり、過剰なログがパフォーマンスを妨げ、保守作業を困難にすることを防ぎます。最終版のライブラリは記事の最後に添付されており、ダウンロード可能です。
ファイルの整理
Logifyライブラリのプロジェクト内で、メインの「Logify」フォルダの中に「Suppression」という新しいフォルダを作成します。これによりコードを整理しやすくなり、「ログの抑制」に関する機能がすべてここに集まっていることが明確になります。次に、「Suppression」フォルダ内に、「LogifySuppression.mqh」という新しいファイルを作成します。このファイルが、新しい抑制クラスの出発点となります。このクラスは、実際にログに表示されるメッセージを制御し、繰り返しや過剰なログを回避する役割を果たします。
最初は、クラスは非常にシンプルで、空のコンストラクタとデストラクタだけを持つ形で構いません。たとえば、次のような構造です。
//+------------------------------------------------------------------+ //| LogifySuppression.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CLogifySuppression { private: public: CLogifySuppression(void); ~CLogifySuppression(void); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CLogifySuppression::CLogifySuppression(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CLogifySuppression::~CLogifySuppression(void) { } //+------------------------------------------------------------------+
これはまだ実装は何もおこなっていない、きれいな出発点です。ファイルの構造を整え、正しくインクルードされることを確認するための準備段階です。
拡張:インポートと定義
このクラスに機能を持たせるためには、まずログデータモデルであるLogifyModel.mqhをインポートする必要があります。このモデルは、処理のために受け取るメッセージの形式を定義しています。さらに、抑制パラメータを検証するための定数も定義します。これにより、無効な設定を避けるために、最小値と最大値の制限を保証することができます。
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../LogifyModel.mqh" //+------------------------------------------------------------------+ //| Validation constants | //+------------------------------------------------------------------+ #define MAX_SUPPRESSION_MODE 255 // Maximum valid mode combination #define MIN_THROTTLE_SECONDS 1 // Minimum interval between messages #define MIN_REPEAT_COUNT 1 // Minimum number of repetitions
ビット演算による抑制モード
繰り返しや過剰なログを回避するさまざまな方法を制御するためには、基本的な概念であるビット演算付き列挙型について理解する必要があります。
では、これは具体的に何でしょうか。プログラミングにおいて列挙型は、名前付き定数の集合を整理して定義する方法です。たとえば、DEBUG、INFO、ERRORなどのログレベルを表す列挙型を作ることができます。一方、ビット演算は、整数を構成するビットに直接作用する演算です。ビットはスイッチのようなもので、オン(1)かオフ(0)になります。列挙型とビット演算を組み合わせると、2の累乗を表すユニークな値、つまり1、2、4、8、16…のような値を作ることができます。これらの値は、2進数のそれぞれのビットに対応しています。
なぜビット演算付き列挙型を使うのでしょうか。たとえば、連続して繰り返されるメッセージの制限と、同じメッセージが何度も出ることの制限を、同時に適用したい場合を考えます。単純な列挙型値だけを作った場合、1回に1つのモードしか選べません。それではシステムの柔軟性が制限されてしまいます。ビット演算付き列挙型を使うと、対応するビットを有効にすることで、複数のモードを組み合わせて適用することができます。この組み合わせは、ビット演算のOR演算子「|」を用いておこない、選択したモードのビットをひとつの数値として「連結」することができます。
では、実際に私たちが使用している定義を見てみましょう。
enum ENUM_LOG_SUPRESSION_MODE { LOG_SUPRESSION_MODE_NONE = 0, // No suppression LOG_SUPRESSION_MODE_CONSECUTIVE = 1 << 0, // 00001 = 1: Identical consecutive messages LOG_SUPRESSION_MODE_THROTTLE_TIME = 1 << 1, // 00010 = 2: Same message within X seconds LOG_SUPRESSION_MODE_BY_REPEAT_COUNT = 1 << 2, // 00100 = 4: After N repetitions LOG_SUPRESSION_MODE_BY_ORIGIN = 1 << 3, // 01000 = 8: Based on message origin LOG_SUPRESSION_MODE_BY_FILENAME = 1 << 4, // 10000 = 16: Based on source filename };
ここで、「1 << N」は「1をN回左にシフト」することを意味します。シフトごとに1ビットが作成されます。
- 1 << 0 = 1(2進数00001)
- 1 << 1 = 2(2進数00010)
- 1 << 2 = 4(2進数00100)
- 1 << 3 = 8(2進数01000)
- 1 << 4 = 16(2進数10000)
複数のモードを同時に有効にしたい場合は、単純に値をOR演算子「|」で組み合わせます。
int mode = LOG_SUPRESSION_MODE_CONSECUTIVE | LOG_SUPRESSION_MODE_THROTTLE_TIME; // 1 | 2 = 3 (00011)
この数値3(2進数で00011)は、最初の2つのモードが同時に有効であることを示しています。では、これがログ抑制にどのように役立つのでしょうか。
- 柔軟性:システムは複数の抑制ルールを同時に受け入れることができ、可能な組み合わせごとに別の列挙値を作成する必要がありません。
- 効率性:モードが有効かどうかの確認は簡単かつ高速で、ANDビット演算子「&」を使って、そのモードのビットがオンかどうかを確認するだけです。
- 拡張性:将来的に新しい抑制モードを追加したい場合、列挙型に新しいビットを追加するだけで、既存の動作を壊すことなく拡張できます。
たとえば、「発生元による抑制」モードが有効かどうかを確認する場合は、次のようにおこないます。
if((mode & LOG_SUPRESSION_MODE_BY_ORIGIN) == LOG_SUPRESSION_MODE_BY_ORIGIN) { // Apply filter by source }
対応するビットがオンの場合、条件はtrueになります。
構造体による設定
可能な抑制モードを定義した後は、すべての設定ロジックをひとつの場所にまとめる時です。ここで登場するのがMqlLogifySuppressionConfig構造体です。この構造体は「コントロールパネル」のように機能し、ログメッセージをどのように、いつ抑制するかを定義します。アイデアはシンプルです。抑制動作を制御するパラメータを格納し、EA、インジケーター、補助ライブラリのいずれでも、クリーンで再利用可能な設定を提供することです。
では、中身を詳しく見ていきましょう。
-
mode:抑制モードの組み合わせ
これは設定の核心です。ここでは、ビット演算を用いたintで抑制モードの組み合わせを格納します。これにより、開発者は複数の抑制モードを同時に有効化できます。たとえば:
- 連続した同一ログの抑制
- 一定時間内の繰り返しメッセージの無視
- X回の繰り返し後の抑制
-
throttle_seconds:時間による制限
このフィールドは、同一メッセージが再び表示されるまでの間隔(秒)を定義します。関数が1秒間に複数回同じログを発生させる場合に有効で、コンソールがすぐに汚染され、読みにくくなるのを防ぎます。
-
max_repeat_count:回数による制限
ここでは、同じメッセージが抑制される前に表示される最大回数を定義します。たとえば、数回発生したエラーを確認したい場合に役立ちますが、無限に表示され続けることは避けられます。
-
発生元やファイルによる許可/拒否リスト
開発者は、特定のコードから来るメッセージだけに抑制を適用したり、特定の発生元を完全に除外したりしたい場合があります。
そのため、この構造体には4つの配列があります。
- allowed_origins[]:指定すると、この発生元だけが許可されます。
- blocked_origins[]:ここにリストされた発生元は常に抑制されます。
- allowed_filenames[]:同じ概念ですが、発生元ファイル名に適用されます。
- blocked_filenames []:ファイルは常にブロックされます。
これらのフィールドにより、極めて細かい制御が可能です。たとえば、自分のメインEAからのログは許可し、ノイズの多いサードパーティライブラリのログはすべて抑制するといった設定が可能です。
次に、デフォルト値を持つコンストラクタを定義します。取引システム向けのログ抑制クラスでは、ログスパムを防ぎつつ重要な情報を隠さない、バランスの取れたデフォルト設定が重要です。以下のデフォルト設定を提案し、理由も示します。
- mode = LOG_SUPRESSION_MODE_THROTTLE_TIME | LOG_SUPRESSION_MODE_CONSECUTIVE | LOG_SUPRESSION_MODE_BY_REPEAT_COUNT:基本的な3つの抑制モードを組み合わせています。重要な情報を失わずにログスパムを防ぎ、追跡可能な履歴を維持します。
- throttle_seconds = 5:5秒はほとんどのケースでバランスが良く、重要な変化を見逃さず、かつ追跡可能性を保つのに十分な間隔です。
- max_repeat_count = 15:15回の繰り返しは、パターンの特定やデバッグに十分であり、問題発生時のログの洪水を避けることができます。
//+------------------------------------------------------------------+ //| Struct: MqlLogifySuppressionConfig | //+------------------------------------------------------------------+ struct MqlLogifySuppressionConfig { // Basic configuration int mode; // Combination of suppression modes int throttle_seconds; // Seconds between messages int max_repeat_count; // Max repetitions before suppression // Origin whitelist/blacklist string allowed_origins[]; // If not empty, only these are allowed string blocked_origins[]; // Always blocked // Filename whitelist/blacklist string allowed_filenames[]; // If not empty, only these are allowed string blocked_filenames[]; // Always blocked //--- Default constructor MqlLogifySuppressionConfig(void) { mode = LOG_SUPRESSION_MODE_THROTTLE_TIME | LOG_SUPRESSION_MODE_CONSECUTIVE | LOG_SUPRESSION_MODE_BY_REPEAT_COUNT; throttle_seconds = 5; max_repeat_count = 15; ArrayResize(allowed_origins, 0); ArrayResize(blocked_origins, 0); ArrayResize(allowed_filenames, 0); ArrayResize(blocked_filenames, 0); } //--- Destructor ~MqlLogifySuppressionConfig(void) { } }; //+------------------------------------------------------------------+
ユーザーがArrayResize()やインデックスを用いて配列を手動で操作するのを防ぐために、2つのヘルパーメソッドも定義します。また、AddAllowedOrigin()やAddBlockedFilename()などの実用的なメソッドを作成しています。これにより、構成が明確になり、読みやすくなり、間違いが起こる可能性が低くなります。
//+------------------------------------------------------------------+ //| Struct: MqlLogifySuppressionConfig | //+------------------------------------------------------------------+ struct MqlLogifySuppressionConfig { //--- Helper methods for array configuration void AddAllowedOrigin(string origin) { int size = ArraySize(allowed_origins); ArrayResize(allowed_origins, size + 1); allowed_origins[size] = origin; } void AddBlockedOrigin(string origin) { int size = ArraySize(blocked_origins); ArrayResize(blocked_origins, size + 1); blocked_origins[size] = origin; } void AddAllowedFilename(string filename) { int size = ArraySize(allowed_filenames); ArrayResize(allowed_filenames, size + 1); allowed_filenames[size] = filename; } void AddBlockedFilename(string filename) { int size = ArraySize(blocked_filenames); ArrayResize(blocked_filenames, size + 1); blocked_filenames[size] = filename; } }; //+------------------------------------------------------------------+
最後に、ValidateConfig()メソッドを追加しました。これは、指定された値が妥当であるかを確認し、不正な設定による障害を防ぐためのものです。主な検証内容は以下のとおりです。
- throttle_secondsは最小閾値以上である(0や負の値を防ぐ)
- max_repeat_countは、規定されたシンボリック値より大きい
- modeの値は、有効な組み合わせである
このメソッドは、問題がある場合にfalseを返し、併せてerror_messageにその内容を設定します。これはデバッグ時だけでなく、ユーザー向けの分かりやすいエラーメッセージを表示したいアプリケーションでも有用です。
//+------------------------------------------------------------------+ //| Struct: MqlLogifySuppressionConfig | //+------------------------------------------------------------------+ struct MqlLogifySuppressionConfig { //--- Validates configuration parameters bool ValidateConfig(string &error_message) { if(throttle_seconds < MIN_THROTTLE_SECONDS) { error_message = "throttle_seconds must be greater than or equal to " + (string)MIN_THROTTLE_SECONDS; return false; } if(max_repeat_count < MIN_REPEAT_COUNT) { error_message = "max_repeat_count must be greater than or equal to " + (string)MIN_REPEAT_COUNT; return false; } if(mode < LOG_SUPRESSION_MODE_NONE || mode > MAX_SUPPRESSION_MODE) { error_message = "invalid suppression mode"; return false; } return true; } }; //+------------------------------------------------------------------+
この構造体を使うことで、いくつかのコマンドだけでログ抑制を設定でき、動作を細かく制御しつつ、すべての値が許容範囲に収まっていることを保証できます。コード中にロジックが散在することもなく、すべてをクリーンかつ拡張性の高い形で集中管理できる点も特徴です。
CLogifySuppressionの発展
構成用のstructureが整ったので、次はこの設定をリアルタイムで適用する役割を持つクラス、「CLogifySuppression」を構築していきます。このクラスは、各ログが出力されるたびに、そのメッセージをコンソールに表示すべきかどうかを、現在有効なルールに基づいて判断します。
実際の抑制ロジックを実装する前に、このクラスが外部から動作指示を受け取れるようにしておくことが重要です。つまり、ルールや制限値、例外リストといった設定はクラス外部から渡され、実行ロジックとパラメータ指定の方法が分離されている必要があります。このロジックと設定の分離こそが、システムに柔軟性と再利用性を与える要素になります。このため、抑制クラスには以下の2つのメソッドを追加しました。
class CLogifySuppression { public: //--- Configuration management void SetConfig(MqlLogifySuppressionConfig &config); MqlLogifySuppressionConfig GetConfig(void) const; }; //+------------------------------------------------------------------+ //| Updates suppression configuration | //+------------------------------------------------------------------+ void CLogifySuppression::SetConfig(MqlLogifySuppressionConfig &config) { m_config = config; string err_msg = ""; if(!m_config.ValidateConfig(err_msg)) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg); } } //+------------------------------------------------------------------+ //| Returns current configuration | //+------------------------------------------------------------------+ MqlLogifySuppressionConfig CLogifySuppression::GetConfig(void) const { return m_config; } //+------------------------------------------------------------------+
SetConfig()メソッドは、MqlLogifySuppressionConfigstructのオブジェクトを参照渡しで受け取り、その内容を内部に保持します。その後、構造体自身が持つValidateConfig()メソッドを呼び出して自動検証を実行します。throttle_secondsが許容最小値を下回っている、modeが不正であるなど、設定が許容範囲外であれば、その場ですぐにエラーを出力し問題を知らせます。これにより、実行途中でのサイレントエラーを防ぎ、デバッグの手間を減らし、ランタイム中に動的に設定された場合でもシステムを健全に保つことができます。
GetConfig()メソッドは、現在保持している設定の状態を参照するためのものです。大規模システムにおいて、診断やデバッグ、抑制ルールを表示するUIを構築する場合などに役立ちます。これにより、抑制の設定は正式な構造体で定義され、検証され、中央集約された管理対象となります。
private変数の宣言
設定が揃ったので、次のステップは、その設定を呼び出し履歴に基づいて適用するためのデータを保持することです。抑制ロジックは、前に何が起きたかを「覚えておく」必要があります。たとえば、直前に記録したメッセージや、同じメッセージが何回繰り返されたかといった情報を保持しておく必要があります。そこで、クラスに以下のprivateフィールドを追加しました。
class CLogifySuppression { private: //--- Configuration MqlLogifySuppressionConfig m_config; //--- State tracking string m_last_message; ENUM_LOG_LEVEL m_last_level; int m_repeat_count; datetime m_last_time; };
それぞれの役割を理解しましょう。
- m_config:現在の構成構造体インスタンスです。メッセージが評価されるたびに、ここで定義されたルール(throttle_secondsの最小間隔、max_repeat_countの許容回数、sourceやfileリストなど)と照合されます。
- m_last_message:抑制フィルタを通過した直近のメッセージ内容を保持します。新しいメッセージが前回と同じかどうかを判定するための基準であり、連続した繰り返し検出における重要な要素です。
- m_last_level:前回処理したメッセージのレベル(info、warning、errorなど)を保持します。同じメッセージ文字列であってもレベルによって意味が異なるため、この値は重要です。たとえば「Info: connection lost」と「Error: connection lost」は同じ扱いにすべきではありません。
- m_repeat_count:同一メッセージが連続で何回現れたかをカウントします。メッセージ内容とレベルが前回と一致した場合にインクリメントされます。この値が設定された上限を超えると、抑制が発動される可能性があります。
- m_last_time:最後にログが受理された時刻(タイムスタンプ)を記録します。前回のメッセージからの経過時間を計算し、非常に短い間隔で発生するログを抑制するthrottleモードを正しく適用するための基盤になります。
これらの変数はすべて、抑制処理の内部状態を表します。システムが、過去に何が起きたかを記憶しながらルールを適用できるようにするもので、新しいメッセージを表示すべきかどうかを、信頼性とパフォーマンスを両立しつつ判断するために不可欠です。
ShouldSuppress()メソッドの作成
ここまでで、外部設定、内部状態、モード定義といった基盤が整いました。次のステップは、抑制システムの中心となるShouldSuppress()メソッドを構築することです。このメソッドはログが発行されるたびに呼び出され、引数として受け取るMqlLogifyModelには、そのログに関するすべての情報(メッセージ、レベル、発生元、日時、ファイル名)が含まれています。ShouldSuppress()の役割は、現在の設定に基づいて、そのログを表示すべきか、それとも抑制すべきかを判断することです。
まずは、メソッドの論理的な土台として、最もシンプルなモードの処理から始めます。
class CLogifySuppression { private: //--- Main suppression logic bool ShouldSuppress(MqlLogifyModel &data); }; //+------------------------------------------------------------------+ //| Checks if a message should be suppressed based on active modes | //+------------------------------------------------------------------+ bool CLogifySuppression::ShouldSuppress(MqlLogifyModel &data) { datetime now = data.date_time; //--- Reset counters if message or level changed if(data.msg != m_last_message || data.level != m_last_level) { m_repeat_count = 0; m_last_message = data.msg; m_last_level = data.level; m_last_time = now; return false; } //--- Increment counter once per check m_repeat_count++; //--- Check suppression modes if(((m_config.mode & LOG_SUPRESSION_MODE_BY_REPEAT_COUNT) == LOG_SUPRESSION_MODE_BY_REPEAT_COUNT) && m_repeat_count >= m_config.max_repeat_count) { return true; } if(((m_config.mode & LOG_SUPRESSION_MODE_THROTTLE_TIME) == LOG_SUPRESSION_MODE_THROTTLE_TIME) && (now - m_last_time) < m_config.throttle_seconds) { return true; } if((m_config.mode & LOG_SUPRESSION_MODE_CONSECUTIVE) == LOG_SUPRESSION_MODE_CONSECUTIVE) { return true; } m_last_time = now; return false; } //+------------------------------------------------------------------+
ここでは、次の3つのモードを扱います。
- 連続性による抑制:同じメッセージが連続して複数回記録された場合、最初のメッセージのみが表示されます。
- 繰り返し回数による抑制:連続する同一メッセージをすべて抑制するのではなく、何回までは表示してよいかという許容回数を設定できるようにします。この回数を超えた時点で抑制が開始されます。
- 時間間隔による抑制:メッセージが同一であっても、前回のログから設定値未満の短い間隔で発生した場合にのみ抑制します。
これは、多くのケースで既に動作します。しかし、さらにもう一段の精度が必要です。それは、ログの発生元(originフィールド)やファイル(filename)に応じてログを無視する能力です。この機能は、開発者が特定のシステムコンポーネントから発生するログを隠したい場合に非常に有用です。たとえば、外部ライブラリの内部ログや、特定の.mq5/.mqhファイルから出力される非常に冗長なデバッグメッセージなどを非表示にすることができます。
発生元とファイル名による抑制の追加(インテリジェント検索付き)
より複雑な環境では、複数のシステムコンポーネントやモジュールが同時にログを生成するため、開発者が特定の部分からのログだけを抑制したいという状況がよく発生します。たとえば、自動売買システムからのメッセージや、副次的なインジケータが生成するログなどがその例です。このため、originフィールド(ログの論理的な発生元)およびfilenameフィールド(ログを生成したMQLファイル名)に基づいてログを抑制できる機能を追加しました。
最初のバージョンでは、完全一致による単純な文字列比較を使うこともできます。しかし、これは実用上かなり制限が大きい方法です。たとえば、ソースが「Trade.Signal」で、抑制対象として「signal」を指定した場合、完全一致では当然マッチせず、抑制が機能しません。そこで、StringContainsIgnoreCase()というヘルパーメソッドを作成しました。このメソッドは、大文字小文字を無視した部分一致検索をおこない、比較をより柔軟かつ寛容にします。
その実装は次のとおりです。
class CLogifySuppression { private: //--- Helper methods for string comparison bool StringContainsIgnoreCase(string text, string search_term); }; //+------------------------------------------------------------------+ //| Checks if a string contains another string (case insensitive) | //+------------------------------------------------------------------+ bool CLogifySuppression::StringContainsIgnoreCase(string text, string search_term) { string text_lower = text; string term_lower = search_term; StringToLower(text_lower); StringToLower(term_lower); return StringFind(term_lower, text_lower) >= 0; } //+------------------------------------------------------------------+
この関数は、部分文字列の出現を検索する前に、両方のテキストを小文字に変換します。つまり、「Trade.Signal」が「signal」や「trade」と関連付けて認識されるようになり、ソース名を完全に書き下す必要がなくなります。これにより、ソース間でミニセマンティック階層を作ることが可能です。たとえば、「signal」をブロック対象に設定すると、自動的に「Trade.Signal」「Risk.Signal」「Execution.Signal」からのログも抑制されます。この戦略により、実用的なフィルタを設定するための手間を大幅に削減しつつ、ロジックは明快で効率的に保たれます。
このロジックを抑制システムに適用すると、次のようになります。
//+------------------------------------------------------------------+ //| Checks if a message should be suppressed based on active modes | //+------------------------------------------------------------------+ bool CLogifySuppression::ShouldSuppress(MqlLogifyModel &data) { datetime now = data.date_time; //--- Check origin-based suppression if((m_config.mode & LOG_SUPRESSION_MODE_BY_ORIGIN) == LOG_SUPRESSION_MODE_BY_ORIGIN) { //--- Check blacklist first if(ArraySize(m_config.blocked_origins) > 0) { for(int i = 0; i < ArraySize(m_config.blocked_origins); i++) { if(StringContainsIgnoreCase(data.origin, m_config.blocked_origins[i])) { return true; } } } //--- Then check whitelist if(ArraySize(m_config.allowed_origins) > 0) { bool origin_allowed = false; for(int i = 0; i < ArraySize(m_config.allowed_origins); i++) { if(StringContainsIgnoreCase(data.origin, m_config.allowed_origins[i])) { origin_allowed = true; break; } } if(!origin_allowed) { return true; } } } //--- Check filename-based suppression if((m_config.mode & LOG_SUPRESSION_MODE_BY_FILENAME) == LOG_SUPRESSION_MODE_BY_FILENAME) { //--- Check blacklist first if(ArraySize(m_config.blocked_filenames) > 0) { for(int i = 0; i < ArraySize(m_config.blocked_filenames); i++) { if(StringContainsIgnoreCase(data.filename, m_config.blocked_filenames[i])) { return true; } } } //--- Then check whitelist if(ArraySize(m_config.allowed_filenames) > 0) { bool filename_allowed = false; for(int i = 0; i < ArraySize(m_config.allowed_filenames); i++) { if(StringContainsIgnoreCase(data.filename, m_config.allowed_filenames[i])) { filename_allowed = true; break; } } if(!filename_allowed) { return true; } } } //--- Reset counters if message or level changed if(data.msg != m_last_message || data.level != m_last_level) { m_repeat_count = 0; m_last_message = data.msg; m_last_level = data.level; m_last_time = now; return false; } //--- Increment counter once per check m_repeat_count++; //--- Check suppression modes if(((m_config.mode & LOG_SUPRESSION_MODE_BY_REPEAT_COUNT) == LOG_SUPRESSION_MODE_BY_REPEAT_COUNT) && m_repeat_count >= m_config.max_repeat_count) { return true; } if(((m_config.mode & LOG_SUPRESSION_MODE_THROTTLE_TIME) == LOG_SUPRESSION_MODE_THROTTLE_TIME) && (now - m_last_time) < m_config.throttle_seconds) { return true; } if((m_config.mode & LOG_SUPRESSION_MODE_CONSECUTIVE) == LOG_SUPRESSION_MODE_CONSECUTIVE) { return true; } m_last_time = now; return false; } //+------------------------------------------------------------------+
同様のアプローチをallowed_originsおよびallowed_filenamesフィールドにも適用します。これにより許可リストの作成も可能になります。つまり、特定のログだけを通し、それ以外をすべてブロックするフィルタを作ることができ、従来の拒否リストとは逆の動作になります。
発生元、ファイル名、そして大文字小文字を無視した文字列パターンによるフィルタを組み合わせることで、非常に強力な選択的抑制システムが構築できます。これは、開発者がロボットを作る場合、戦略をバックテストする場合、あるいは本番のリアルタイムシステムを分析する場合など、文脈に応じて寛容にも非常に厳格にも設定可能です。
Reset()メソッドの作成
ログ抑制システムは使用される中で、最後にログされたメッセージ、繰り返し回数、最終出現時刻などの内部情報を蓄積していきます。この内部状態は、抑制ルールを正しく適用するために不可欠です。しかし、場合によってはこの状態を「リセット」することが有効です。
典型的な例として、チャート、銘柄、または時間足を切り替えたときなど、実行コンテキストが変化する場合が挙げられます。このような場合に以前の履歴を保持していると、新しいコンテキストで適切でない抑制がおこなわれ、重要なメッセージが隠されてしまう可能性があります。
この問題を解決するため、Reset()メソッドを作成しました。このメソッドは、クラスがこれまで保持していた内部データをすべてクリアし、まるで最初から開始したかのような状態に戻します。
//+------------------------------------------------------------------+ //| Resets all internal state tracking | //+------------------------------------------------------------------+ void CLogifySuppression::Reset(void) { m_last_message = ""; m_repeat_count = 0; m_last_time = 0; m_last_level = LOG_LEVEL_INFO; } //+------------------------------------------------------------------+
このメソッドは非常にシンプルですが、抑制ロジックの正確性を確保する上で非常に重要です。クラスのコンストラクタが呼ばれる際にも、内部的にReset()を必ず呼び出すようにしています。これにより、クラスの新しいインスタンスは、前回のメッセージ、繰り返し回数、タイムスタンプなどの履歴を引き継ぐことなく、「クリーンな状態」で開始されます。
さらに、必要に応じてReset()を手動で呼び出すことも可能です。たとえば、実行間で抑制を再開させたい場合や、コード内の特定のイベント後に状態をリセットしたい場合などに活用できます。
補助的なGetterの作成
抑制処理が自動化されていても、多くの場合、開発者は、裏側で何が起きているのかを確認したくなります。期待していたログが隠されてしまっている場合、クラスの内部状態を調べ、理由を理解することが有用です。
そこで、抑制の主要な制御変数にアクセスできる、シンプルなpublicメソッド(getters)を追加しました。これらのメソッドはクラスの状態を変更することはなく、診断やログ、デバッグツールで役立つ値を返すだけです。
class CLogifySuppression { public: //--- Monitoring getters int GetRepeatCount(void) const { return m_repeat_count; } datetime GetLastMessageTime(void) const { return m_last_time; } string GetLastMessage(void) const { return m_last_message; } ENUM_LOG_LEVEL GetLastLevel(void) const { return m_last_level; } };
- GetRepeatCount()は、同じメッセージが連続して何回現れたかを返します。これにより、なぜメッセージが抑制されたのか、あるいはされなかったのかを理解する手助けになります。
- GetLastMessageTime()は、最後にメッセージがフィルタを通過した時刻を教えてくれます。これは、時間による抑制が期待通りに動作しているかを検証するために不可欠です。
- GetLastMessage()は、最後に処理されたメッセージの内容を文字列として返します。
- GetLastLevel()は、最後のメッセージのレベル(info、warning、errorなど)を返し、システムの重要度制御と照合する際に役立ちます。
これらのメソッドは、クラスを一般的に使用する際には必須ではありません。しかし、予期せぬ挙動が発生した場合には非常に有用です。自動でメッセージを抑制する仕組みは両刃の剣であり、ノイズを減らせる一方で問題を隠してしまうこともあります。そのため、内部ロジックを確認する手段を持つことは、調査時間を大幅に短縮する上で重要です。
CLogifyメインクラスへのログ抑制の統合
CLogifySuppressionが完成したので、次はこれをライブラリの中核であるCLogifyクラスに統合します。ここでは、メッセージのルーティングやハンドラー制御など、すべての処理がおこなわれています。そして今後は、繰り返しや不要なログを抑制する判断もこのクラスでおこなわれます。まず最初のステップとして、抑制用ファイルをインポートし、CLogifyのprivateメンバーとして抑制クラスのインスタンスを宣言します。
//+------------------------------------------------------------------+ //| Imports | //+------------------------------------------------------------------+ #include "LogifyModel.mqh" #include "Suppression/LogifySuppression.mqh" #include "Handlers/LogifyHandler.mqh" #include "Handlers/LogifyHandlerComment.mqh" #include "Handlers/LogifyHandlerConsole.mqh" #include "Handlers/LogifyHandlerDatabase.mqh" #include "Handlers/LogifyHandlerFile.mqh" #include "Error/LogifyError.mqh" //+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifySuppression*m_suppression; }; //+------------------------------------------------------------------+
このインスタンスは、特定のメッセージをログに出力すべきかどうかを、内部で判断するために使用されます。CLogifyのコンストラクタ内で、このインスタンスを初期化します。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogify::CLogify() { m_suppression = new CLogifySuppression(); } //+------------------------------------------------------------------+
もちろん、デストラクタではメモリが解放されていることを確認します。
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogify::~CLogify() { //--- Delete handlers int size_handlers = ArraySize(m_handlers); for(int i=0;i<size_handlers;i++) { if(CheckPointer(m_handlers[i]) != POINTER_INVALID) { m_handlers[i].Close(); delete m_handlers[i]; } } delete m_suppression; } //+------------------------------------------------------------------+
次に、ログ出力を担うAppend()メソッド内で、ログテンプレートが作成された直後に抑制の判定をおこないます。メッセージが不要であると判断された場合、そのメッセージはそのまま無視されます。
//+------------------------------------------------------------------+ //| 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,int code_error=0) { //--- Ensures that there is at least one handler this.EnsureDefaultHandler(); //--- Textual name of the log level string levelStr = ""; switch(level) { case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break; case LOG_LEVEL_INFO : levelStr = "INFO"; 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,m_error.Error(code_error)); //--- Supression if(m_suppression.ShouldSuppress(data)) { return(true); } //--- Call handlers int size = this.SizeHandlers(); for(int i=0;i<size;i++) { data.formated = m_handlers[i].GetFormatter().Format(data); m_handlers[i].Emit(data); } return(true); } //+------------------------------------------------------------------+
このシンプルなチェックによって、不要なメッセージが処理されるのを防ぎます。現在のログが冗長であると判定された場合(何度も連続して出現した、コード内の同じ箇所から発生した、あるいはその他設定された基準に該当した場合など)、そこで処理を打ち切ります。これで完了です。
テスト
抑制システムが内部でどのように動作するか理解できたので、次は実際にテストをおこないましょう。ここでの目的は、各抑制モードが期待通りに動作しているかを実践的に検証することです。重複や不要なログが、選択した設定に従って適切に抑制されるかを確認していきます。順を追って見ていきましょう。
テスト1:連続メッセージの抑制(LOG_SUPRESSION_MODE_CONSECUTIVE)これは最も基本的な抑制モードです。ロジックは単純で、同じメッセージが連続して記録された場合、最初の1回だけを表示します。これは、ループ内で同じログが繰り返される場合にコンソールのスパムを避けたりするのに有効です。今回は、内容と発生元が同じログを11回送信するコードで、この挙動をテストします。
//+------------------------------------------------------------------+ //| Import | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify Logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MqlLogifySuppressionConfig config; config.mode = LOG_SUPRESSION_MODE_CONSECUTIVE; Logify.Suppression().SetConfig(config); for(int i=0;i<11;i++) { Logify.Info("Check signal buy", "Signal"); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
上記のコードを実行すると、コンソールに次の結果が表示されます。
2025.07.31 04:34:26 [INFO]: Check signal buy
それだけです。重複はありません。同じメッセージが11回記録されても、すべてが連続して同一だったため、システムは1回表示すれば十分だと判断しました。これにより、このモードが正しく動作していることが確認できます。
テスト2:繰り返し回数による抑制(LOG_SUPRESSION_MODE_BY_REPEAT_COUNT)このモードは、前のモードよりも柔軟性があります。連続した同一メッセージをすべて抑制するのではなく、何回まで表示するかという許容回数を設定できるようになっています。このオプションは、一定回数だけログを確認したい場合に便利です。
同じメッセージを最大2回まで繰り返し送信できるようにシステムを設定しましょう。
//+------------------------------------------------------------------+ //| Import | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify Logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MqlLogifySuppressionConfig config; config.mode = LOG_SUPRESSION_MODE_BY_REPEAT_COUNT; config.max_repeat_count = 2; Logify.Suppression().SetConfig(config); for(int i=0;i<11;i++) { Logify.Info("Check signal buy", "Signal"); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
コンソールで期待される結果
2025.07.31 04:40:49 [INFO]: Check signal buy 2025.07.31 04:40:49 [INFO]: Check signal buy
設定したとおり、最初の2つのメッセージのみが表示されました。残りは繰り返し回数の上限を超えたため、自動的に破棄されました。この制御は、非常に冗長なログを生成する環境でも、最初の警告信号だけは確実に確認したい場合に特に有用です。
テスト3:時間間隔による抑制(LOG_SUPRESSION_MODE_THROTTLE_TIME)このモードでは、メッセージ間の時間を基準に抑制がおこなわれます。メッセージが同一であっても、設定された時間間隔未満で発生した場合にのみ抑制されます。
ここでは、同じメッセージが1秒ごとに表示されるようにシステムを設定します。シミュレーションとして、同じメッセージを11回出力し、その間にSleep(200)を挟みます(つまり、各ログの間隔は200ミリ秒)。これにより、1秒間に5回メッセージが発生しますが、システムは1秒ごとに1回のみ表示し、残りは破棄されるはずです。
//+------------------------------------------------------------------+ //| Import | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify Logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MqlLogifySuppressionConfig config; config.mode = LOG_SUPRESSION_MODE_THROTTLE_TIME; config.throttle_seconds = 1; Logify.Suppression().SetConfig(config); for(int i=0;i<11;i++) { Logify.Info("Check signal buy", "Signal"); Sleep(200); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
実行すると、コンソールに次のようなものが表示されます。
2025.07.31 04:45:26 [INFO]: Check signal buy 2025.07.31 04:45:27 [INFO]: Check signal buy 2025.07.31 04:45:28 [INFO]: Check signal buy
1秒ごとに3件のログが表示されました。残りは許可された範囲外であったため破棄されました。このモードは、価格更新や市場状況のチェックなど、発生頻度が変動するイベントに対して特に有効です。
テスト4:発生元による抑制(LOG_SUPRESSION_MODE_BY_ORIGIN)このモードでは、ログに記録された発生元に基づいてメッセージをブロックします。特定の発生元が拒否リストに登録されている場合、その発生元から発生するメッセージは内容や時間間隔に関わらず無視されます。以下の例では、Signalソースをブロックし、Tradeソースのみを通す設定にしています。
//+------------------------------------------------------------------+ //| Import | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify Logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MqlLogifySuppressionConfig config; config.mode = LOG_SUPRESSION_MODE_BY_ORIGIN; config.AddBlockedOrigin("signal"); Logify.Suppression().SetConfig(config); for(int i=0;i<11;i++) { Logify.Info("Check signal buy", "Signal"); } Logify.Info("Purchase order sent successfully", "Trade"); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
コンソールでの結果
2025.07.31 04:48:36 [INFO]: Purchase order sent successfully
発生元が「Trade」のメッセージのみが表示されています。他のメッセージはすべて、明示的にブロックされたソースに属していたため抑制されました。
ファイルベースの抑制モードもほぼ同じ仕組みで動作しますが、ブロックの判定基準がログ内で定義された論理的な発生元ではなく、ログを発生させたファイル名になります。そのため、発生元による抑制テストとファイルによる抑制テストは同じコード構造を共有しています。違いはチェック時の呼び出し先だけで、片方は発生元を確認し、もう片方はファイル名を確認します。したがって、このテストにより、ファイルによる抑制の動作も検証されたと考えられます。
言語の自動検出と調整
これまでCLogifyErrorクラスは、エラーメッセージを常に英語で開始していました。最初はこれで問題ありませんでしたが、大きな欠点がありました。ユーザーのターミナルがスペイン語、フランス語、ポルトガル語などに設定されていても、エラーの言語は常に英語のままだったのです。Logifyの多言語対応が進む中で、この問題を解決するため、エラーメッセージのデフォルト言語をMetaTraderターミナルに設定された言語に自動で適応させるようにしました。そのために、クラスコンストラクタに小さいながらも強力な変更を加えました。英語のエラーセットを直接使うのではなく、ターミナル自体にどの言語を使うかを判断させるようにしたのです。
CLogifyError::CLogifyError()
{
SetLanguage(GetLanguageFromTerminal());
} GetLanguageFromTerminal()メソッドは、ネイティブ関数TerminalInfoString(TERMINAL_LANGUAGE)を使用して、MetaTraderに設定されている現在の言語を取得します。この値は文字列で返され、たとえば「French」、「Korean」、「Portuguese (Brazil)」のような言語名になります。取得した文字列は、次にENUM_LOG_LANGUAGEにマッピングされます。ENUM_LOG_LANGUAGEは、Logifyのエラーシステムがサポートする言語を表す列挙型です。
ENUM_LOG_LANGUAGE CLogifyError::GetLanguageFromTerminal(void) { string lang = TerminalInfoString(TERMINAL_LANGUAGE); if(lang == "German") return LOG_LANGUAGE_DE; if(lang == "Spanish") return LOG_LANGUAGE_ES; if(lang == "French") return LOG_LANGUAGE_FR; if(lang == "Italian") return LOG_LANGUAGE_IT; if(lang == "Japanese") return LOG_LANGUAGE_JA; if(lang == "Korean") return LOG_LANGUAGE_KO; if(lang == "Portuguese (Brazil)" || lang == "Portuguese (Portugal)") return LOG_LANGUAGE_PT; if(lang == "Russian") return LOG_LANGUAGE_RU; if(lang == "Turkish") return LOG_LANGUAGE_TR; if(lang == "Chinese (Simplified)" || lang == "Chinese (Traditional)") return LOG_LANGUAGE_ZH; //--- Default language: English return LOG_LANGUAGE_EN; }
この自動適応機能は、特に国際的な顧客とやり取りするディストリビューターやトレーダー、企業にとって非常に有用です。ライブラリは今や「ターミナルの言語で話す」ことができ、手動で設定する必要はありません。これにより操作のハードルが下がり、技術に詳しくないユーザーでも、エラーの言語設定に悩むことがなくなります。万が一、ターミナルの言語が認識されなかったり、マッピングされなかった場合でも問題ありません。システムは自動的に英語に切り替わり、例外的なケースでも機能する体験が保証されます。
この変更は実装自体は簡単ですが、ライブラリの使いやすさを大幅に向上させ、Logifyの動作を「自動で便利に使える」という原則に沿ったものにします。つまり、システムがユーザーに適応するのであって、ユーザーがシステムに合わせる必要はなくなります。
結論
ここまで見てきたように、Logifyはさらに賢くなりました。ログの繰り返しが多すぎる場合を自動で判定できるようになり、さらにターミナルの言語に合わせて自動的にメッセージを表示するようになりました。設定は一切不要です。
繰り返しメッセージの抑制方法もいくつか用意されており、プロジェクトに応じて単独または組み合わせて使用できます。
- 連続する同一メッセージ:同じログが連続して大量に表示されるのを防止
- ログ間の最小時間:短時間で同じメッセージが繰り返し表示されるのを防止
- 一定回数以上の繰り返し時のみ:繰り返しが設定回数を超えた場合にのみ再表示
- 同じコードスニペットからのログ:同じ箇所からの重複ログをブロック。
- 異なるファイルから同一内容のログ:別ファイルから来た場合でも同じ内容の重複をブロック
これらはすべて設定から簡単に有効化でき、コードを複雑にする必要はありません。さらに、新しい自動言語検出機能により、ターミナルの設定に基づいて最適な言語が自動選択され、国際環境やチームでの開発時にも大きな助けとなります。
新しいアイデアや追加すべき抑制モード、改善点を見つけた場合は、コメントでお知らせください。Logifyは常に変更や改善にオープンです。進化に合わせて、新しい記事で最新情報をお届けしていきます。
| ファイル名 | 詳細 |
|---|---|
| Experts/Logify/LogiftTest.mq5 | ライブラリの機能をテストするファイル。実用的な例が含まれています。 |
| Include/Logify/Error/Languages/ErrorMessages.XX.mqh | 各言語のエラーメッセージを格納します。Xは言語の頭字語を表します。 |
| Include/Logify/Error/Error.mqh | エラー情報を格納するためのデータ構造体 |
| Include/Logify/Error/LogifyError.mqh | 詳細なエラー情報を取得するためのクラス |
| Include/Logify/Formatter/LogifyFormatter.mqh | ログレコードのフォーマット、プレースホルダーを特定の値に置き換えるクラス |
| Include/Logify/Handlers/LogifyHandler.mqh | レベル設定やログ送信を含むログハンドラを管理するための基本クラス |
| Include/Logify/Handlers/LogifyHandlerComment.mqh | MetaTraderのターミナルチャートのコメントにフォーマットされたログを直接送信するログハンドラ |
| Include/Logify/Handlers/LogifyHandlerConsole.mqh | フォーマットされたログをMetaTraderの端末コンソールに直接送信するログハンドラ |
| Include/Logify/Handlers/LogifyHandlerDatabase.mqh | フォーマットされたログをデータベースに送信するログハンドラ(現在は出力のみが含まれているが、すぐに実際のSQLiteデータベースに保存する予定) |
| Include/Logify/Handlers/LogifyHandlerFile.mqh | フォーマットされたログをファイルに送るログハンドラ |
| Include/Logify/Suppression/LogifySuppression.mqh | インテリジェントなログメッセージ抑制ルールを適用し、不要な繰り返しをフィルタリングする |
| Include/Logify/Utils/IntervalWatcher.mqh | 時間間隔が経過したかどうかをチェックし、ライブラリ内でルーチンを作成できるようにする |
| Include/Logify/Logify.mqh | ログ管理、レベル、モデル、フォーマットの統合のためのコアクラス |
| Include/Logify/LogifyBuilder.mqh | CLockifyオブジェクトを作成し、構成を簡素化するクラス |
| Include/Logify/LogifyLevel.mqh | Logifyライブラリのログレベルを定義するファイル。詳細な制御が可能 |
| Include/Logify/LogifyModel.mqh | レベル、メッセージ、タイムスタンプ、コンテキストなどの詳細を含むログレコードをモデル化する構造 |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19014
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
共和分株式による統計的裁定取引(第2回):エキスパートアドバイザー、バックテスト、最適化
初心者からエキスパートへ:MQL5を使用したアニメーションニュースヘッドライン(III) - ニュース取引のためのクイック取引ボタン
MQL5取引ツール(第8回):ドラッグ&最小化可能な拡張情報ダッシュボード
MQL5で自己最適化エキスパートアドバイザーを構築する(第11回):初心者向け線形代数入門
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
改善提案ありがとうございます!
こんにちは、作者様。マクロを作ってください。1つのファイルをインクルードするだけです。 設定は必要ありません。デフォルトの設定でライブラリを使用できます。ロギングを無効にすると、マクロは最終的にコンパイルされたex5に実際のコードを生成しません。