rksoftware

Visual Studio とか C# とかが好きです

.NET Aspire を見てみる OpenTelemetry 編 全部まとめ

以前に .NET Aspire を動かしてみました。
rksoftware.hatenablog.com rksoftware.hatenablog.com その際に、OpenTelemetry をどこで設定しているのか、という疑問がありました。というわけでやっていきます。

■ Nuget パッケージ

まず、OpenTelemetry の機能は Nuget パッケージになっているはずなので、パッケージをアンインストールしてみます。

OpenTelemetry で検索して......

全部アンインストールします、

■ エラーを解消

Nuget パッケージをアンインストールしたので、使用場所がエラーになります。
エラーになっているコードを削除していきます。 ※コードは記事の後ろに記載

■ 実行

実行すると、記録されていません! 今回削除した場所がテレメトリーの設定で良さそうです。

■ エラー

前述した Nuget パッケージを削除したことによるエラーは次のようなものでした。

CS0246   型または名前空間の名前 'OpenTelemetry' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)
CS0246  型または名前空間の名前 'OpenTelemetry' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)
CS0246  型または名前空間の名前 'OpenTelemetry' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)
CS0246  型または名前空間の名前 'MeterProviderBuilder' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)
CS0246  型または名前空間の名前 'MeterProviderBuilder' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)
CS1061  'ILoggingBuilder' に 'AddOpenTelemetry' の定義が含まれておらず、型 'ILoggingBuilder' の最初の引数を受け付けるアクセス可能な拡張メソッド 'AddOpenTelemetry' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください
CS1061  'IServiceCollection' に 'AddOpenTelemetry' の定義が含まれておらず、型 'IServiceCollection' の最初の引数を受け付けるアクセス可能な拡張メソッド 'AddOpenTelemetry' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください
CS0246  型または名前空間の名前 'OpenTelemetryLoggerOptions' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)
CS1061  'IServiceCollection' に 'ConfigureOpenTelemetryMeterProvider' の定義が含まれておらず、型 'IServiceCollection' の最初の引数を受け付けるアクセス可能な拡張メソッド 'ConfigureOpenTelemetryMeterProvider' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください
CS1061  'IServiceCollection' に 'ConfigureOpenTelemetryTracerProvider' の定義が含まれておらず、型 'IServiceCollection' の最初の引数を受け付けるアクセス可能な拡張メソッド 'ConfigureOpenTelemetryTracerProvider' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください

■ コード

変更前 (エラーがある状態)

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

namespace Microsoft.Extensions.Hosting;

public static class Extensions
{
    public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();

        builder.AddDefaultHealthChecks();

        builder.Services.AddServiceDiscovery();

        builder.Services.ConfigureHttpClientDefaults(http =>
        {
            // Turn on resilience by default
            http.AddStandardResilienceHandler();

            // Turn on service discovery by default
            http.UseServiceDiscovery();
        });

        return builder;
    }

    public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
    {
        builder.Logging.AddOpenTelemetry(logging =>
        {
            logging.IncludeFormattedMessage = true;
            logging.IncludeScopes = true;
        });

        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics =>
            {
                metrics.AddRuntimeInstrumentation()
                       .AddBuiltInMeters();
            })
            .WithTracing(tracing =>
            {
                if (builder.Environment.IsDevelopment())
                {
                    // We want to view all traces in development
                    tracing.SetSampler(new AlwaysOnSampler());
                }

                tracing.AddAspNetCoreInstrumentation()
                       .AddGrpcClientInstrumentation()
                       .AddHttpClientInstrumentation();
            });

        builder.AddOpenTelemetryExporters();

        return builder;
    }

    private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
    {
        var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);

        if (useOtlpExporter)
        {
            builder.Services.Configure<OpenTelemetryLoggerOptions>(logging => logging.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
        }

        // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
        // builder.Services.AddOpenTelemetry()
        //    .WithMetrics(metrics => metrics.AddPrometheusExporter());

        // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.Exporter package)
        // builder.Services.AddOpenTelemetry()
        //    .UseAzureMonitor();

        return builder;
    }

    public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
    {
        builder.Services.AddHealthChecks()
            // Add a default liveness check to ensure app is responsive
            .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);

        return builder;
    }

    public static WebApplication MapDefaultEndpoints(this WebApplication app)
    {
        // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
        // app.MapPrometheusScrapingEndpoint();

        // 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;
    }

    private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) =>
        meterProviderBuilder.AddMeter(
            "Microsoft.AspNetCore.Hosting",
            "Microsoft.AspNetCore.Server.Kestrel",
            "System.Net.Http");
}

変更後 (エラーがなくなった状態)

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;

namespace Microsoft.Extensions.Hosting;

public static class Extensions
{
    public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();

        builder.AddDefaultHealthChecks();

        builder.Services.AddServiceDiscovery();

        builder.Services.ConfigureHttpClientDefaults(http =>
        {
            // Turn on resilience by default
            http.AddStandardResilienceHandler();

            // Turn on service discovery by default
            http.UseServiceDiscovery();
        });

        return builder;
    }

    public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
    {


        builder.AddOpenTelemetryExporters();

        return builder;
    }

    private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
    {
        var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);

        if (useOtlpExporter)
        {
        }

        // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
        // builder.Services.AddOpenTelemetry()
        //    .WithMetrics(metrics => metrics.AddPrometheusExporter());

        // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.Exporter package)
        // builder.Services.AddOpenTelemetry()
        //    .UseAzureMonitor();

        return builder;
    }

    public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
    {
        builder.Services.AddHealthChecks()
            // Add a default liveness check to ensure app is responsive
            .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);

        return builder;
    }

    public static WebApplication MapDefaultEndpoints(this WebApplication app)
    {
        // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
        // app.MapPrometheusScrapingEndpoint();

        // 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;
    }
}

■ 差分

前述のコードの差分です。

using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
        builder.Logging.AddOpenTelemetry(logging =>
        {
            logging.IncludeFormattedMessage = true;
            logging.IncludeScopes = true;
        });
        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics =>
            {
                metrics.AddRuntimeInstrumentation()
                       .AddBuiltInMeters();
            })
            .WithTracing(tracing =>
            {
                if (builder.Environment.IsDevelopment())
                {
                    // We want to view all traces in development
                    tracing.SetSampler(new AlwaysOnSampler());
                }

                tracing.AddAspNetCoreInstrumentation()
                       .AddGrpcClientInstrumentation()
                       .AddHttpClientInstrumentation();
            });
            builder.Services.Configure<OpenTelemetryLoggerOptions>(logging => logging.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
    private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) =>
        meterProviderBuilder.AddMeter(
            "Microsoft.AspNetCore.Hosting",
            "Microsoft.AspNetCore.Server.Kestrel",
            "System.Net.Http");

OpenTelemetry の送信先設定はどこ?

OpenTelemetry の送信先設定はどこで設定されているのか、これが今の関心ごとです。

■ 多分

多分ここに Telemetry を送信するようになっているのだと思います。

この OTLP Server (ここでは http://localhost:16061 ) だと思います。しかしどこでこれが Web アプリに設定されるのでしょう?

■ Web アプリの起動時の設定

Web アプリの起動時の設定を見てみると、OTEL_EXPORTER_OTLP_ENDPOINT_ というのがそれっぽい気がします。

というわけでどこで設定されるのか見てみます。

■ 起動プロジェクトの起動時

まだ、ここでは設定されていませんね。

各プロジェクトを実際に起動する Build().Run() の実行前にも設定されていません。

■ API サービスの起動時

冒頭と同じ画像です。ここでは既に設定されていますね。

というわけで、まずは今回のところは Aspire の中でいい感じで設定しているのかなと思いました。

環境変数を書き換えてみます

■ 起動プロジェクトの最初で

環境変数をセットしてみます。

System.Environment.SetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL", "http://localhost:16062");

■ 起動

期待通り、 http://localhost:16062 で起動してきました。

■ 設定

WebFront プロジェクトでも値が変わりました。

{Path = OTEL_EXPORTER_OTLP_ENDPOINT, Value = http://localhost:16062, Provider = EnvironmentVariablesConfigurationProvider}

これですね。

ちゃんと設定を変えてみます

設定を変えてみたいと思います。

■ 起動プロジェクトの launchSettings.json を変更


変更前

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:15250",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "DOTNET_ENVIRONMENT": "Development",
        "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16061"
      }
    }
  }
}

変更後

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:15250",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "DOTNET_ENVIRONMENT": "Development",
        "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16063"
      }
    }
  }
}

■ 実行

変更されました!

■ Web フロントプロジェクトだけ変えたい!

ということで、Web フロントプロジェクトの設定を変更してみます。

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5196",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16064",
        "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:16064"
      }
    }
  }
}

■ 実行

実行時に上書きされてしまうようです。

■ いったん変えられるように

こんな感じにコードを書き換えれば変えられました。ここから先は動かして試すのではなくコードを使う必要がありそうですね。

設定に A という値を追加しました。

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5196",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "A": "http://localhost:16064"
      }
    }
  }
}

Web フロントプロジェクトの起動時のコードの最初で、A の値で環境変数を上書きしてしまいます。

using AspireApp1.Web;
using AspireApp1.Web.Components;

System.Environment.SetEnvironmentVariable(
    "OTEL_EXPORTER_OTLP_ENDPOINT"
    , System.Environment.GetEnvironmentVariable("A")
    );

var builder = WebApplication.CreateBuilder(args);

これで、いったんは変えることができました。

素晴らしい情報の追加

.NET Aspire のテレメトリ送信先の設定についてこれまで実際に動かして確認してきました。

そんな日々の中、素晴らしい情報を得ました。こちらのイベントのセッションでの情報です。
dotnet-communities.connpass.com

■ 情報はこちら

こちらの素晴らしいセッションのセッション資料を貼りますね。
こちらの 16 ページ目に ドキュメント参照 のリンクがあります。 www.docswell.com

.NET Aspire deployment environments should configure OpenTelemetry environment variables that make sense for their environment. For example, OTEL_EXPORTER_OTLP_ENDPOINT should be configured to the environment's local OTLP collector or monitoring service.

.NET Aspire telemetry works best in environments that support OTLP. OTLP exporting is disabled if OTEL_EXPORTER_OTLP_ENDPOINT isn't configured.

さらにリンクをたどっていくと、ここにもたどり着けます。

github.com

とりあえず、動かしてみてあたりをつけていた OTEL_EXPORTER_OTLP_ENDPOINT 環境変数でよさそうですね。