EmotionTechテックブログ

株式会社エモーションテックのProduct Teamのメンバーが、日々の取り組みや技術的なことを発信していくブログです。

Error Reporting におけるエラーの報告とグループ化について調べてみた

はじめに

こんにちは、バックエンドエンジニアのおおたわらです。

過去にもこのブログで紹介したことがありますが、弊社ではマイクロサービス(言語は Rust、Node.js、Python など)の実行基盤として Cloud Run を利用しています。 アプリケーションのエラーについては現在 Cloud Monitoring によるログベースの監視を行っていますが、同原因のエラーがまとめられずエラーの対応状況の管理が困難なため、Error Reporting の利用を検討しています。

Error Reporting はログに出力されたスタックトレースに基づいてエラーをグループ化し、発生回数の集計などを自動で行ってくれる便利なサービスです。 ところが、スタックトレースを解析できるのは一部の言語のみであり、例えば弊社で使っている Rust については対応していません。

こういった言語についても活用ができるのかを探るべく、Error Reporting へのエラーの報告の方法やグループ化の仕組みについて調べたので記事にしてみます。

エラーの報告

Error Reporting へのエラーの報告方法には、大きく以下の2つがあります。

API を使う方法の場合、アプリケーション側でエラー発生ごとに都度 API 呼び出しによる報告が必要です。

一方、Cloud Logging による方法であれば指定の形式でエラーログを出力しておけばよいため、実装時に意識することは少なく済むと思われます。また、Cloud Run や App Engine などのサービスであれば特に何も設定をしなくても Error Reporting によりエラーの検出が行われます。

今回は Cloud Logging による報告の詳細について紹介します。 ログの記録方法は以下の2つがあります。

参考:ログでエラーをフォーマットする  |  Error Reporting  |  Google Cloud

スタックトレースを記録する

ログに指定の形式でスタックトレースを記録することで、Error Reporting がスタックトレースを自動で解析し類似とみなされたエラーをグループ化します。(グループ化の詳細なロジックについては後述します。)

この記事公開時点では以下の言語のスタックトレースのみが解析可能です。つまり Rust ではこの方法を使うことはできません。

例外スタック トレースのパーサーは、Go、Java™、.NET、Node.js、PHPPythonRuby を解析できます。

参考:Error Reporting  |  Google Cloud

スタックトレースは以下のいずれかの形式で記録します。

  • 複数行の textPayload
  • messagestack_traceexception フィールドを含む jsonPayload
  • messagestack_traceexception フィールドを含まないがスタックトレースが含まれる jsonPayload

試しに Cloud Run 上で動く Node.js のアプリケーションから以下のようにログを出力してみます。

{
  "jsonPayload": {
    "stack_trace": "InternalServerErrorException: Internal Server Error\n    at bootstrap (/var/www/dist/main.js:8:47)"
  }
}

以下のように検出されました。スタックトレースの情報によりエラーがグループ化された上で、エラー発生場所のソースコード位置も認識されています。また、「発生場所」として Cloud Run のサービス・リビジョン名が表示されています。

テキストメッセージを記録する方法

以下のように、jsonPayload@type として Error Reporting のイベントであることの指定と、メッセージの指定をすることで報告が可能です。 詳細なロジックについては後述しますが、 message フィールドがエラーのグループ化に使われます。

    "jsonPayload": {
      "@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent",
      "message": "Text message"
    }

Cloud Run で動く Rust アプリケーションから上記のようなログを出力してみたところ、Error Reporting に報告できました🎉

ですが、スタックトレースによる報告の結果と比べるとどの Cloud Run サービスで発生したのかなど、エラーに関する情報が少ないです。 なぜこうなるのかは明確な情報がないのですが、Error Reporting に自動で収集されたものではなくこちらからエラーだと認識させているので、自動で発生元の情報が取得されない可能性があります。

ログを ReportedErrorEvent の形式にフォーマットすることで情報を付与することができます。一部、エラーの調査時に役立ちそうなフィールドを紹介してみます。

  • serviceContext:エラーが発生したサービスの情報。Error Reporting のコンソール画面では「発生場所」として表示される。
  • message:エラーメッセージ。context.reportLocation の指定がない場合は、ヘッダー(例外のタイプとエラーメッセージ)とスタックトレースを含む必要がある(おそらく Error Reporting に発生箇所の情報を認識させる必要があるため)
  • context:エラー発生のコンテキスト
    • httpRequest:エラー発生した HTTP リクエストの情報
    • user:エラーの影響を受けたユーザーの情報。例えばユーザーIDなど
    • reportLocation:エラー報告することが決定されたソースコード上の場所。通常はログが記録された場所。
    • sourceReferencesソースコードの情報。GitHubリポジトリ名や、コミットハッシュを指定できる。

試しに以下のように一部のフィールドを指定したログを出力してみます。

{
  "jsonPayload": {
    "@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent",
    "message": "Text message",
    "serviceContext": {
      "service": "test-service",
      "version": "ee4cb1b8e7432a538bcea0da0a9add2e30b2e808"
    },
    "context": {
      "httpRequest": {
        "method": "GET",
        "url": "http://example.com/health_check",
        "responseStatusCode": 500
      },
      "user": "9f32f587135aa6774e78ed30fbaabcce3ec5528f",
      "reportLocation": {
        "filePath": "src/main.rs",
        "lineNumber": 80,
        "functionName": "api::handler::health_check_handler::health_check"
      }
    }
  }
}

報告されたエラーをコンソールで確認すると、以下のようになりました。 設定した情報が表示されているのがわかります。

スタックトレースがないので呼び出し階層の情報は与えられませんが、ソースコード上の発生箇所については認識させることができます。

エラーのグループ化

次に、報告されたエラーがグループ化される方法を確認してみます。公式ドキュメントに記載があります。

Error Reporting の概要  |  Google Cloud

以下の順でグループ化規則が適用されます。 基本は同じエラーのタイプ、発生箇所(ソースコード上のどの位置か、スタックトレースによって提供された呼び出し階層)が似ていれば同じグループになります。

エラーの種類 グループ化規則
環境内の一般的な問題に起因するエラー 例外タイプによりグループ化
スタックトレースがあるエラー 例外タイプと上位 5 つのフレームによりグループ化
スタックトレースはないが、メッセージがあるエラー メッセージと(存在する場合は)関数名によりグループ化。最初の 3 つのリテラル トークンのみが考慮されます。

各エラーの種類について、詳細に確認してみます。

環境内の一般的な問題に起因するエラー

例えば以下のようなエラーです。

com.google.apphosting.runtime.HardDeadlineExceededError

こちらはアプリケーション内というよりは、クラウド環境で発生したエラーなどが該当するようです。例えば、運用する中で Cloud Run の HTTP 429: 使用可能なコンテナ インスタンスがない エラーがグループ化されているのを見たことがあります。

スタックトレースがあるエラー

例えば以下のようなエラーです。

runtime error: index out of range
package1.func1()
      file1:20
package2.func2()
      file2:33

弊社プロダクトでこの条件に当てはまるエラーを見てみると、確かに規則通りにグループ化されていそうでした。

例えば以下のような、メッセージは異なるが型が同じ Node.js のエラーは同じグループにまとめられていました(スタックトレースは同じです)。

  • NotFoundException: Cannot GET /api/hoge
  • NotFoundException: Cannot GET /api/fuga

また、メッセージが同じだがスタックトレースが異なるエラーは別のグループとなっていました。

スタックトレースはないが、メッセージがあるエラー

例えば以下のようなエラーです。グループ化規則における「最初の3つのリテラルトークン」はこの例における、runtime、error、index です。

runtime error: index out of range
    func1()

こちらについては、スタックトレースを解析できない言語の場合に活用できる方法かもしれないと考えて、深堀って挙動を確認してみました。 条件を変えた 2 つのエラーをログに出力して、どのようにグループ化されるか確認してみました。 推測も含まれますが、以下のような傾向が確認できます。 実際に使う上では、「最初の3つのリテラルトークンのみ考慮」に注意が必要そうです。エラーメッセージの出力の仕方によっては、原因の違うエラーが同じグループとして扱われてしまいそうです。

  • メッセージ
    • 類似しているものは同じグループになる
    • ドキュメント通り最初の3つのリテラルトークンのみが考慮されていそう
  • 関数名は完全一致しているものが同じグループになる

メッセージの組み合わせを変えてみる

関数名は同じ値 api::handler::health_check_handler::health_check としています。

パターン エラー1 エラー2 結果
同一メッセージ message1 message1 同じグループ
類似メッセージ message1 message1 同じグループ
最初の 3 単語だけ同じメッセージ Unexpected error occurred in bigquery Unexpected error occurred in sqlx 同じグループ
最初の3単語内でID(ULID)だけ違うメッセージ error id:01HW2N1BJV1J64ZSMQYSBK4J6B error id:01HW2N0FKCEBBTH92SJH939R71 同じグループ
大幅に異なるメッセージ message1 あああああ 別グループ

関数名を変えてみる

メッセージは同じ message1 にして関数名だけ変えてみての検証もしてみました。

パターン エラー1 エラー2 結果
同一関数名 api::handler::health_check_handler::health_check api::handler::health_check_handler::health_check 同じグループ
類似関数名 api::handler::health_check_handler::health_check api::handler::health_check_handler::health_check2 別グループ

Rust のエラー管理に使用できそうか

まだ悩み中ではあるのですが、以下のような方法でうまく Rust のエラーを報告できないか模索中です。

おわりに

Error Reporting への報告の仕方や、グループ化の挙動について調べてみました。同じように気になった方への参考となれば幸いです。

そして、Error Reporting により Rust のスタックトレースも解析できるようになることを祈っています。

エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。もし記事を通じて興味を持っていただけましたら、ぜひ採用ページからご応募をお願いいたします。

hrmos.co