ログの 1 行を見つめているとします: Order abc123 failed: error:timeout。どのサービスがタイムアウトしたのでしょうか? 決済でしょうか? データベースでしょうか? それともネットワークでしょうか? ClickStack を開き、トレース ID をクリックすると、リクエスト全体のタイムラインが即座に表示されます。Order API は Payment Service を 3 秒間待機しており、接続が切断された時点でも Payment Service は不正検知チェックを実行中でした。わずか 2 クリックで根本原因を特定できたのです。
これこそが分散トレーシングの力です。ASP.NET で最小限の OpenTelemetry 設定を行うだけで、独立したログ行の確認から ClickStack でのサービス横断の完全な実行ビューへと移行し、サービス境界を越えてどこに時間が費やされ、どこで問題が発生したかを正確に把握できます。
本記事では、OpenTelemetry を組み込んだ 2 つの ASP.NET サービスを構築し、データを SQLite に永続化して、トレース、ログ、メトリクスを ClickStack へ送信します。
構築するもの

ClickStack は、OpenTelemetry 向けのオープンソースのオールインワン・オブザーバビリティスタックです。標準的な OTel データを受け入れ、ClickHouse に保存し、UI で探索できるようにしながら、基盤となるテレメトリへの直接の SQL アクセスも提供します。
本記事では、相互に通信しデータを SQLite に永続化する 2 つの ASP.NET サービスを構築します:
- Order API: 注文を受け付け、在庫を確認し、Payment Service を呼び出し、完了した注文を SQLite に保存します。
- Payment Service: 設定可能な障害モードで決済処理をシミュレートし、決済結果を SQLite に保存します。
両サービスともに OpenTelemetry が組み込まれており、3 つのシグナル(トレース、ログ、メトリクス)すべてを OTLP/gRPC 経由で ClickStack にエクスポートします。SQLite を採用しているのは、EF Core の計装によってデータベーススパンが自動的に現れる様子を示しながら、デモを自己完結させるためです。SQLite レイヤーは EF Core 計装パッケージによって自動計装されるため、手動でスパンを作成することなくデータベース操作が ClickStack に表示されます。
1 件の注文のフローは以下のとおりです:
- クライアントが Order API に POST リクエストを送信
- Order API が在庫を確認(SQLite 内の製品カタログ)
- Order API が HTTP 経由で Payment Service を呼び出し
- Payment Service が不正検知チェックを実行し、請求を処理して、結果を SQLite に保存
- Order API が決済結果を受信し、注文を SQLite に保存
完成すると、ClickStack を使用して、複数サービスやデータベース呼び出しにまたがる単一のリクエストを追跡できるようになります。Order API によるリクエストの検証、不正検知チェックと請求処理のための Payment Service への HTTP 呼び出し、そして両側でのデータベース書き込みが、すべて 1 つのトレース ID の配下にネストされて表示されます。

なぜ ClickStack なのか?
- OpenTelemetry と標準で連携。 ClickStack は OTLP/gRPC エンドポイントを標準で提供します。OTel SDK の送信先をそこに設定するだけで、トレース、ログ、メトリクスの送信が開始されます。カスタムエクスポーターも、スキーマのセットアップも、管理すべき中間パイプラインも不要です。
- ClickHouse をバックエンドに採用。 ClickHouse は、大規模データセットに対するリアルタイム分析向けに構築されたオープンソースの列指向データベースです。すべてのテレメトリデータは ClickHouse のテーブルに格納されるため、列指向圧縮(通常 10〜20 倍)、数十億のスパンに対する 1 秒未満の分析クエリ、転置インデックスによる全文検索が活用できます。用途が限定されたクエリ言語ではなく、本格的なデータベースのパワーが得られます。しかも、従来のオブザーバビリティソリューションと比較してわずかなコストで実現できます。
- シグナル間の相関付け。 ClickStack はトレース、ログ、メトリクスをまとめて受信するため、それらを自動的にリンクできます。ログ行をクリックして親トレースにジャンプしたり、特定のトレースの時間枠に絞り込んだログを表示したり、メトリクスのレイテンシスパイクから原因となった個々のスパンまでドリルダウンしたりできます。
- すべてに SQL でアクセス可能。 テレメトリは標準の ClickHouse テーブルに保存されます。SQL で直接クエリを実行したり、リアルタイム集計用のマテリアライズドビューを構築したり、組み込み UI に加えて Grafana などのツールを接続したりできます。
Elasticsearch との比較では、ClickHouse は現実的なベンチマークにおいて約 5 倍優れた圧縮率と 4 倍以上の高速なクエリを達成しています。 Trip.com は Elasticsearch から ClickHouse へ移行し、同一ハードウェア上でデータ容量を 4 倍に拡大した 50 PB 規模のロギングプラットフォームを構築しました。
インフラのセットアップ
スタック全体は Docker Compose で動作します。オブザーバビリティ側は ClickStack がすべてを処理します。イメージには、ストレージ用の ClickHouse、取り込み用の OTLP/gRPC コレクター、探索用のオブザーバビリティ UI がバンドルされています。
services:
clickstack:
image: docker.io/clickhouse/clickstack-all-in-one:2.21.0
ports:
- "8080:8080" # ClickStack UI
- "18123:8123" # ClickHouse HTTP (Play UI)
volumes:
- ./clickstack/entry.sh:/etc/local/entry.sh:ro
- clickhouse_data:/var/lib/clickhouse
- clickhouse_logs:/var/log/clickhouse-server
healthcheck:
test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1:8123/ping || exit 1"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s次に 2 つの ASP サービスを追加します。これらは ClickStack が健全(healthy)になってから起動するように設定されており、さらに、すべてが立ち上がった後に自動でトラフィックを生成する seed-data コンテナも追加します:
order-api:
build:
context: .
dockerfile: src/OrderApi/Dockerfile
ports:
- "5000:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- OTEL_EXPORTER_OTLP_ENDPOINT=http://clickstack:4317
- PaymentService__BaseUrl=http://payment-service:8080
depends_on:
clickstack:
condition: service_healthyこの OTEL_EXPORTER_OTLP_ENDPOINT 環境変数だけで、OTel SDK がデータの送信先を認識できます。ClickStack はデフォルトでポート 4317 に OTLP/gRPC レシーバーを公開しています。
すべてを起動します:
docker compose up -d
Payment Service と Order API の構築
OpenTelemetry のセットアップ
Program.cs 内の OTel 設定でトレース、メトリクス、ログを構成します:
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(DiagnosticConfig.ServiceName))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource(DiagnosticConfig.ActivitySourceName)
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddMeter(DiagnosticConfig.MeterName)
.AddOtlpExporter());
builder.Logging.AddOpenTelemetry(options =>
{
options.IncludeFormattedMessage = true;
options.IncludeScopes = true;
options.AddOtlpExporter();
});注目すべき点がいくつかあります:
- 3 つの計装ライブラリが一般的なケースをカバーしています:
AddAspNetCoreInstrumentation()は受信 HTTP リクエストを捕捉し、AddHttpClientInstrumentation()は送信 HTTP 呼び出しを捕捉し、AddEntityFrameworkCoreInstrumentation()はデータベース操作を捕捉します。 ConfigureResource(resource => resource.AddService(DiagnosticConfig.ServiceName)): これにより、ClickStack 内でサービス名が表示されるようになります。AddSource(DiagnosticConfig.ActivitySourceName)は、カスタムスパンをリッスンするようトレーサーに指示します(詳細は後述)。- 各シグナルの
AddOtlpExporter()は、OTEL_EXPORTER_OTLP_ENDPOINTが指す先(今回は ClickStack)へ OTLP/gRPC 経由でデータを送信します。 - ログは
builder.Logging.AddOpenTelemetry()経由で個別に設定されます。IncludeFormattedMessageとIncludeScopesオプションにより、ログメッセージが人間にとって読みやすくなり、スコープのコンテキストが含まれるようになります。
カスタムスパンとメトリクス
DiagnosticConfig クラスですべてのテレメトリ定義を一元管理します:
public static class DiagnosticConfig
{
public const string ServiceName = "payment-service";
public const string ActivitySourceName = "PaymentService.Payments";
public const string MeterName = "PaymentService.Metrics";
public static readonly ActivitySource ActivitySource = new(ActivitySourceName);
public static readonly Meter Meter = new(MeterName);
public static readonly Counter<long> PaymentsProcessed = Meter.CreateCounter<long>(
"payments.processed",
description: "Number of payments processed");
public static readonly Histogram<double> FraudCheckDuration = Meter.CreateHistogram<double>(
"fraud_check.duration",
unit: "ms",
description: "Duration of fraud check processing");
}.NET では、OpenTelemetry は System.Diagnostics をベースに構築されているため、スパンやメトリクスを作成するためのネイティブなプリミティブとして ActivitySource と Meter を使用します。
実際の実装は次のようになります: PaymentProcessor クラスは各処理ステップに対して子スパンを作成します:
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
var paymentId = Guid.NewGuid().ToString("N")[..12];
// Start Activity for trace and enrich it with tags
using var activity = DiagnosticConfig.ActivitySource.StartActivity("process-payment");
activity?.SetTag("payment.id", paymentId);
activity?.SetTag("payment.order_id", request.OrderId);
activity?.SetTag("payment.amount", request.Amount);
// Step 1: Fraud check (creates its own child span)
var fraudScore = await RunFraudCheckAsync(paymentId, request);
// Step 2: Determine outcome based on configured rates
var outcome = DetermineOutcome();
// Step 3: Process the charge (creates its own child span)
var result = await ProcessChargeAsync(paymentId, request, outcome, fraudScore);
// Persist to SQLite (auto-instrumented by EF Core)
await using var db = await _dbFactory.CreateDbContextAsync();
db.Payments.Add(result);
await db.SaveChangesAsync();
// Record metrics
DiagnosticConfig.PaymentsProcessed.Add(1,
new KeyValuePair<string, object?>("status", result.Status),
new KeyValuePair<string, object?>("payment_method", request.PaymentMethod));
return result;
}不正検知チェックスパンには、スコアが疑わしい場合のイベントが含まれます。これらはすべて ClickStack のトレースウォーターフォールに表示されます:
private async Task<int> RunFraudCheckAsync(string paymentId, PaymentRequest request)
{
using var activity = DiagnosticConfig.ActivitySource.StartActivity("fraud-check");
var sw = Stopwatch.StartNew();
// 不正検知チェックのレイテンシをシミュレート(10〜50ms)
var delay = Random.Shared.Next(10, 51);
await Task.Delay(delay);
var fraudScore = Random.Shared.Next(0, 101);
activity?.SetTag("fraud.score", fraudScore);
activity?.SetTag("fraud.delay_ms", delay);
if (fraudScore > 70)
{
activity?.AddEvent(new ActivityEvent("suspicious-activity", tags: new ActivityTagsCollection
{
{ "fraud.score", fraudScore },
{ "payment.amount", request.Amount },
}));
}
sw.Stop();
DiagnosticConfig.FraudCheckDuration.Record(sw.Elapsed.TotalMilliseconds);
return fraudScore;
}設定可能な障害モード
Payment Service はすべてを承認するだけではありません。デモ内で多様なログやトレースが得られるように、現実的な障害モードをシミュレートします(発生頻度は PaymentConfiguration.cs で設定できます)。
タイムアウトのケースはトレーシングにおいて特に興味深いものです: Payment Service が 3〜8 秒スリープするのに対し、Order API の HTTP クライアントタイムアウトは 3 秒に設定されています。これにより、Payment Service が正常に処理を続けている最中に、Order API 側では TaskCanceledException が発生するというシナリオが生まれます。その両側の挙動が ClickStack のトレースに表示されます。
サービスを跨ぐ分散トレーシング
Order API が Payment Service を呼び出す際、トレースコンテキストは HTTP ヘッダーを介して自動的に伝播されます。これは、AddHttpClientInstrumentation() が送信リクエストに traceparent ヘッダーを注入し、Payment Service 側の AddAspNetCoreInstrumentation() がそれを抽出するためです。手動での相関付けは不要です。
OrderService は、先ほどの Payment Service と同様の方法で、注文処理の各ステップのスパンを作成します。生成されるトレースウォーターフォールには、一連の完全な処理の流れが表示されます: place-order → validate-order → call-payment-service → HTTP POST /payments →(Payment Service のスパン)→ SaveChanges(EF Core/SQLite)。
SQLite と Entity Framework Core によるデータベースレイヤー
両サービスとも、Entity Framework Core を使用してデータを SQLite に永続化します。
自動計装されるデータベーススパン
OpenTelemetry.Instrumentation.EntityFrameworkCore パッケージは、EF Core 内部の DiagnosticSource イベントにフックします。すべての SaveChangesAsync()、FirstOrDefaultAsync()、およびその他の EF Core 操作は、標準の OTel データベースセマンティック規則に従ったスパンを自動的に生成します。セットアップはスタートアップ設定の 1 行で完了します:
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation() // <-- instruments database calls
.AddSource(DiagnosticConfig.ActivitySourceName)
.AddOtlpExporter())テストトラフィックの生成
Order API には現実的な負荷を生成する /generate-traffic エンドポイントが含まれており、Docker Compose 内の seed-data コンテナが起動時にこのエンドポイントを自動的に呼び出します。さらにデータを追加したい場合は、以下を実行するだけです:
curl -X POST http://localhost:5000/generate-trafficClickStack でのテレメトリの探索
トラフィックが流れ始めたら、http://localhost:8080 で ClickStack を開きます。
OTel パイプラインが 3 つのシグナルすべてを ClickStack に送信するため、ログ単体では不可能な機能が利用できます。自動検出されるサービスマップ、分散トレースウォーターフォール、ログとトレースの相関ビュー、データベース操作の内訳などです。ClickStack UI はこのデータを探索するための簡単な方法を提供します。あらゆる種類のシグナルの検索、フィルタリング、そして類似パターンをグループ化して根本原因分析を加速するログクラスタリングが利用できます。また、ClickStack は ClickHouse の極めて高速な転置インデックスによる全文検索をサポートしており、最近のリリースでは ClickStack UI 内で直接テキストインデックスのサポートが追加されました。
分散トレースとログ

正常に処理された注文のトレースには、完全なウォーターフォールが表示されます:
place-order(Order API)validate-order(Order API)call-payment-service(Order API)HTTP POST /payments(HttpClientInstrumentationによる自動計装)process-payment(Payment Service)fraud-check(Payment Service)process-charge(Payment Service)- 両側の EF Core
SaveChangesスパン (自動計装)
ウォーターフォール内の任意のスパンやログをドリルダウンして、それらのプロパティをすべて確認できます。
エラーの追跡
ClickStack のイベントパターン機能を使用すると、類似したメッセージを自動的にクラスタリングして、エラーのパターンを迅速に特定できます。これにより、何百万ものメッセージを調べる代わりに、少数のグループを確認するだけで済みます。

グループをクリックすると、個々のメッセージが表示されます:

さらにそのいずれかをクリックすると、メッセージのプロパティ、トレースウォーターフォール、ログコンテキスト、および関連サービスのマップが表示されます。
ログとトレースの相関付け
トレース対象のリクエスト中に発行されたすべてのログ行には、自動的にトレース ID とスパン ID が付与されます。ClickStack では、任意のログ行をクリックして親トレースに直接ジャンプできます。手動での相関付けは不要です。OTel ログエクスポーターがこれを自動的に処理します。
これは逆方向にも機能します。トレースを表示している際、ClickStack はそのトレースの実行中に発行されたログを自動的に表示します。また、データベース呼び出しも計装されているため、すべてのデータベース操作もウォーターフォール内に表示されます。つまり、トレース ID に一致するログを手動で検索する必要はなく、コンテキスト内に直接表示されます。この自動相関付けは、OTel + ClickStack パイプラインの最大の利点の 1 つであり、手動で連携処理を作り込むことなく全体像を把握できます。

メトリクス
ClickStack 内でメトリクスに基づいたカスタムダッシュボードを構築できます。デモには、注文処理サービスを監視し、警告ログやエラーログに簡単にアクセスできるようにするダッシュボードがあらかじめ用意されています。

これらのメトリクスに基づいてアラートを定義することも可能です。ClickStack は、Slack、PagerDuty、または汎用 Webhook によるアラート連携をサポートしています。
組み込みダッシュボード
ClickStack には、標準でいくつかのダッシュボードも用意されています。これらを使用して ClickHouse を監視したり、サービス(自動検出)やデータベース呼び出しの最も重要なメトリクスを表示したり、Kubernetes イベントを探索したりできます。
サービスダッシュボードでは、主要なエンドポイント、レイテンシ、エラーが強調表示されます。ここのデータは SQL または Lucene を使用してフィルタリングできます。サービスマップも、分散トレースから order-api と payment-service の関係を自動的に検出します。手動での設定は不要です。

最後に、データベースタブにはサービス内のデータベース操作の統計情報が表示されます。EF Core の自動計装を使用しているため、すべてのクエリおよび保存操作が標準の db.* 属性とともに捕捉されます。操作のレイテンシ、スループット、エラー率を一目で確認できます。

本番環境での考慮事項
このデモはシンプルさとわかりやすさを優先しています。大規模な本番ワークロード向けに ClickStack を最適化するための包括的なガイドについては、ClickStack のパフォーマンスチューニングのドキュメントを参照してください。追加を検討すべき項目をいくつか挙げます:
- リソース属性: 本番環境でのデータのフィルタリングに役立つ
deployment.environment、service.version、service.instance.idを追加します。Kubernetes では、OTel Operator またはOTEL_RESOURCE_ATTRIBUTES環境変数を使用して、k8s.namespace.name、k8s.pod.name、k8s.deployment.name、およびその他のクラスターメタデータを自動的に注入できます。ClickStack のデフォルトのテーブルスキーマは、高速なフィルタリングのためにこれらの Kubernetes 属性をすでに専用カラムにマテリアライズしています。ユーザー側で行うべきは、それらが OTel リソースに存在することを確認するだけです。 - バッチエクスポーターのチューニング: デフォルトのバッチエクスポーター設定(バッチサイズ 512、エクスポート間隔 5 秒)は妥当ですが、スループットに応じて調整が必要になる場合があります。
- セキュリティ: OTLP エンドポイントの TLS を有効にし、認証ヘッダーを追加します。ClickStack は OTLP 取り込み用の API キーをサポートしています。
- マテリアライズドビュー: データ量が増加するにつれて、ClickStack はインクリメンタルマテリアライズドビューを自動的に活用してダッシュボードやアラートを高速化できます。挿入時にデータを事前集計するビュー(サービス別・分別の平均リクエスト時間など)を定義すると、ClickStack は該当する可視化処理でそれを透過的に使用します。ダッシュボード側の変更は不要です。
- アラート: 保存された検索(エラー率の急上昇など)やダッシュボードのグラフ(p99 レイテンシがしきい値を超えるなど)に対してアラートを設定します。ClickStack は定期的な間隔でそれらを評価し、Slack、PagerDuty、または汎用 Webhook 経由で通知します。
まとめ
ASP.NET でのわずかな OpenTelemetry のセットアップにより、単一のタイムアウトログの行から、HTTP 呼び出し、アプリケーションコード、データベース操作にまたがる、実際に何が起きたのかという完全なサービス横断ビューへと移行できました。どのサービスが失敗したかを推測したりログをつなぎ合わせたりする代わりに、リクエストをエンドツーエンドで追跡し、どこで時間が費やされ、どこでエラーが発生し、どのようなログが出力され、どのデータベース呼び出しが関与していたかを確認できます。
ClickStack は、標準の OpenTelemetry データを受け入れ、すべてのシグナルを自動的に相関付け、ClickHouse にすべて保存することで、これを容易に実現します。探索用の UI を備えた高速で柔軟なバックエンドが得られ、さらに詳細に掘り下げる必要がある場合には SQL アクセスも利用できます。
デモをクローンし、docker compose up -d を実行して、ぜひご自身で試してみてください。いくつかの障害を発生させ、トレースを開き、リクエストを追跡してみましょう。
参考資料
今すぐ始める
ClickHouse が自社のデータでどのように機能するか試してみませんか?ClickHouse Cloud は数分で利用開始でき、300 ドル分の無料クレジットも受け取れます。
サインアップ


