例: 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 パッケージを追加します。
<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
</ItemGroup>
注意
OTel API は絶えず進化し続けるので、最新バージョンを使用してください。
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")
.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
ページが要求されるたびに、行われたグリーティングの数のためのカウントをインクリメントします。 同じベース URL でパス /metrics
を使用して、メトリック エンドポイントにアクセスできます。
6.1 ログ出力
コードのログ ステートメントは、ILogger
を使用して出力されます。 既定では、出力がコンソールに送信されるようにコンソール プロバイダーが有効になっています。
.NET からログを出力できる方法には、次のようないくつかのオプションがあります。
stdout
およびstderr
出力は、Kubernetes などのコンテナー システムによってログ ファイルにリダイレクトされます。- ILogger と統合されるログ ライブラリの使用。これには Serilog や NLog が含まれます。
- OTLP や以下でさらに詳しく述べる Azure Monitor エクスポーターなどの OTel のログ プロバイダーの使用。
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 は HTTP ヘッダーとして送信されます。 ASP.NET Core は、要求を受信したときに存在しない場合に TraceId
を割り当てます。 HttpClient
は、既定で送信要求にヘッダーを含めます。 各アクティビティには SpanId
があり、これは各アクティビティを一意に識別する TraceId
と SpanId
の組み合わせです。 Greeter
アクティビティに対しては、その HTTP アクティビティの SpanId
にマッピングされる ParentSpanId
を介してその 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 で、メトリックのクエリを実行できるようになっているはずです。 次の図で強調表示されているボタンを使用して、メトリック エクスプローラーを開きます。ここには使用可能なすべてのメトリックが示されます。
greetings_count
メトリックを選択して、値のグラフを表示します。
8. Grafana を使用してメトリック ダッシュボードを作成する
Grafana は、Prometheus またはその他のデータ ソースに基づいてダッシュボードとアラートを作成できるダッシュボード製品です。
自分のプラットフォーム用の手順に従って、https://grafana.com/oss/grafana/ から OSS バージョンの Grafana をダウンロードしてインストールします。 インストールが完了したら、Grafana は通常、ポート 3000 で実行されるため、ブラウザーで http://localhost:3000
を開きます。 ログインする必要があります。既定のユーザー名とパスワードはどちらも admin
です。
ハンバーガー メニューから接続を選択し、テキスト prometheus
を入力してエンドポイントの種類を選択します。 [Prometheus データ ソースの作成] を選択して、新しいデータ ソースを追加します。
次のプロパティを設定する必要があります。
- Prometheus サーバー URL:
http://localhost:9090/
(必要に応じてポートを変更)
[保存 & テスト] を選択して構成を検証します。
成功メッセージが表示されたら、ダッシュボードを構成できます。 成功メッセージのポップアップに表示される [ダッシュボードの構築] リンクをクリックします。
[視覚化の追加] を選択した後に、先ほどデータ ソースとして追加した Prometheus データ ソースを選択します。
ダッシュボード パネル デザイナーが表示されるはずです。 画面の下半分で、クエリを定義できます。
greetings_count
メトリックを選択した後に、[クエリの実行] を選択して結果を表示します。
Grafana を使用すると、任意の数のメトリックを追跡する洗練されたダッシュボードを設計できます。
.NET の各メトリックは追加のディメンションを持つことができ、これらはデータのパーティション分割に使用できるキーと値のペアです。 ASP.NET メトリックはすべて、カウンターに適用できる多数のディメンションを特徴とします。 たとえば、Microsoft.AspNetCore.Hosting
の current-requests
カウンターには次のディメンションがあります。
属性 | Type | 説明 | 例 | プレゼンス |
---|---|---|---|---|
method |
string |
HTTP 要求メソッド。 | Always (常に) | |
scheme |
string |
使用されるプロトコルを特定する URI スキーム。 | $ | 常時 |
host |
string |
要求を受信したローカル HTTP サーバーの名前。 | localhost |
常時 |
port |
int |
要求を受信したローカル HTTP サーバーのポート。 | 8080 |
既定値 (http は 80、https は 443) でない場合に追加 |
Grafana のグラフは、通常、ディメンションの一意の組み合わせごとにパーティション分割されます。 ディメンションは、Grafana クエリでデータをフィルター処理または集計するために使用できます。 たとえば、current_requests
をグラフ化すると、ディメンションの組み合わせごとにパーティション分割された値が表示されます。 ホストのみに基づいてフィルター処理するには、Sum
の操作を追加し、ラベル値として host
を使用します。
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 を表示できるはずです。
トレースの一覧を表示するには、[サービス] ドロップダウンから OTel-Prometheus-grafana-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"); } }
これにより、各レベルが前の呼び出しからの応答を待機するので、要求がピラミッドの形状を持つより興味深いグラフが作成されます。
.NET