この記事は .NET Aspire に関する一連の記事の一部です。
- .NET Aspire って何? - 概要
- .NET Aspire を使ってみる
- .NET Aspire をデプロイする
- .NET Aspire で Prometheus, Jaeger, Grafana を使う
- Next.js + ASP.NET Core を .NET Aspire で構成する(with YARP)
- .NET Aspire でデータベースを扱う - PostgreSQL編
- .NET Aspire でデータベースを扱う - SQL Server編
- .NET Aspire のダッシュボードを単独で使う
.NET Aspire + Dapr についてはこちらをご覧ください。メインは Dapr についてですが、.NET Aspire を使用する場合についても記載があります。
この記事は .NET Aspire に搭載されているダッシュボードではなく、Prometheus、Jaeger、Grafana を使う記事です。
開発環境
- Windows 11
- Visual Studio 2022 17.11.0 Preview 2.0
- .NET Aspire 8.0.0
- Docker Desktop 4.31.1 (153621)
使用するソースコード
Visual Studio を立ち上げると .NET Aspire のプロジェクトテンプレートが5つあります。今回はそのうちの1つ .NET Aspire Starter Applicaiton を使います。
Starter Applicationの選択すると、Redis Cacheを Docker Containerで使用して出力キャッシュを既定で使用するように選択できます。今回はチェックを入れて選択しておきます。
もし Mac 環境のように Visual Studio を使用しない場合は dotnet コマンドで Starter Application を作成します。
dotnet new aspire-starter -o starterapp --use-redis-cache true
dotnet コマンドで .NET Aspire アプリケーションをプロジェクトテンプレートから新規作成する場合は Asipre Workload を事前にインストールしておく必要があります。詳しくはこちらをご確認ください。
この Starter アプリケーションの中身は webfrontend を Blazor Web App, backend が ASP.NET Core APIで構成してあります。さらに webfrontend では .NET 8 から使用可能になった 外部 Redis を使った出力キャッシュを使用するようになっています。実はこの Starter アプリケーションで構築した内容は、.NET Aspire を使ってみるで手動で構築した結果とほぼ同じものです。
Prometheus を Setup する
セットアップは3段階で行います。
- Exporter, Endpoint公開の設定
- Prometheus の設定用 yaml ファイルの作成
- Prometheus コンテナの起動設定
1. Exporter, Endpoint公開の設定
まず Prometheus 用のライブラリのインストールが必要です。ServiceDefault プロジェクトの Nuget パッケージマネージャーを開いて、OpenTelemetry.Exporter.Prometheus.AspNetCore を選択してインストールしてください。
インストールするバージョンに注意してください。
2024年4月現在、OpenTelemetry.Exporter.Prometheus.AspNetCore はまだ Stable バージョンがリリースされていません。最新版を使用するとうまく動かないことがあります。そのような時は Nuget Gallery を参照して一つ前のバージョンを確認し、インストールするようにしてください。
Nuget Gallery | OpenTelemetry.Exporter.Prometheus.AspNetCore
dotnet add package --prerelease OpenTelemetry.Exporter.Prometheus.AspNetCore
次に Prometheus 用の Exporter を実装して、さらに Prometheus からアクセスするためのエンドポイントを公開する実装をします。
Previewの頃は Prometheus 用のテンプレートコードがコメントアウトされた状態で実装済みだったのですが、GA 後は削除されたため、次のように実装します。
private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics => metrics.AddPrometheusExporter());
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
//{
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
//}
return builder;
}
...
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
+ app.MapPrometheusScrapingEndpoint();
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment())
{
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks("/health");
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
return app;
}
これらの実装によって Prometheus の Exporeter が有効になり、 Prometheus からアクセスしてもらうためのエンドポイントが公開されます。Prometheus はメトリクスを対象システムから Pull するモデルなのでエンドポイントの公開が必要です。
2024/6 時点の最新版である 1.9.0-alpha.2では下記実装は不要になっていますが、情報として残しておきます。
2024/5 時点で最新の OpenTelemetry.Exporter.Prometheus.AspNetCore 1.8.0-rc.1 を使用するとバグのためにうまくテレメトリを取得できません。最初のコメントアウトした実装に対してさらに次の修正が必要です。
private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
// Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
builder.Services.AddOpenTelemetry()
- .WithMetrics(metrics => metrics.AddPrometheusExporter());
+ .WithMetrics(metrics => metrics.AddPrometheusExporter(options => options.DisableTotalNameSuffixForCounters = true));
このバグについてはこちらの Issue でトラッキングされていますので、今後のリリースについて注目が必要です。
Prometheus fails to scrap Process/Runtime Instrumentation metrics due to issues with units
2. Prometheus の設定用 yaml ファイルの作成
本当に、心から、とても残念なことに Prometheus は 設定用 yaml ファイルの作成が必須です。全部コードで書けないと .NET Aspire の便利さが薄れてしまうのですが仕方ありません。
まず AppHost プロジェクトのプロジェクトファイルがある場所より1つ上の階層に prometheus フォルダを作成します。
次にそのフォルダの中に prometheus.yml というファイルを作ります。
global:
scrape_interval: 1s # makes for a good demo
scrape_configs:
- job_name: 'metricsapp'
static_configs:
- targets: ['host.docker.internal:5433','host.docker.internal:5004'] # hard-coded port matches launchSettings.json
tls_config:
insecure_skip_verify: true
この yaml ファイルの中の 5433, 5004 というポート番号ですが、これは .NET Aspire Starter プロジェクトの Blazor プロジェクトと WebAPI プロジェクトのポート番号です。つまり環境によって異なります。それぞれのプロジェクトの Properties\launchSettings.json を開いて、 profile が http の applicationUrl に記載されている Url の ポート番号を確認してそれを使用するように変更してください。5433, 5004はあくまでも私の環境でのポート番号ですので、そのままコピペでは動きません。
3. Prometheus コンテナの起動設定
最後に Prometheus のコンテナを立てて、コンテナ内の Prometheus がymlファイルを読み込めるようにします。AppHostプロジェクトの Program.cs を開き、次のように追加します。
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedisContainer("cache");
var apiservice = builder.AddProject<Projects.AspireSampleApp_ApiService>("apiservice");
builder.AddProject<Projects.AspireSampleApp_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WithReference(apiservice);
+ builder.AddContainer("prometheus", "prom/prometheus")
+ .WithBindMount("../prometheus", "/etc/prometheus")
+ .WithHttpEndpoint(9090, targetPort: 9090);
builder.Build().Run();
AddContainerメソッドの第一引数には自分で好きな名前を付けます。第二引数が Image 名です。どこの Container Registry を使っているのか確認できていないのですが、おそらく Docker Hubだと思われます。
WithBindMount でメソッドの第一引数がマウントするローカルマシン側のパスで、第二引数がコンテナ内部でそのマウント先を使用するためのパスです。つまり Prometheus は先ほど作成した prometheus.ymlファイルを /etc/prometheus/prometheus.yml というパスで読み込むことができます。 Docker に慣れている人であれば Bind Mount のことだな、とすぐお分かりいただけるでしょう。
WithVolumeMount メソッドによる Docker の BindMount は .NET Aspire Preview 4 で WithBindMount メソッドに分割されました。WithVolumeMount メソッドは Docker のボリュームを使用する VolumeMount のみの機能になりました。
WithEndpoint メソッドというメソッドもあるのですが、プロトコルが Http/gRPC の場合は WithHttpEndpoint メソッドを使用します。WithEndpoint、WithHttpEndpoint どちらも第一引数はローカルマシン側のポート番号で第二引数はコンテナ側のポート番号をセットします。docker コマンドの -p オプションと同じです。(第一引数と第二引数の順番が逆だったのですが修正されました)
4. 実行してみる
では起動してみましょう。Resources に Prometheus のコンテナがポート 9090 で立ち上がっていることが分かります。
Docker Desktopを見ると、Prometheusコンテナがいますね。Redis コンテナは Starter プロジェクトで元からセットアップしてあるので一緒に立ち上がります。
webfrontend を開いて、画面を適当に遷移したり、リロードしてから http://localhost:9090 を開きます。適当なメトリクスを選ぶとこのようにデータが表示されます。
Jaeger を Setup する
Prometheus はメトリクスだけなので、ログ・トレースの収集は別の仕組みが必要です。いくつかの有名なツールがありますが、Go言語で作られた Jaeger を使ってみましょう。
セットアップは2段階で行います。
- Exporter の設定
- Jaeger コンテナの起動設定
1. Exporter の設定
既定ではダッシュボード向けの Exporter が設定されています。ServiceDefaults プロジェクトの Extentions.cs の AddOpenTelemetryExporters メソッドの中にある次の箇所です。
builder.Services.AddOpenTelemetry().UseOtlpExporter();
UseOtlpExporter メソッドを叩くと、ダッシュボードに含む設定された全ての Exporter に対してテレメトリデータが送信されるはず・・・なのですが、UseOtlpExporter メソッドが実装されている.NET 用の OpenTemetry ライブラリ1.8.0 系にはバグがあるようです。Jaeger などの既定で用意されていない AMP のために手動で Exporeter を追加すると次のエラーとなります。
System.NotSupportedException: 'Signal-specific AddOtlpExporter methods and the cross-cutting UseOtlpExporter method being invoked on the same IServiceCollection is not supported.
GitHub 上で Issue なっています。
[otlp] UseOtlpExporter and AddOtlpExporter extensions don't play nice together
仕方がないので、ダッシュボードと Jaeger の両方にテレメトリデータを送信するのは今の所諦めて、コメントアウトします。そして、 Jeager 用に設定を追加します。
private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
{
- var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
- if (useOtlpExporter)
- {
- builder.Services.AddOpenTelemetry().UseOtlpExporter();
- }
+ // var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+ //if (useOtlpExporter)
+ //{
+ // builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ //}
// Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics.AddPrometheusExporter(options => options.DisableTotalNameSuffixForCounters = true));
+ builder.Services.AddOpenTelemetry()
+ .WithTracing(configure: tracing => tracing.AddOtlpExporter(configure: options => options.Endpoint = new Uri("http://localhost:4317")));
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.Exporter package)
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
return builder;
}
Endpoint として指定している http://localhost:4317 は Jaeger が gRPC でデータを受け付けるポートとして指定されているものなので、変更できません。Jaeger が公開・受付するポートの一覧は次をご確認ください。
2. Jaeger コンテナの起動設定
Jaeger のコンテナを立てて画面とデータを受け付けるためのポートを公開します。AppHostプロジェクトの Program.cs を開き、次のように追加します。
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedisContainer("cache");
var apiservice = builder.AddProject<Projects.AspireSampleApp_ApiService>("apiservice");
builder.AddProject<Projects.AspireSampleApp_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WithReference(apiservice);
builder.AddContainer("prometheus", "prom/prometheus")
.WithBindMount("../prometheus", "/etc/prometheus")
.WithHttpEndpoint(9090, targetPort: 9090);
+ builder.AddContainer("jaeger", "jaegertracing/all-in-one")
+ .WithHttpEndpoint(16686, targetPort: 16686, name: "jaegerPortal")
+ .WithHttpEndpoint(4317, targetPort: 4317, name: "jaegerEndpoint");
builder.Build().Run();
16686ポートを公開していますが、これは Jaeger の画面を開くためです。4317については上で説明しましたね。
WithHttpEndpointメソッドの name 引数に値をセットしていますが、これは複数のエンドポイントを公開する場合は必須です。どんな名前でも良いので、基本的にエンドポイントには名前をつけるようにしておくようにしておくことを推奨します。
3. 実行してみる
では起動してみましょう。Resources の Jaeger 行があると思います。Details 列の View をクリックすると下に詳細が表示されて、ポート 16686, 4317を公開した状態 で立ち上がっていることが分かります。
Docker Desktop もみてみましょう。 Jaeger コンテナが立ち上がっていますね。
今回も webfrontend を開いて、画面を適当に遷移したり、リロードしてから http://localhost:16686 を開きます。Service に webfrontend, Operation に GET を選択すると余計なデータが表示されなくて見やすいでしょう。 /weather にアクセスした履歴があります。この行をクリックすると・・・
綺麗にトレースが表示されました。いい感じです。localhost:56416 への GET と SETX は Redis へのアクセスですね。
Redis へアクセスしたトレースをクリックすると、詳細が確認できます。
Grafana を Setup する
Grafana は Prometheus と Jaeger で収集したデータを格好よく見やすくしてくれるダッシュボードを作ることができます。なので、.NET Aspire とは無関係にコンテナを立ち上げて設定すればいいのですが、せっかくなので.NET Aspire 管理でやってみましょう。
素の Grafana を立ち上げる
AppHost プロジェクトの Program.cs に次のコードを追加します。
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedisContainer("cache");
var apiservice = builder.AddProject<Projects.AspireSampleApp_ApiService>("apiservice");
builder.AddProject<Projects.AspireSampleApp_Web>("webfrontend")
.WithReference(cache)
.WithReference(apiservice);
builder.AddContainer("prometheus", "prom/prometheus")
.WithBindMount("../prometheus", "/etc/prometheus")
.WithHttpEndpoint(9090, targetPort: 9090);
builder.AddContainer("jaeger", "jaegertracing/all-in-one")
.WithHttpEndpoint(16686, targetPort: 16686, name: "jaegerPortal")
.WithHttpEndpoint(4317, targetPort: 4317, name: "jaegerEndpoint");
+ builder.AddContainer("grafana", "grafana/grafana")
+ .WithHttpEndpoint(3000, targetPort: 3000, name: "grafanaPortal");
builder.Build().Run();
.NET Aspire でコンテナを扱うのもそろそろ慣れてきましたね。.NET Aspire でコンテナ管理をするとイメージのPullからコンテナ起動・削除してくれるはかなり便利です。
起動して http://localhost:3000 にアクセスし、admin/admin でログインします。もしアクセスしても何も表示されない場合はリロードを繰り返すと表示されると思います。初期表示がちょっと重いです。
初期画面が表示されます。まずデータソースの設定が必要です。Prometheus と Jaeger のそれぞれ1つずつについて設定しましょう。DATA SOURCES をクリックします。
Prometheus をクリックします。
Connection の URL に http://host.docker.internal:9090 と入力します。 localhost ではないので注意してください。
画面の一番下で Save&Test をクリックします。このように Successfully queried the Prometheus API. と表示されれば接続成功です。
では続けて Jaeger もセットアップしましょう。左サイドバーに Add new connection リンクがあるのでクリックします。検索ボックスに Ja と入力すれば Jaeger が見つかるはずです。クリックします。
Add new data source をクリックします。
HTTP の URL に http://host.docker.internal:16686 と入力します。やはり localhost ではないので注意してください。
画面の一番下で Save&Test をクリックします。このように Data source connected and services found. と表示されれば接続成功です。
ではダッシュボードでデータを軽く見てみましょう。Home に戻り、DASHBOARDS をクリックします。
作業を始める前にまずダッシュボードを保存しておきましょう。フロッピーディスクアイコンをクリックして保存します。(保存を意味するアイコンとしてフロッピーディスクって、今時どうなんでしょうね...)
Add Visualization をクリックします。
Prometheus を選択します。
Metricsで何か選びます。ここでは scrape_duration_seconds を選びました。選んだら、 Run queries ボタンをクリックすると、上部にグラフが表示されます。時間幅を選んでいい感じにしたら、右上のApplyをクリックします。
メトリクスは無事に表示されました。次にトレースをみてみましょう。右上の Add → Visualization をクリックします。
トレースの一覧を表示させたいので、表示形式を変えます。右上の Time series をクリックします。
Table を選択します。
Data source に jaeger を指定して、Query Type を Search、Service Name を webfrontend、Operation Name に GET を指定したら、右上の Refresh dashboard のアイコンをクリックします。
するとこのようにトレースが一覧で表示されます。もし表示されない場合は表示期間を調整してください。古いトレースが表示されずにデータがない場合がよくあります。右上の Apply をクリックします。
ダッシュボードが表示されます。今追加したばかりのトレースの一覧から、/weather へのアクセスをしている行の先頭のリンクを別タブで開いてみましょう。
トレース情報が表示されました。
デザインを読み込み・保存できるようにする
今のままでは、実行を停止すると Grafana のダッシュボードがリセットされてしまいます。データソースとダッシュボードはファイル形式で保存することができますので、それを立ち上げ時に読み込むようにした方が現実的です。
まず、AppHost プロジェクトに追加した Grafana の設定を修正します。
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedisContainer("cache");
var apiservice = builder.AddProject<Projects.AspireSampleApp_ApiService>("apiservice");
builder.AddProject<Projects.AspireSampleApp_Web>("webfrontend")
.WithReference(cache)
.WithReference(apiservice);
builder.AddContainer("prometheus", "prom/prometheus")
.WithBindMount("../prometheus", "/etc/prometheus")
.WithHttpEndpoint(9090, targetPort: 9090);
builder.AddContainer("jaeger", "jaegertracing/all-in-one")
.WithHttpEndpoint(16686, targetPort: 16686, name: "jaegerPortal")
.WithHttpEndpoint(4317, targetPort: 4317, name: "jaegerEndpoint");
var grafana = builder.AddContainer("grafana", "grafana/grafana")
+ .WithBindMount("../grafana/config", "/etc/grafana")
+ .WithBindMount("../grafana/dashboards", "/var/lib/grafana/dashboards")
.WithHttpEndpoint(3000, targetPort: 3000);
builder.Build().Run();
AppHost プロジェクトの1つ上の階層に grafana というフォルダを作って、そこに設定ファイルを格納する想定です。フォルダを作ります。
そして便利なサンプルがあります。.NET Aspire のサンプルページに ASP.NET Core 用の Grafana のサンプルがあるので、その grafana フォルダの中身をそっくり grafana フォルダにコピーします。
https://github.com/dotnet/aspire-samples/tree/main/samples/Metrics/grafana
フォルダ構成がややこしいのですが、このようになります。
それでは再び実行してみましょう。http://localhost:3000 にアクセスし、admin/admin でログインします。かっこいいダッシュボードが表示されます。
残念なことにこのサンプルには Jaeger のトレースを一覧を表示する機能がないので、上記手順を参考に追加してみてください。
まとめ
.NET Aspire では OpenTelemetry のセットアップが完了していますので、使いたい AMP の Exporter を公開すればすぐに使えます。とても便利ですね。また、Docker Desktopを裏側で使ってイメージのダウンロードからコンテナ稼働・削除もしてくれるので環境構築がとても楽です。
.NET Aspire のダッシュボードがものすごく便利なので開発時にはそれを使えばいいではないか、と思う人もいらっしゃると思うのですが、本番環境ではどうでしょうか。2024年5月の Microsoft Build で .NET Aspire は GA しましたが、それと同時に Azure Container Apps はなんと .NET Aspire ダッシュボードをユーザーがデプロイしなくても使用できるようになりました。ただし、データが永続化できませんし、機能も専用のAMPアプリに比べると貧弱です。パッと状況を確認するのに .NET Aspire ダッシュボードはとても便利ですが、本番運用環境ではやはり Prometheusや Jaeger, Grafanaなどをセットアップすることになるでしょう。事前にローカルで Prometheus, Jaeger, Grafana のセットアップをして、本番環境へデプロイする前にテストをする場合があると思います。そういった場合にこの記事がお役に立てば嬉しいです。
.NET Aspire のダッシュボードは 単独でデプロイが可能で、コンテナで提供されています。このコンテナで稼働する.NET Aspire ダッシュボードは 次のようなシナリオで使用できるでしょう。
- .NET 以外の開発環境で使用する
- .NET Aspire を使用できない.NET プロジェクトで使用する
- Azure Container Apps 以外の環境で状況を素早く確認するために使用する
デプロイの解説記事を書いています。併せてご覧ください。
.NET Aspire のダッシュボードを単独で使う