.NET Aspire のダッシュボードを単独で使う

この記事は .NET Aspire に関する一連の記事の一部です。

- [.NET Aspire って何？ - 概要](https://qiita.com/takashiuesaka/items/f45e930ef296c5710acc)
- [.NET Aspire を使ってみる](https://qiita.com/takashiuesaka/items/8794f99b0bf3ce3c3106)
- [.NET Aspire を デプロイする](https://qiita.com/takashiuesaka/items/3da744d0511f29270299)
- [.NET Aspire で Prometheus, Jaeger, Grafana を使う](https://qiita.com/takashiuesaka/items/4811e62aa52e39ed834c)
- [Next.js + ASP.NET Core を .NET Aspire で構成する（with YARP）](https://qiita.com/takashiuesaka/items/e167852af299a7b00939)
- [.NET Aspire でデータベースを扱う - PostgreSQL編](https://qiita.com/takashiuesaka/items/137cb1450c3d9fbc8d1e)
- [.NET Aspire でデータベースを扱う - SQL Server編](https://qiita.com/takashiuesaka/items/6d5ec4d4994649c5dca5)
- .NET Aspire のダッシュボードを単独で使う

.NET Aspire + Dapr についてはこちらをご覧ください。メインは Dapr についてですが、.NET Aspire を使用する場合についても記載があります。

- [Dapr って何？ - 概要編](https://qiita.com/takashiuesaka/items/ecb8d232d00f0a6591a5)
- [Dapr を使ってみる - 入門編](https://qiita.com/takashiuesaka/items/bdae30de41b0daab881f)
- [Dapr を使ってみる - State management 編](https://qiita.com/takashiuesaka/items/713499c531095d5687c2)

# .NET Aspire のダッシュボード

.NET Aspire は AppHost プロジェクトを起動するとダッシュボードという Web アプリケーションが立ち上がります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/70fd9d5f-f792-5d21-284c-be2ac80dc321.png)

ダッシュボードでは.NET Aspire 管理下にした.NETアプリケーションやコンテナ、実行ファイルなどのログ、環境変数などの詳細情報や、メトリクス、分散トレースなどのテレメトリデータを簡単に見ることができるため、非常に便利です。

.NET Aspire リリース直後からこのダッシュボードを単独で利用できないか、という issue が上がっていましたが、.NET Aspire Preview 4のリリース時にこの.NET Aspire ダッシュボードを単独で使用できるイメージの公開がアナウンスされました。そして .NET Aspire の GA とともに .NET Aspire ダッシュボードも GA しました。

https://learn.microsoft.com/ja-jp/dotnet/aspire/fundamentals/dashboard/standalone?tabs=bash

.NET Aspire ダッシュボードのためのポータルサイトもオープンして、 Discord に .NET Aspire ダッシュボードのチャンネルも作成されました。
https://www.aspiredashboard.com/

何か質問あったら気軽に Discord で質問できるのは良いですね。

それでは.NET Aspire ダッシュボードを単独で使ってみましょう。

# .NET Aspire ダッシュボードを動かす

.NET Aspire ダッシュボードは Docker イメージとして公開されています。次のコマンドでイメージを Pull して、実際に起動することができます。

```bash
docker run --rm -it -p 18888:18888 -p 4317:18889 -d --name aspire-dashboard mcr.microsoft.com/dotnet/aspire-dashboard:latest
```

18888 と 4317 の２つのポート番号を公開しています。18888 はダッシュボードの Web 画面で 4317 は OpenTelemetery のデータを受け付けます。

http://localhost:18888 を開くと次のようなログイン画面が表示されま
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/7f7a5229-5bcc-6d39-b83d-a8120d145be6.png)

入力するトークンはコンテナログに表示されています。Docker Desktop の画面から稼働している aspire-dashboard コンテナをクリックしてログを表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/168ff4f0-be49-6247-053e-872791a2b071.png)

t= の後ろのトークンをコピーして、ログイン画面に貼り付けてログインすると、ダッシュボードが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/b1e63d9a-a8d0-cad8-b846-c14551e4af0a.png)

オプションに --rm がついていますので、コンテナを停止するとコンテナは削除されます。
この後、このダッシュボードにテレメトリデータを送信しますので、コンテナはそのまま稼働させておいてください。

# テレメトリデータをダッシュボードに送信する

ではダッシュボードにテレメトリデータを送信してみましょう。OpenTelemetry のデータを送信するセットアップをする場合、.NET Aspire の サービスの既定値（Service Defaults） プロジェクトを利用するとあっという間です。.NET Aspire Starter アプリケーションを使った方がセットアップ済みなので簡単なのですが、今回は WebApi プロジェクトを作成して Service Default プロジェクトを適用することにします。

## 開発環境

- Windows 11
- Visual Studio 2022 17.11.0 Preview 2
    - .NET Aspire 8.0.1

## WebApi プロジェクトを作成し、OpenTelemetry をセットアップする

Visual Studio の新しいプロジェクトの作成画面で、ASP.NET Core Web API テンプレートを選択します。右上の検索ボックスで API を選ぶと探しやすいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/783beb02-c73e-4379-ef10-fb9c5105904b.png)

追加情報画面では、HTTPS 用の構成のチェックボックスを外しておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/41f525d6-c914-9d99-9bd4-e4c763cff9ba.png)

プロジェクトが作成できたら、実行してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/16faf3c8-8e9b-dc52-4292-b1147fc987af.png)

ランダムに生成された天気予報データが json 形式で取得できます。

では、.NET Aspire の サービスの既定値（Service Defaults） プロジェクトを追加します。ソリューションファイルを右クリック→追加→新しいプロジェクト　を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ed03466e-337a-3091-ea90-1e4b83a8edc8.png)

プロジェクトの種類に「.NET Aspire」を選択すると、.NET Aspire サービスの既定値 がすぐ見つかるはずです。これを選択してソリューションに追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/309a86a7-17df-096a-96bb-cdb0ede4c4a9.png)

ソリューションに2つのプロジェクトが存在する状態になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8cbcb3f8-0481-189c-9430-1fc420c6de63.png)

WebApi プロジェクトから、今追加した ServiceDefaults　プロジェクトをプロジェクト参照します。ServiceDefaults プロジェクトファイルを WebApi プロジェクトにドラッグ＆ドロップしてください。すると、WebApi プロジェクトにプロジェクト参照が追加されます。ちゃんと参照が追加されたかを確認するためには、プロジェクトファイルを選択します。（選択すると自動的に開きます）

```xml:WebApi.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\DashboardDemo.ServiceDefaults\DashboardDemo.ServiceDefaults.csproj" />
  </ItemGroup>

</Project>
```

ProjectReference 要素で サービスの既定値（ServiceDefaults） プロジェクトのパスを参照していることがわかります。

では、WebApi プロジェクトで サービスの既定値（ServiceDefaults） を利用するためのセットアップをします。WebApi プロジェクトの Program.cs を開き、次の2行を追加します。

```diff_c_sharp:WebApi/Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

+ builder.AddServiceDefaults();

var app = builder.Build();

// Configure the HTTP request pipeline.

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
});

+ app.MapDefaultEndpoints();

app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

最後にダッシュボードに対して OpenTelemetry のテレメトリデータを送信します。OpenTelemetryに関する設定はすべて環境変数で行うことができます。最低限、次の２つを設定すればOKです。

- OTEL_EXPORTER_OTLP_ENDPOINT
- OTEL_SERVICE_NAME

WebApi プロジェクトの appsettings.Deveopment.json を開いて、次のように環境変数を2つ追加します。

```diff_json:WebApi/appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
- }
+ },
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
+ "OTEL_SERVICE_NAME": "WebApi"
}
```

実行します。すると、立ち上げておいたダッシュボードに構造化ログ・メトリクス・トレースが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/b3802169-f9ac-62c2-c369-9ad4462891d9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/09e537c8-8892-44b0-f186-3bd67b3ab52f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/dbb25458-1002-7950-015d-7a3f53d67e3d.png)

## Blazor Web プロジェクトを作成し、トレースを確認する

プロジェクトが1つしかないと、複数スパンについてトレース情報がしっかりと表示されるのかがわかりません。これを確認するために、　Web プロジェクトを作成し、 WebApi を呼び出すようにしてみましょう。

ソリューションファイルを右クリック→追加→新しいプロジェクト を選択します。　ダイアログの検索条件で「Web」を選択し、表示された一覧から Blazor Web App を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/404d9351-5d47-e4b8-16ca-324181251d88.png)

「Include sample page」にチェックを入っていることを確認して、作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/5d9ea5f2-8022-dc1b-8672-b9bec195bbdb.png)

Web プロジェクトでも サービスの既定値（ServiceDefaults）プロジェクト　について　WebApi　プロジェクトと同様のセットアップをします。サービスの既定値（ServiceDefaults） プロジェクトファイルを　Web プロジェクトファイルにドラッグ＆ドロップしてプロジェクト参照するようにセットアップしてから、Program.csファイルを開いて次のように２行追加します。

```diff_c_sharp:Web/Program.cs
using DashboardDemo.Web.Components;

var builder = WebApplication.CreateBuilder(args);

+ builder.AddServiceDefaults();

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

+ app.MapDefaultEndpoints();

app.Run();
```

appsettings.Development.json ファイルにも WebApi プロジェクトと全く同じ設定をします。ただし、サービス名は「Web」に変更しておきます。

```diff_json:Web/appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
- }
+ },
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
+ "OTEL_SERVICE_NAME": "Web"
}
```

これで OpenTelemetry の準備は終わりました。次に Web プロジェクトから WebApi を呼び出して天気予報データを取得するように実装します。 Web プロジェクトに WeatherApiClient.cs ファイルを追加して、次の実装に入れ替えてください。ただし namespcae は作成時のままにしておいてください。

```csharp:Web/WeatherApiClient.cs
namespace DashboardDemo.Web;

public class WeatherApiClient(HttpClient httpClient)
{
    public async Task<WeatherForecast[]> GetWeatherAsync()
    {
        return await httpClient.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast") ?? [];
    }
}
public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

今作成した WeatherApiClient を DI する設定を Program.cs に追加実装します。

```diff_c_sharp:Web/Program.cs
using DashboardDemo.Web;
using DashboardDemo.Web.Components;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

+ builder.Services.AddHttpClient<WeatherApiClient>(client =>
+ {
+     client.BaseAddress = new Uri("http://localhost:5028");
+ });

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.MapDefaultEndpoints();

app.Run();
```

localhost の ポート番号に 5028 を指定していますが、これは　WebApi のポート番号です。みなさんの環境では異なる数字のはずですので、適宜修正してください。 WebApi/Properties/launchSettings.json を開き、profiles が http の中にある applicationUrl を確認してください。

最後に Weather ページを修正して、WebApi にアクセスしてデータを取得するようにします。次の内容に丸ごと変更します。

```csharp:Web/Components/Pages/Weather.razor
@page "/weather"
@attribute [StreamRendering]

@inject WeatherApiClient WeatherApi

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<p>This component demonstrates showing data.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await WeatherApi.GetWeatherAsync();
    }

}

```

これで実装はおしまいです。今のままだと実行しても WebApi プロジェクトしか起動しませんので、WebApi プロジェクトが起動した後に Web プロジェクトが起動するようにスタートアップ設定を変更します。

ソリューションファイルを右クリック→スタートアップ プロジェクトの構成　を選択します。表示されたダイアログで、次のように設定します。

1. マルチスタートアップ プロジェクトを選ぶ
1. WebApi　プロジェクトが1番上になるようにして、アクションに開始を選ぶ
1. Web プロジェクトが上から２番目になるようにして、アクションに開始を選ぶ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/35e95494-4ed4-9e37-37b4-4024ecd83502.png)

実行してみましょう。WebApi と Web ページが立ち上がったはずです。Weather ページを開いてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c3974366-f948-af19-898a-3a1f384f4a80.png)

問題なく天気予報データが取得できています。では、ダッシュボードを見てみましょう。トレース画面を見てみると、Web と WebApi のトレースがあります。 詳細列の表示リンクをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/a151f0fb-121e-f8cb-0fda-7bc5e54a5ba4.png)

トレースが各スパンと共に表示されています。良い感じですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/29ae3bf6-3391-afa3-67ce-2c5c98ca1521.png)

# Azure Container Apps でダッシュボードを動かす

:::note info

.NET Aspire の GA の時に Azure Container Apps は正式に.NET Aspire ダッシュボードをサポートしました。以下の記事は手動で.NET Aspire ダッシュボードをデプロイしていますが、このようなことをしなくても Container Apps 環境からボタンクリックだけで .NET Aspire ダッシュボードはデプロイできるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/9e10f940-5f4d-2551-04a7-4f9e74fc7204.png)

以下の情報は Azure Container Apps 以外の環境に.NET Aspire ダッシュボードをデプロイする時の参考のために残しておきます。

:::

ダッシュボードを本番環境で運用することはできるでしょうか。先に結論を言うと、可能です。状況によっては便利かもしれません。まずはやってみましょう。

まず、.NET Aspire Starter アプリケーションを何も修正せずそのまま Azure Container Apps にデプロイします。すると、webfrontend と apiservice という２つのアプリケーションが稼働します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/5c696163-5c92-8851-fd0d-d0d35bf57701.png)

この２つアプリケーションそれぞれについて、環境変数を次のように設定します。

- OTEL_EXPORTER_OTLP_ENDPOINT: http://dashboard:4317
- OTEL_SERVICE_NAME: webfrontend または　apiservice

http://dashboard となっているのは、これからデプロイするダッシュボードのアプリ名を dashboard にするからです。

webfrontend
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/db4aaf20-a44e-5ea9-39c2-1d6d13940b64.png)

apiservice
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/398b6d5a-1e08-27df-d8b3-2c7917c19c68.png)

ここに、ダッシュボードをデプロイします。コンテナアプリの作成画面で、コンテナアプリ名を dashboard と入力して、コンテナーをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1121dce5-f57a-557b-725b-da412094076e.png)

次のように選択・入力してからイングレスをクリックします。

- イメージのソースを「Docker Hub またはその他のレジストリ」
- レジストリログインサーバーを mcr.microsoft.com
- イメージとタグを dotnet/aspire-dashboard:latest

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/56872c63-fbda-dda4-9547-bad55129a6a5.png)

　次のように選択・入力してから確認・作成をクリックします。

 - イングレスを「有効」
 - イングレス トラフィックを「どこからでもトラフィックを受け入れます」にする
 - クライアント証明書モードを「無視」
 - ターゲットポートを 18888
 - 追加のTCPポートでターゲットポートを 18889、公開されたポートを 4317

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1378227d-7917-8aa3-bfa0-b7cfbdeecb5f.png)

デプロイが成功したら、ダッシュボードを開きます。ログインを求められますので、dashboard アプリのリビジョンからコンソールログを開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ec44092d-e60b-925b-6ab7-826f02786c07.png)

トークンをコピーして、ログインします。その後、Webfrontend を開いて Weather ページに何度かアクセスしてみましょう。

問題なく、ログ・トレース・メトリクスが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c8bcd31c-5809-8baa-07e8-39f03fc4ffee.png)

無事に.NET Aspire ダッシュボードをデプロイし、テレメトリをダッシュボードで見ることができるようになりました。
しかし、残念ながらこの方法ではダッシュボードに送信されたテレメトリデータを保存することができません。dashboard コンテナを再起動したら、それまで送信されたテレメトリデータは消えてしまいます。それを理解した上での運用であれば本番環境での運用も良いと思います。

# まとめ

.NET Aspire のダッシュボードを単独で動かすことで、ZipKin　や　Prometheus、Grafana などのツール類をセットアップすることなく、ログ・メトリクス・トレースを画面で簡単に確認することができます。OpenTelemetryは Java, Python, Node.js にも対応していますので、.NET Aspire ダッシュボードは言語を問わず使用可能です。

メインの用途はローカル開発時の状況確認ですが、本番運用でもデータ保存を気にしないのであれば採用できる場合があると思います。今回 Azure Container Apps へのダッシュボードのデプロイ時にはイングレスをインターネット公開しましたが、セキュリティに十分注意するのであれば仮想ネットワーク内部のみからのアクセスのみに限定するのが良いでしょう。