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


39

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

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

この3番目のステップでは、コード内に「Coo not find Foo」(またはテンプレート化された文字列Could not find {name})が印刷される場所が多くあるため、デバッグプロセスが停止することがよくあります。実際、スペルミスが何度かあったとすると、実際の場所を見つけるのに非常に速くなりました。システム全体で、そして多くの場合世界中でメッセージが一意になり、関連する検索エンジンがすぐにヒットしました。

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


54
代わりにスタックトレースを使用してください。スタックトレースは、エラーが発生した場所を正確に示すだけでなく、それを呼び出したすべての関数を呼び出したすべての関数も示します。必要に応じて、例外が発生したときにトレース全体を記録します。Cのように例外のない言語で作業している場合、それは別の話です。
ロバートハーベイ

6
@ l0b0言葉遣いに関する小さなアドバイス。「このコミュニティは...長所と短所をどのように考えていますか」は、広すぎると思われるフレーズです。これは「良い主観的な」質問を許可するサイトであり、このタイプの質問を許可する見返りに、OPは意味のあるコンセンサスに向けてコメントと回答を「シェパーディング」する作業を期待されます。
-rwong

@rwongありがとうございます!質問はすでに非常に良い点の回答を受け取っていると感じていますが、これはフォーラムでよりよく尋ねられたかもしれません。JohnWuの明確な回答を読んだ後、RobertHarveyのコメントに対する回答を撤回しました。そうでない場合、具体的なシェパーディングのヒントはありますか?
l0b0

1
私のメッセージは「bar()の呼び出し中にFooが見つかりませんでした」のように見えます。問題が解決しました。肩をすくめて 欠点は、顧客に見られるのが少し漏れやすいことですが、とにかくエラーメッセージの詳細を非表示にする傾向があり、いくつかの関数名を見ることができるサルを与えることができなかったシステム管理者のみが利用できるようにします。それに失敗すると、はい、素敵な小さな一意のID /コードがトリックを行います。
モニカとライトネスレース

1
これは、顧客から電話があり、コンピュータが英語で実行されていない場合に非常に便利です。最近では、電子メールとログファイルがあるので、ほとんど問題はありません
イアン

回答:


12

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

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

収集する必要のある重要なデータの一部(すべて知っている)は次のとおりです。

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

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

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

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

ユーティリティ関数でさえ、「トレーサー」引数を追加する必要があります。そのため、失敗した場合、ログメッセージは特定の高レベルコマンドとの相関を可能にします。

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

ログメッセージの作成に関して:

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

1
実際、AOPは主に、この問題を解決する固有の機能を宣伝しています。つまり、関連するすべての呼び出しにTracerを追加し、コードベースへの侵入を最小限に抑えています。
ビショップ

また、「ログメッセージの書き込み」のリストに、「何が起こったのか」ではなく「理由」と「修正方法」の観点から障害を特徴付けることが重要であることを追加します。
ビショップ

58

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

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からコードを盗む


32
うーん、私は自分の主張を十分に明確にしなかったと思います。私は彼らがコードの場所ごとにユニークなロバートであることを知っていますコードパスごとに一意ではありませんたとえば、真の問題が入力が適切に設定されていないことである場合など、場所を知ることはしばしば役に立ちません。強調するために言語を少し編集しました。
ジョン・ウー

1
良い点、あなたの両方。スタックトレースには別の問題があり、状況によっては契約違反になる場合もあれば、そうでない場合もあります。サイズによっては、特に一部の言語のような短縮バージョンではなくスタックトレース全体を含める場合、メッセージが圧倒される可能性がありますデフォルトで行う。別の方法としては、スタックトレースログを個別に書き込み、そのログに番号付きインデックスをアプリケーションの出力に含めることができます。
l0b0

12
これらの多くがI / Oのフラッディングを心配している場合、深刻な問題があります。それとも、ただケチなだけですか?実際のパフォーマンスヒットは、おそらくスタックのアンワインドです。
ジョン・ウー

9
スタックトレースを短縮するためのソリューションで編集し、場合にあなたが3.5フロッピーディスクにログを書いている;)
ジョン・ウー

7
@JohnWuまた、 "IOException 'File not Found' at [...]"を忘れないでください。これは、呼び出しスタックの50層について通知しますが、どのファイルが見つからなかったのか正確には通知しません。
Joker_vD

6

SAP NetWeaverはこれを何十年も行っています。

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

エラーメッセージは中央リポジトリで管理され、各メッセージはメッセージクラスとメッセージ番号で識別されます。

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

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

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

  • SAPナレッジベースの記事でエラーを検索し、「Coo not find Foo」を検索する場合よりも関連性の高い検索結果を取得できます。

  • メッセージのテキスト表現は翻訳可能です。そのため、文字列の代わりにメッセージの使用を奨励することにより、国際化機能も利用できます。

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

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ベースのビジネスプロセスモデリングツールがあります。これらのツールの実装は完全にデータ駆動型であるため、これらのエラーは使用先リストに表示されません。これは、エラーの原因を見つけようとするときに使用先リストに頼りすぎていることが、ニシンである可能性があることを意味します。


メッセージカタログは、しばらくの間GNU / Linux(および一般的にPOSIX標準としてのUNIX)の一部でもありました。
ビショップ

@bishop通常、私はPOSIXシステム専用のプログラミングをしているわけではないので、慣れていません。POSIXメッセージカタログとOPがその実装から何を学ぶことができるかを説明する別の回答を投稿できます。
フィリップ

3
私はこれを本来の目的で行ったプロジェクトの一部でした。私たちが遭遇した問題の1つは、他のすべてと一緒に、データベースに「データベースに接続できませんでした」という人間のメッセージを入れることでした。
ジミージェームズ

5

このアプローチの問題は、それがより詳細なロギングにつながることです。99.9999%のうち、あなたは決して見ません。

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

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

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

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

さらに、必要に応じて、エラーのログのみを記録するか、状態を他の場所(データベース?メッセージキュー?)に保持することで、より多くのログを回避できます。

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


機密データをキューに入れると、まだログに記録されます。暗号化の形式を使用せずに機密情報をDBに保存するのと同じように、これは賢明ではありません。
jpmc26

システムがキューまたはdbで実行されている場合、データはすでにそこにあるため、セキュリティも同様です。ログがセキュリティ管理の範囲外になる傾向があるため、ログの記録が多すぎるだけです。
ユアン

その通りですが、それがポイントです。そのデータは永続的に、通常は完全にクリアテキストのままであるため、お勧めできません。機密データの場合は、リスクを冒さずに、保管場所を最小限に抑えることをお勧めします。その後、保管方法を十分に認識し、慎重に行うことをお勧めします。
jpmc26

ファイルに書き込むため、従来は永続的です。ただし、エラーキューは一時的なものです。
ユアン

おそらくキューの実装(そして場合によっては設定も)に依存すると思います。単にキューにダンプして、安全であると期待することはできません。そして、キューが消費されるとどうなりますか?ログは、誰かが表示するためにまだどこかになければなりません。さらに、それは一時的にでも開放したい追加の攻撃ベクトルではありません。攻撃が機密データがそこにあることを発見した場合、最新のエントリでさえも価値があるかもしれません。そして、誰かがスイッチを知らないでスイッチを入れないでディスクへのロギングを開始するリスクがあります。ワームの缶です。
jpmc26

1

ロギングはこれを実行する方法ではなく、むしろこの状況は例外的であると見なされ(プログラムをロックする)、例外をスローすることをお勧めします。あなたのコードは:

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を必要とし、そこにいないときにそれを処理できない場合は、システムを破壊します。


0

適切なロギングライブラリは拡張メカニズムを提供するため、ログメッセージの発信元の方法を知りたい場合は、すぐに使用できます。プロセスはスタックトレースを生成し、ロギングライブラリを終了するまでそれを走査する必要があるため、実行に影響があります。

とは言っても、それは本当にあなたがあなたにあなたのIDに何をして欲しいかによって異なります:

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

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.