web-dev-qa-db-ja.com

グローバルに一意のメッセージIDを使用してコードを検索可能にする

バグを見つけるための一般的なパターンは、次のスクリプトに従います。

  1. 奇妙な点を観察します。たとえば、出力がないか、プログラムがハングしています。
  2. ログまたはプログラムの出力で関連するメッセージを見つけます(たとえば、「Fooが見つかりませんでした」)。 (以下は、これがバグを特定するために取られたパスである場合にのみ関連します。スタックトレースまたは他のデバッグ情報がすぐに利用できる場合、それは別の話です。)
  3. メッセージが出力されるコードを見つけます。
  4. Fooが画像を入力した(または入力する必要がある)最初の場所とメッセージが印刷される場所の間のコードをデバッグします。

その3番目のステップでは、コードに「Fooが見つかりませんでした」(またはテンプレート化された文字列Could not find {name})が出力される場所が多数あるため、デバッグプロセスが頻繁に停止します。実際、数回スペルミスを使用すると、実際の場所を他の方法よりもはるかに速く見つけることができました-これにより、メッセージがシステム全体で、また世界中で一意になり、関連する検索エンジンにヒットしましたすぐに。

これから明らかな結論は、コードではグローバルに一意のメッセージIDを使用し、メッセージ文字列の一部としてハードコーディングし、コードベースで各IDが1つだけ存在することを確認する必要があるということです。保守性の観点から、このコミュニティはこのアプローチの最も重要な長所と短所をどのように考えていますか?これをどのように実装するか、またはそれを実装することが絶対に必要にならないようにしますか(ソフトウェアに常にバグがあると仮定)?

39
l0b0

全体として、これは有効で価値のある戦略です。ここにいくつかの考えがあります。

この戦略は「テレメトリ」とも呼ばれます。このような情報をすべて組み合わせると、実行トレースを「三角測量」し、トラブルシューティング担当者がユーザー/アプリケーションが達成しようとしていることと実際に何が起こったかを理解できるようになるという意味です。 。

収集する必要があるいくつかの重要なデータ(私たち全員が知っています)は次のとおりです。

  • コードの場所、つまりコールスタックとコードのおおよその行
    • 関数が適切に小さな単位に合理的に分解されている場合、「おおよそのコード行」は必要ありません。
  • 関数の成功/失敗に関連するすべてのデータ
  • 人間のユーザー/外部エージェント/ APIユーザーが達成しようとしていることを特定できる高レベルの「コマンド」。
    • アイデアは、ソフトウェアがどこかから来るコマンドを受け入れて処理するということです。
    • このプロセス中に、数十から数百から数千の関数呼び出しが行われた可能性があります。
    • このプロセス全体で生成されたテレメトリは、このプロセスをトリガーする最高レベルのコマンドまで追跡できるようにしたいと考えています。
    • Webベースのシステムの場合、元のHTTP要求とそのデータは、そのような「高レベルの要求情報」の一例です。
    • GUIシステムの場合、ユーザーが何かをクリックすると、この説明に適合します。

多くの場合、低レベルのログメッセージをトレースして、それをトリガーする最高レベルのコマンドに戻ることができないため、従来のロギングアプローチでは不十分です。 スタックトレースは、最高レベルのコマンドの処理に役立つ上位の関数の名前のみをキャプチャし、そのコマンドを特徴付けるために必要になることがある詳細(データ)はキャプチャしません。

通常、ソフトウェアは、この種のトレーサビリティ要件を実装するように作成されていません。これにより、低レベルのメッセージを高レベルのコマンドに関連付けることがより困難になります。この問題は、自由にマルチスレッド化されたシステムで特に悪化します。このシステムでは、多くの要求と応答がオーバーラップする可能性があり、処理が元の要求受信スレッドとは異なるスレッドにオフロードされる場合があります。

したがって、テレメトリから最大の価値を引き出すには、ソフトウェアアーキテクチャ全体の変更が必要になります。ほとんどのインターフェースと関数呼び出しは、「トレーサー」引数を受け入れて伝搬するように変更する必要があります。

ユーティリティ関数でも「トレーサー」引数を追加する必要があるため、失敗した場合でも、ログメッセージ自体が特定の高レベルコマンドと関連付けられるようにします。

テレメトリのトレースを困難にするもう1つの障害は、オブジェクト参照(nullポインターまたは参照)がないことです。重要なデータの一部が欠落している場合、障害に役立つ情報を報告することが不可能になる可能性があります。

ログメッセージの記述に関して:

  • 一部のソフトウェアプロジェクトでは、管理者のみを対象としたログメッセージであっても、ローカリゼーション(外国語への翻訳)が必要になる場合があります。
  • 一部のソフトウェアプロジェクトでは、ログ記録の目的であっても、機密データと非機密データを明確に分離する必要があり、管理者が特定の機密データを誤って表示する可能性がありません。
  • エラーメッセージを難読化しないでください。それは顧客の信頼を損なうことになります。顧客の管理者は、これらのログを読んで理解することを期待しています。顧客の管理者から隠す必要のある独自の秘密があることを彼らに感じさせないでください。
  • 顧客がテレメトリログを1つ持ってきて、テクニカルサポートスタッフをグリルすることを期待してください。彼らは知ることを期待しています。テレメトリログを正しく説明するようにテクニカルサポートスタッフをトレーニングします。
12
rwong

コードの何百もの場所で使用されている簡単なユーティリティ関数があるとします。

decimal Inverse(decimal input)
{
    return 1 / input;
}

あなたが提案したように私たちがするなら、私たちは書くかもしれません

decimal Inverse(decimal input)
{
    try 
    {
        return 1 / input;
    }
    catch(Exception ex)
    {
        log.Write("Error 27349262 occurred.");
    }
}

発生する可能性のあるエラーは、入力がゼロの場合です。これにより、ゼロ除算例外が発生します。

したがって、出力またはログに27349262が表示されているとします。ゼロ値を渡したコードはどこにありますか?この関数は、一意のIDとともに数百の場所で使用されています。したがって、ゼロによる除算が発生したことを知っているかもしれませんが、誰が0です。

わざわざメッセージIDをログに記録するつもりなら、スタックトレースもログに記録するとよいでしょう。

スタックトレースの冗長性が気になる場合は、ランタイムが提供する方法で文字列としてダンプする必要はありません。あなたはそれをカスタマイズすることができます。たとえば、nレベルのみに進む簡略化されたスタックトレースが必要な場合は、次のように記述できます(c#を使用する場合)。

static class ExtensionMethods
{
    public static string LimitedStackTrace(this Exception input, int layers)
    {
        return string.Join
        (
            ">",
            new StackTrace(input)
                .GetFrames()
                .Take(layers)
                .Select
                (
                    f => f.GetMethod()
                )
                .Select
                (
                    m => string.Format
                    (
                        "{0}.{1}", 
                        m.DeclaringType, 
                        m.Name
                    )
                )
                .Reverse()
        );
    }
}

次のように使用します。

public class Haystack
{
    public static void Needle()
    {
        throw new Exception("ZOMG WHERE DID I GO WRONG???!");
    }

    private static void Test()
    {
        Needle();
    }

    public static void Main()
    {
        try
        {
            Test();
        }
        catch(System.Exception e)
        {
            //Get 3 levels of stack trace
            Console.WriteLine
            (
                "Error '{0}' at {1}", 
                e.Message, 
                e.LimitedStackTrace(3)
            );  
        }
    }
}

出力:

Error 'ZOMG WHERE DID I GO WRONG???!' at Haystack.Main>Haystack.Test>Haystack.Needle

おそらく、メッセージIDを維持するよりも簡単で、より柔軟です。

DotNetFiddleからコードを盗みます

58
John Wu

SAP NetWeaverはこれを数十年にわたって行っています。

これは、典型的なSAP ERPシステムである巨大なコードベヒーモスのエラーをトラブルシューティングするときに、貴重なツールであることが証明されています。

エラーメッセージは、各メッセージがそのメッセージクラスとメッセージ番号によって識別される中央リポジトリで管理されます。

エラーメッセージを出力する場合は、クラス、番号、重大度、メッセージ固有の変数のみを記述します。メッセージのテキスト表現は実行時に作成されます。通常、メッセージが表示されるコンテキストでは、メッセージクラスと番号が表示されます。これにはいくつかのきちんとした効果があります:

  • 特定のエラーメッセージを作成するABAPコードベースのコード行を自動的に見つけることができます。

  • 特定のエラーメッセージが生成されたときにトリガーされる動的デバッガーブレークポイントを設定できます。

  • SAPナレッジベースの記事でエラーを検索し、「Fooが見つかりませんでした」を探すよりも、関連性の高い検索結果を得ることができます。

  • メッセージのテキスト表現は翻訳可能です。したがって、文字列ではなくメッセージの使用を奨励することで、i18n機能も利用できます。

メッセージ番号付きのエラーポップアップの例:

error1

エラーリポジトリでそのエラーを検索します。

error2

コードベースで見つけてください:

error3

ただし、欠点もあります。ご覧のとおり、これらのコード行はもう自己文書化されていません。ソースコードを読んで、上のスクリーンショットのようなMESSAGEステートメントを見ると、それが実際に何を意味するのかをコンテキストからしか推測できません。また、実行時にメッセージクラスとメッセージ番号を受け取るカスタムエラーハンドラーを実装することもあります。その場合、エラーは自動的に検出されないか、エラーが実際に発生した場所で検出されません。最初の問題の回避策は、ソースコードに常にコメントを追加して、メッセージの意味を読者に知らせることを習慣にすることです。 2番目の問題は、いくつかのデッドコードを追加して、自動メッセージルックアップが機能することを確認することで解決されます。例:

" Do not use special characters
my_custom_error_handler->post_error( class = 'EU' number = '271').
IF 1 = 2.
   MESSAGE e271(eu).
ENDIF.    

ただし、これが不可能な状況もあります。たとえば、ビジネスルールに違反したときに表示されるエラーメッセージを構成できるUIベースのビジネスプロセスモデリングツールがいくつかあります。これらのツールの実装は完全にデータ駆動型であるため、これらのエラーは使用先リストに表示されません。つまり、エラーの原因を見つけようとするときに、使用先のリストに頼りすぎると、ニシンが発生する可能性があります。

6
Philipp

このアプローチの問題は、これがさらに詳細なロギングにつながることです。 99.9999%あなたは決して見ないでしょう。

代わりに、プロセスの開始時の状態とプロセスの成功/失敗をキャプチャすることをお勧めします。

これにより、バグをローカルで再現してコードをステップ実行し、ログをプロセスごとに2か所に制限できます。例えば。

OrderPlaced {id:xyz; ...order data..}
OrderPlaced {id:xyz; ...Fail, ErrorMessage..}

開発者のマシンでまったく同じ状態を使用してエラーを再現し、デバッガーでコードをステップ実行し、新しいユニットテストを作成して修正を確認できます。

さらに、必要に応じて、失敗をログに記録するか、別の場所(データベース?メッセージキュー?)

明らかに、機密データのロギングについては特に注意する必要があります。したがって、ソリューションがメッセージキューまたはイベントストアパターンを使用している場合、これは特にうまく機能します。ログは「Message xyz Failed」とだけ言う必要があるため

5
Ewan

ロギングはこれに対処する方法ではなく、この状況は例外的であると見なされ(プログラムをロックする)、例外がスローされることをお勧めします。あなたのコードが次のとおりだったとしましょう:

public Foo GetFoo() {

     //Expecting that this should never by null.
     var aFoo = ....;

     if (aFoo == null) Log("Could not find Foo.");

     return aFoo;
}

コードを呼び出すのは、Fooが存在しないという事実に対処するように設定されていないようです。

public Foo GetFooById(int id) {
     var aFoo = ....;

     if (aFoo == null) throw new ApplicationException("Could not find Foo for ID: " + id);

     return aFoo;
}

そして、これはスタックトレースと、デバッグに役立つ例外を返します。

あるいは、取得時にFooがnullになる可能性があり、問題がなければ、呼び出し元のサイトを修正する必要があります。

void DoSomeFoo(Foo aFoo) {

    //Guard checks on your input - complete with stack trace!
    if (aFoo == null) throw new ArgumentNullException(nameof(aFoo));

    ... operations on Foo...
}

予期しない状況下でソフトウェアがハングしたり「奇妙に」動作するという事実は私には間違っているようです-Fooが必要で、そこにない場合に処理できない場合は、クラッシュする可能性のあるパスに沿って進むよりも、クラッシュする方が良いようです。システムを破壊する。

1
Paddy

適切なロギングライブラリは拡張のメカニズムを提供するため、ログメッセージの発信元のメソッドを知りたい場合は、すぐにそれを実行できます。プロセスはスタックトレースを生成し、ロギングライブラリから移動するまでそれをトラバースする必要があるため、実行に影響を与えます。

そうは言っても、それはあなたがあなたのIDに何をしてほしいかによります。

  • ユーザーに提供されるエラーメッセージをログに関連付けますか?
  • メッセージが生成されたときに実行されていたコードの表記を提供しますか?
  • マシン名とサービスインスタンスを追跡しますか?
  • スレッドIDを追跡しますか?

これらすべては、適切なロギングソフトウェアを使用して、すぐに実行できます(つまり、Console.WriteLine()またはDebug.WriteLine()ではありません)。

個人的には、実行の経路を再構築する能力がより重要です。それが Zipkin のようなツールが達成するように設計されています。システム全体で1つのユーザーアクションの動作を追跡するための1つのID。ログを中央の検索エンジンに配置することで、実行時間の最も長いアクションを見つけるだけでなく、その1つのアクションに適用されるログを呼び出すことができます( ELKスタック など)。

メッセージごとに変化する不透明なIDはあまり役に立ちません。一連のマイクロサービス全体の動作を追跡するために使用される一貫したID ...非常に便利です。

0
Berin Loritsch