例: OpenTelemetry を Prometheus、Grafana、Jaeger と併用する

この例では、メトリック収集に Prometheus、ダッシュボード作成に Grafana、分散トレースを表示するためには Jaeger を使用します。

1.プロジェクトを作成する

Visual Studio の ASP.NET Core Empty テンプレートまたは次の .NET CLI コマンドを使用して、単純な Web API プロジェクトを作成します。

dotnet new web

2. メトリックおよびアクティビティ定義を追加する

次のコードは、API が呼び出された回数の新しいメトリック (greetings.count) と、新しいアクティビティ ソース (OtPrGrYa.Example) を定義します。

// Custom metrics for the application
var greeterMeter = new Meter("OtPrGrYa.Example", "1.0.0");
var countGreetings = greeterMeter.CreateCounter<int>("greetings.count", description: "Counts the number of greetings");

// Custom ActivitySource for the application
var greeterActivitySource = new ActivitySource("OtPrGrJa.Example");

3. API エンドポイントを作成する

app.MapGet("/", SendGreeting);
async Task<string> SendGreeting(ILogger<Program> logger)
{
    // Create a new Activity scoped to the method
    using var activity = greeterActivitySource.StartActivity("GreeterActivity");

    // Log a message
    logger.LogInformation("Sending greeting");

    // Increment the custom counter
    countGreetings.Add(1);

    // Add a tag to the Activity
    activity?.SetTag("greeting", "Hello World!");

    return "Hello World!";
}

注意

API 定義では、OpenTelemetry に固有のものを使用しません。 監視のために .NET API を使用します。

4. OpenTelemetry パッケージを参照する

NuGet パッケージ マネージャーまたはコマンド ラインを使用して、次の NuGet パッケージを追加します。

dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore --prerelease
dotnet add package OpenTelemetry.Exporter.Zipkin
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http

5. OpenTelemetry と適切なプロバイダーを構成する

var tracingOtlpEndpoint = builder.Configuration["OTLP_ENDPOINT_URL"];
var otel = builder.Services.AddOpenTelemetry();

// Configure OpenTelemetry Resources with the application name
otel.ConfigureResource(resource => resource
    .AddService(serviceName: builder.Environment.ApplicationName));

// Add Metrics for ASP.NET Core and our custom metrics and export to Prometheus
otel.WithMetrics(metrics => metrics
    // Metrics provider from OpenTelemetry
    .AddAspNetCoreInstrumentation()
    .AddMeter(greeterMeter.Name)
    // Metrics provides by ASP.NET Core in .NET 8
    .AddMeter("Microsoft.AspNetCore.Hosting")
    .AddMeter("Microsoft.AspNetCore.Server.Kestrel")
    // Metrics provided by System.Net libraries
    .AddMeter("System.Net.Http")
    .AddMeter("System.Net.NameResolution")
    .AddPrometheusExporter());

// Add Tracing for ASP.NET Core and our custom ActivitySource and export to Jaeger
otel.WithTracing(tracing =>
{
    tracing.AddAspNetCoreInstrumentation();
    tracing.AddHttpClientInstrumentation();
    tracing.AddSource(greeterActivitySource.Name);
    if (tracingOtlpEndpoint != null)
    {
        tracing.AddOtlpExporter(otlpOptions =>
         {
             otlpOptions.Endpoint = new Uri(tracingOtlpEndpoint);
         });
    }
    else
    {
        tracing.AddConsoleExporter();
    }
});

このコードは、ASP.NET Core インストルメンテーションを使用して、ASP.NET Core からメトリックとアクティビティを取得します。 また、メトリックとトレース用にそれぞれ Metrics プロバイダーと ActivitySource プロバイダーを登録します。

このコードはメトリックに Prometheus エクスポーターを使用し、これは ASP.NET Core を使用してエンドポイントをホストするため、次も追加する必要があります。

// Configure the Prometheus scraping endpoint
app.MapPrometheusScrapingEndpoint();

6. プロジェクトを実行する

プロジェクトを実行し、ブラウザーまたは curl を使用して API にアクセスします。

curl -k http://localhost:7275

ページを要求するたびに、行われた案内応答の数がインクリメントされます。 メトリック エンドポイントには、パス /metricsと同じベース URL を使用してアクセスできます。

6.1 ログ出力

コードのログ ステートメントは、ILogger を使用して出力されます。 既定では、出力がコンソールに送信されるようにコンソール プロバイダーが有効になっています。

.NET からログを送信する方法には、いくつかのオプションがあります。

  • stdout および stderr 出力は、Kubernetes などのコンテナー システムによってログ ファイルにリダイレクトされます。
  • ILogger と統合するログ ライブラリの使用。 これらのライブラリには 、Serilog と NLog が含まれます。
  • OTel のログ プロバイダー (OTLP など)、または後で示す Azure Monitor エクスポーターの使用。

6.2 メトリックにアクセスする

/metrics エンドポイントを使用してメトリックにアクセスできます。

curl -k https://localhost:7275/
Hello World!

curl -k https://localhost:7275/metrics
# TYPE greetings_count counter
# HELP greetings_count Counts the number of greetings
greetings_count 1 1686894204856

# TYPE current_connections gauge
# HELP current_connections Number of connections that are currently active on the server.
current_connections{endpoint="127.0.0.1:7275"} 1 1686894204856
current_connections{endpoint="[::1]:7275"} 0 1686894204856
current_connections{endpoint="[::1]:5212"} 1 1686894204856
...

メトリック出力は、エンドポイントが要求された時点でのメトリックのスナップショットです。 結果は Prometheus の開示形式で提供され、これは人間も判読できますが、Prometheus にとって理解しやすい形式となっています。 このトピックは、次のステージでカバーされます。

6.3 トレースにアクセスする

サーバーのコンソールを見ると、コンソール トレース エクスポーターからの出力が確認でき、これは人間が判読できる形式で情報を出力します。 これには、次のようなカスタム ActivitySource からのアクティビティと ASP.NET Core からのアクティビティという 2 つのアクティビティが表示されているはずです。

Activity.TraceId:            2e00dd5e258d33fe691b965607b91d18
Activity.SpanId:             3b7a891f55b97f1a
Activity.TraceFlags:         Recorded
Activity.ParentSpanId:       645071fd0011faac
Activity.ActivitySourceName: OtPrGrYa.Example
Activity.DisplayName:        GreeterActivity
Activity.Kind:               Internal
Activity.StartTime:          2023-06-16T04:50:26.7675469Z
Activity.Duration:           00:00:00.0023974
Activity.Tags:
    greeting: Hello World!
Resource associated with Activity:
    service.name: OTel-Prometheus-Grafana-Jaeger
    service.instance.id: e1afb619-bc32-48d8-b71f-ee196dc2a76a
    telemetry.sdk.name: opentelemetry
    telemetry.sdk.language: dotnet
    telemetry.sdk.version: 1.5.0

Activity.TraceId:            2e00dd5e258d33fe691b965607b91d18
Activity.SpanId:             645071fd0011faac
Activity.TraceFlags:         Recorded
Activity.ActivitySourceName: Microsoft.AspNetCore
Activity.DisplayName:        /
Activity.Kind:               Server
Activity.StartTime:          2023-06-16T04:50:26.7672615Z
Activity.Duration:           00:00:00.0121259
Activity.Tags:
    net.host.name: localhost
    net.host.port: 7275
    http.method: GET
    http.scheme: https
    http.target: /
    http.url: https://localhost:7275/
    http.flavor: 1.1
    http.user_agent: curl/8.0.1
    http.status_code: 200
Resource associated with Activity:
    service.name: OTel-Prometheus-Grafana-Jaeger
    service.instance.id: e1afb619-bc32-48d8-b71f-ee196dc2a76a
    telemetry.sdk.name: opentelemetry
    telemetry.sdk.language: dotnet
    telemetry.sdk.version: 1.5.0

1 つ目は、自分が作成した内部カスタム アクティビティです。 2 つ目は、要求に対して ASP.NET によって作成されたもので、HTTP 要求プロパティのタグが含まれています。 両方とも同じ TraceIdを持ち、1 つのトランザクションを識別していることがわかります。 分散システムでは、トレース ID を使用して、トランザクションに関係する各サービスからのトレースを関連付けることができます。 ID は HTTP ヘッダーとして送信されます。 ASP.NET Core は、要求を受信したときに存在しない場合に TraceId を割り当てます。 HttpClient は、既定で送信要求にヘッダーを含めます。 各アクティビティには SpanId があり、これは各アクティビティを一意に識別する TraceId と SpanId の組み合わせです。 Greeter アクティビティに対しては、その HTTP アクティビティの ParentSpanId にマッピングされる SpanId を介してその HTTP アクティビティが親として設定されます。

後のステージでは、分散トレースを視覚化するために、このデータを Jaeger にフィードします。

7. Prometheus を使用してメトリックを収集する

Prometheus は、メトリック収集、集計、および時系列データベース システムです。 各サービスのメトリック エンドポイントを使用してこれを構成すると、値を定期的にスクレイピングして時系列データベースに保存します。 その後、必要に応じてそれらを分析して処理できます。

Prometheus 形式で公開されるメトリック データは、プロセスのメトリックの特定の時点のスナップショットです。 メトリック エンドポイントに要求が行われるたびに、現在の値が報告されます。 現在の値は興味深いものの、傾向を確認し値が異常な場合の検出を行うために履歴値と比較した方がより価値が高くなります。 一般的に、サービスの利用は、ホリデー シーズンなど、時間帯や世界の出来事に基づいて急増します。 値を過去の傾向と比較することで、異常かどうか、またはメトリックが時間の経過とともに徐々に悪化しているかどうかを検出できます。

このプロセスは、これらのメトリック スナップショットのどの履歴も保存しません。 その機能をプロセスに追加すると、リソースが大量に消費される可能性があります。 また、分散システムでは、通常、各ノードの複数のインスタンスがあるため、それらのすべてからメトリックを収集した後に集計を行い履歴値と比較できることが望まれます。

7.1 Prometheus のインストールと構成

https://prometheus.io/download/ から自分のプラットフォーム用の Prometheus をダウンロードし、ダウンロード内容を展開します。

実行中のサーバーの出力の上部を見て、HTTP エンドポイントのポート番号を取得します。 次に例を示します。

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7275
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5212

Prometheus YAML 構成ファイルを変更して、HTTP スクレイピング エンドポイントのポートを指定し、スクレイピング間隔を低く設定します。 次に例を示します。

  scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: "prometheus"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    scrape_interval: 1s # poll very quickly for a more responsive demo
    static_configs:
      - targets: ["localhost:5212"]

Prometheus を起動し、次のように出力で実行されているポート (通常は 9090) を確認します。

>prometheus.exe
...
ts=2023-06-16T05:29:02.789Z caller=web.go:562 level=info component=web msg="Start listening for connections" address=0.0.0.0:9090

ブラウザーでこの URL を開きます。 Prometheus UI で、メトリックのクエリを実行できるようになりました。 次の図で強調表示されているボタンを使用して、メトリック エクスプローラーを開きます。ここには使用可能なすべてのメトリックが示されます。

Prometheus メトリック エクスプローラー

greetings_count メトリックを選択して、値のグラフを表示します。

greetings_count のグラフ

8. Grafana を使用してメトリック ダッシュボードを作成する

Grafana は、Prometheus またはその他のデータ ソースに基づいてダッシュボードとアラートを作成できるダッシュボード製品です。

自分のプラットフォーム用の手順に従って、https://grafana.com/oss/grafana/ から OSS バージョンの Grafana をダウンロードしてインストールします。 インストールが完了したら、Grafana は通常、ポート 3000 で実行されるため、ブラウザーで http://localhost:3000 を開きます。 ログインする必要があります。既定のユーザー名とパスワードの両方が admin。

ハンバーガー メニューから接続を選択し、 prometheus テキストを入力してエンドポイントの種類を選択します。 [Prometheus データ ソースの作成] を選択して、新しいデータ ソースを追加します。

Prometheus への Grafana の接続

Prometheus サーバーの URL を http://localhost:9090/ に設定し、必要に応じてポートを変更します。

[保存 & テスト] を選択して構成を検証します。

成功メッセージが表示されたら、ダッシュボードを構成できます。 成功メッセージを表示するポップアップにあるダッシュボードの構築リンクを選択します。

[視覚化の追加] を選択した後に、先ほどデータ ソースとして追加した Prometheus データ ソースを選択します。

ダッシュボード パネル デザイナーが表示されるはずです。 画面の下半分で、クエリを定義できます。

greetings_count を使用する Grafana クエリ

greetings_count メトリックを選択した後に、[クエリの実行] を選択して結果を表示します。

Grafana を使用すると、任意の数のメトリックを追跡する洗練されたダッシュボードを設計できます。

.NET の各メトリックは追加のディメンションを持つことができ、これらはデータのパーティション分割に使用できるキーと値のペアです。 ASP.NET メトリックはすべて、カウンターに適用できる多数のディメンションを特徴とします。 たとえば、current-requests の Microsoft.AspNetCore.Hosting カウンターには次のディメンションがあります。

属性 タイプ 説明 例 プレゼンス
method string HTTP 要求メソッド。 GET;POST;HEAD Always (常に)
scheme string 使用されるプロトコルを特定する URI スキーム。 $ Always (常に)
host string 要求を受信したローカル HTTP サーバーの名前。 localhost Always (常に)
port int 要求を受信したローカル HTTP サーバーのポート。 8080 既定値 (http は 80、https は 443) でない場合に追加

Grafana のグラフは、通常、ディメンションの一意の組み合わせごとにパーティション分割されます。 ディメンションは、Grafana クエリでデータをフィルター処理または集計するために使用できます。 たとえば、current_requests をグラフ化すると、ディメンションの組み合わせごとにパーティション分割された値が表示されます。 ホストのみに基づいてフィルター処理するには、Sum の操作を追加し、ラベル値として host を使用します。

Grafana のホストごとの current_requests

9. Jaeger を使用した分散トレース

手順 6 では、分散トレース情報がコンソールへ公開されているのを確認しました。 この情報は、アクティビティの作業単位を追跡します。 一部のアクティビティは、ASP.NET による要求の処理を表すものなど、プラットフォームによって自動的に作成され、ライブラリとアプリ コードもアクティビティを作成できます。 グリーティングの例には、Greeter アクティビティがあります。 アクティビティは、TraceId、SpanId および ParentId タグを使用して関連付けられます。

分散システム内の各プロセスは、独自のアクティビティ情報のストリームを生成するので、メトリックと同様に、各トランザクションで行われた作業を視覚化できるようにアクティビティの収集、保存、関連付けを行うシステムが必要です。 Jaeger は、この収集と視覚化を可能にするオープンソース プロジェクトです。

ご使用のプラットフォーム用の Jaeger の最新のバイナリ配布アーカイブを https://www.jaegertracing.io/download/ からダウンロードします。

次に、簡単にアクセスできるローカルの場所にダウンロードを展開します。 次のように jaeger-all-in-one(.exe) 実行可能ファイルを実行します。

./jaeger-all-in-one --collector.otlp.enabled

コンソール出力を確認して、gRPC 経由で OTLP トラフィックをリッスンしているポートを見つけます。 次に例を示します。

{"level":"info","ts":1686963686.3854616,"caller":"otlpreceiver@v0.78.2/otlp.go:83","msg":"Starting GRPC server","endpoint":"0.0.0.0:4317"}

この出力は、0.0.0.0:4317 でリッスンしていることを示します。そのため、そのポートを OTLP エクスポーターの宛先として構成できます。

プロジェクトの AppSettings.json ファイルを開き、次の行を追加して、必要がある場合はポートを変更します。

"OTLP_ENDPOINT_URL" :  "http://localhost:4317/"

グリーター プロセスがプロパティの変更を取得し、トレース情報の Jaeger への送信を開始できるように、グリーター プロセスを再起動します。

これで、Web ブラウザーから http://localhost:16686/ で Jaeger UI を表示できるはずです。

Jaeger のトレース取得用クエリ

トレースの一覧を表示するには、OTel-Prometheus-grafana-Jaeger ドロップダウンから を選択します。 トレースを選択すると、そのトレースの一部としてアクティビティのガント チャートが表示されます。 各操作をクリックすると、アクティビティの詳細が表示されます。

Jaeger 操作の詳細

分散システムでは、同じ Jaeger インストールがシステム全体のトランザクションを関連付けることができるように、すべてのプロセスからのトレースを同じ Jaeger インストールに送信することが望まれます。

アプリに自分自身に対する HTTP 呼び出しを行わせることで、アプリをもう少し面白くすることができます。

  • HttpClient ファクトリをアプリケーションに追加します。

    builder.Services.AddHttpClient();
    
  • 入れ子になったあいさつの呼び出しを行う新しいエンドポイントを追加します。

    app.MapGet("/NestedGreeting", SendNestedGreeting);
    
  • アプリがトレース可能な HTTP 呼び出しを行えるようにエンドポイントを実装します。 この場合、それは人工ループで自分自身を呼び戻します(実際にはデモシナリオにのみ適用されます)。

    async Task SendNestedGreeting(int nestlevel, ILogger<Program> logger, HttpContext context, IHttpClientFactory clientFactory)
    {
        // Create a new Activity scoped to the method
        using var activity = greeterActivitySource.StartActivity("GreeterActivity");
    
        if (nestlevel <= 5)
        {
            // Log a message
            logger.LogInformation("Sending greeting, level {nestlevel}", nestlevel);
    
            // Increment the custom counter
            countGreetings.Add(1);
    
            // Add a tag to the Activity
            activity?.SetTag("nest-level", nestlevel);
    
            await context.Response.WriteAsync($"Nested Greeting, level: {nestlevel}\r\n");
    
            if (nestlevel > 0)
            {
                var request = context.Request;
                var url = new Uri($"{request.Scheme}://{request.Host}{request.Path}?nestlevel={nestlevel - 1}");
    
                // Makes an http call passing the activity information as http headers
                var nestedResult = await clientFactory.CreateClient().GetStringAsync(url);
                await context.Response.WriteAsync(nestedResult);
            }
        }
        else
        {
            // Log a message
            logger.LogError("Greeting nest level {nestlevel} too high", nestlevel);
            await context.Response.WriteAsync("Nest level too high, max is 5");
        }
    }
    

これにより、各レベルが前の呼び出しからの応答を待機するので、要求がピラミッドの形状を持つより興味深いグラフが作成されます。

Jaeger の入れ子になった依存関係の結果