しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

.NET Core における Activity と Application Insights による分散トレーシング

.NET Core 2.0 から追加された Activity というクラスがありますが、トレース用の ID を管理する重要な機能を持っています。特に Application Insights と組み合わせることで分散トレーシングが容易になります。

.NET Framework 4.5 以降なら System.Diagnostics.DiagnosticSource を NuGet からインストールすれば使えます。.NET 4.6 以降の場合は AsyncLocal<T> で実装されてます。

Activity Class (System.Diagnostics) | Microsoft Docs

新しく ASP.NET Core 2.1 のプロジェクトを作成すると、エラーページにユニークな ID を表示するコードとして、既に Activity を使うようになっています。

Activity.Current は null の可能性があるので HttpContext.TraceIdentifier も参照しています。

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
    return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

とはいえ、基本的には Activity が自動的に開始されているので、変なことをしない限りは値は取れます。

実装としては全く別物なので、それぞれの ID フォーマットは異なります。

f:id:shiba-yan:20181009174144p:plain

これだけだと利用価値がいまいちですが、HttpClient を使うと自動的に ID を HTTP ヘッダーに付与してくれるので、受け取り側が ASP.NET Core の場合は ID が継承されて Activity が開始されます。

実際に以下のようなコードで HTTP リクエストを投げてみます。httpbin.org を使うとリクエストのデータを JSON で返してくれるので簡単に確認出来て便利です。

class Program
{
    private static readonly HttpClient _httpClient = new HttpClient();

    static async Task Main(string[] args)
    {
        var activity = new Activity("Main").Start();
        
        activity.AddBaggage("name", "kazuakix");

        var response = await _httpClient.GetStringAsync("https://httpbin.org/get");

        activity.Stop();
    }
}

デフォルトだと ID だけ付与されますが AddBaggage を使うと追加のデータとして付与されます。

実行した結果は以下の通りです。Request-IdCorrelation-Context というヘッダーが何もしなくても付与されていることが確認出来ます。

f:id:shiba-yan:20181009171209p:plain

このヘッダーに関しては CoreFx のリポジトリにてドキュメントが公開されています。標準化に向けた作業中という話ですが、ちょっと進捗は怪しいですね。

Request-Id の値は親子関係を持っているので、受け取った側が更に別のサービスに処理を投げたとしても、ちゃんとその呼び出し関係は保持されるので、単純な ID よりも情報量が多いです。

Application Insights で分散トレーシング

Activity によって分散トレーシングに必要な値が準備されるので、後は Application Insights に送信してあげるだけです。既に SDK レベルで対応されているので、アプリケーションに Application Insights を追加すればすぐに使える状態になっています。

ドキュメントも用意されていますが、大体は Id の仕様についての話です。

Application Insights 的にはユニークな ID が送信されたら、それを関連付けて扱うだけなので、実際のプロトコルには依存していません。なので別にどんなプロトコルでも使えるはずです。

挙動を確認するために ASP.NET Core なアプリケーションを作って、HttpClient を使って別のアクションを呼び出してみました。E2E Transaction を確認すると分かりやすいです。

f:id:shiba-yan:20181009170743p:plain

特に専用のコードを書いているわけではないですが、透過的に HttpClient に Request-Id が引き継がれているので、一連の処理として Application Insights では扱われています。

これが別の App Service になると、Application Map での見栄えが良くなります。

f:id:shiba-yan:20181009182432p:plain

ちゃんと App Service 間のリクエストが可視化されて表示されています。1 つや 2 つぐらいだと大したことはないですが、関係のあるアプリケーションが増えていくと非常に有用だと思われます。

もちろん AIQL を使って Request-Id 単位でのクエリ実行も可能です。

ASP.NET での対応

ASP.NET Core はデフォルトで Request-Id の値を見るようになっていますが、ASP.NET 向けにも以下の Http Module をインストールすると、同じように処理されるようになります。

Activity の実装には AsyncLocal<T> が使われているので、実行コンテキストが変わった時に意図しない挙動になる可能性はありますが、ASP.NET / ASP.NET Core の両方ともリクエストスコープになるストレージも併用されているので、基本的には問題とならないはずです。

Azure Functions での対応

最近リリースされた Azure Functions v2 でも同じような仕組みを使ったトレースの仕組みが追加されています。基本的には Request-Id を引き継いでいく仕組みですが、いろんな Trigger で使えるようです。

ソースを確認した感じでは Service Bug Trigger の場合は UserProperties を使って、Request-Id を引き継いでいくみたいです。Storage の Trigger も一部は対応しているようでした。

念のために HttpTrigger でも試してみましたが、ちゃんと Request-Id を引き継いでくれませんでした。最新のランタイムでもまだ未対応のようです。

なお Request-Id を引き継ぐ必要があるので、メタデータが無い Storage Queue は難しそうです。

f:id:shiba-yan:20181009171034p:plain

ちなみに Application Map での表示は Cloud Role Name でまとめられるので、Telemetry Initializer を使って上書きすることが出来ます。実際に Kubernetes 向け拡張ではコンテナ名で上書きされています。

大注目されている Durable Functions の場合は、Queue の値が隠蔽されているので比較的簡単に対応は行えそうですが、今のところは特に動きは無さそうです。Azure Functions の Consumption Plan の場合はスケーリングの状態を可視化出来るので、結構面白そうです。