opentelemetry について
opentelemetryは、.NET開発者向けのOpenTelemetry実践ガイドです。トレース、メトリクス、ログの設定に加え、メトリクスのライフタイム、Activityのnull安全性、低カーディナリティのタグなどを説明します。
主な機能
- 01トレース、メトリクス、ログを一括設定
- 02OTLPによるテレメトリのエクスポート
- 03IMeterFactoryでメトリクスを作成
- 04ActivitySourceで独自のトレースを追加
- 05ローカル用Aspire Dashboardの設定
活用シーン
- 01.NETアプリで3種類のテレメトリを設定
- 02業務処理にメトリクスを追加
- 03独自スパンで分散処理を追跡
- 04ローカル開発中にテレメトリを確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill opentelemetryインストール後、Claude Code で/opentelemetryを入力して読み込まれたか確認します。
詳細
OpenTelemetry
基本原則
- 3つのシグナルを1か所で設定 — トレース、メトリクス、ログを単一の
AddOpenTelemetry()呼び出しで設定します。UseOtlpExporter()を使用すると、OTLP 互換の任意のバックエンドへ横断的にエクスポートできます。 - メトリクスには
IMeterFactoryを使用 —newでMeterインスタンスを作成しないでください。ファクトリーが DI を通じてライフタイムを管理し、リークを防ぎます。 - アクティビティの null 安全性 — リスナーがアタッチされていない場合、
StartActivity()はnullを返します。タグやイベントを設定するときは、必ず?.を使用してください。 - コードより環境変数を優先 —
OTEL_EXPORTER_OTLP_ENDPOINTとOTEL_SERVICE_NAMEを使用すれば、コードを変更せずにデプロイ環境からテレメトリのルーティングを制御できます。 - メトリクスタグのカーディナリティを低く保つ — 計測器あたりのメトリクスタグの組み合わせを約 1000 未満に保ちます。ユーザー ID やリクエスト ID のようなカーディナリティの高いデータには、スパン属性またはログを使用してください。
パターン
3つのシグナルをすべて使用する完全なセットアップ
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(
serviceName: builder.Environment.ApplicationName,
serviceVersion: "1.0.0"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource("MyApp.Orders"))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("MyApp.Orders"))
.WithLogging() // no per-signal exporter here —
.UseOtlpExporter(); // UseOtlpExporter covers all three signals
// UseOtlpExporter replaces per-signal AddOtlpExporter calls. Never combine
// the two — mixing them throws NotSupportedException (see Anti-patterns).
OTLP エンドポイントの既定値は http://localhost:4317(gRPC)です。次の方法で上書きできます。
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
OTEL_SERVICE_NAME=MyApp.Api
IMeterFactory を使用したカスタムメトリクス
メトリクスクラスをシングルトンとして登録します。IMeterFactory は DI を通じて Meter の破棄を処理します。
public sealed class OrderMetrics
{
private readonly Counter<int> _ordersCreated;
private readonly Histogram<double> _orderDuration;
private readonly UpDownCounter<int> _activeOrders;
private readonly Gauge<double> _queueDepth;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MyApp.Orders");
_ordersCreated = meter.CreateCounter<int>(
"myapp.orders.created", "{orders}", "Number of orders created");
_orderDuration = meter.CreateHistogram<double>(
"myapp.orders.duration", "s", "Order processing duration",
advice: new InstrumentAdvice<double>
{
HistogramBucketBoundaries = [0.01, 0.05, 0.1, 0.5, 1, 5, 10]
});
_activeOrders = meter.CreateUpDownCounter<int>(
"myapp.orders.active", "{orders}", "Currently active orders");
_queueDepth = meter.CreateGauge<double>(
"myapp.orders.queue_depth", "{items}", "Current queue depth");
}
public void OrderCreated() => _ordersCreated.Add(1);
public void RecordDuration(double seconds) => _orderDuration.Record(seconds);
public void OrderStarted() => _activeOrders.Add(1);
public void OrderCompleted() => _activeOrders.Add(-1);
public void SetQueueDepth(double depth) => _queueDepth.Record(depth);
}
// Registration
builder.Services.AddSingleton<OrderMetrics>();
多次元メトリクスタグ
タグが3つ以下の場合、割り当ては発生しません。それより多い場合は TagList を使用してください。
// Allocation-free (3 or fewer tags)
_ordersCreated.Add(1,
new KeyValuePair<string, object?>("order.type", "standard"),
new KeyValuePair<string, object?>("payment.method", "credit_card"));
// 4+ tags — use TagList to avoid allocations
var tags = new TagList
{
{ "order.type", "standard" },
{ "payment.method", "credit_card" },
{ "region", "us-east" },
{ "priority", "high" }
};
_ordersCreated.Add(1, tags);
分散トレース用のカスタム ActivitySource
public sealed class OrderService(ILogger<OrderService> logger)
{
private static readonly ActivitySource Source = new("MyApp.Orders");
public async Task<Order> ProcessOrderAsync(CreateOrderRequest request, CancellationToken ct)
{
using var activity = Source.StartActivity("ProcessOrder", ActivityKind.Internal);
activity?.SetTag("order.customer_id", request.CustomerId);
try
{
await ValidateOrder(request, ct);
activity?.AddEvent(new ActivityEvent("OrderValidated"));
var order = await SaveOrder(request, ct);
activity?.SetTag("order.id", order.Id.ToString());
activity?.SetStatus(ActivityStatusCode.Ok);
return order;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
}
}
トレースビルダーで .AddSource("MyApp.Orders") を指定して、ソースを登録します。
ローカル開発用の Aspire Dashboard
Aspire によるオーケストレーションを使用せずに、スタンドアロンの Aspire Dashboard を実行します。
docker run --rm -it -p 18888:18888 -p 4317:18889 \
mcr.microsoft.com/dotnet/aspire-dashboard:latest
次に、アプリをその Dashboard に接続します。
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
Dashboard の UI は http://localhost:18888 にあります。
OTel を使用したソース生成ログ
最大限のパフォーマンスを得るには、[LoggerMessage] を使用します。これによりボックス化とメモリ割り当てがなくなります。
public partial class OrderService(ILogger<OrderService> logger)
{
[LoggerMessage(Level = LogLevel.Information,
Message = "Processing order {OrderId} for customer {CustomerId}")]
partial void LogOrderProcessing(Guid orderId, Guid customerId);
}
OpenTelemetry のログには、Activity が現在のものである場合、TraceId と SpanId が自動的に含まれます。
アンチパターン
リクエストごとに Meter を作成しない
// BAD — new Meter per request causes memory leaks
public void HandleRequest()
{
var meter = new Meter("MyApp");
meter.CreateCounter<int>("requests").Add(1);
}
// GOOD — singleton via IMeterFactory
public class MyMetrics(IMeterFactory meterFactory)
{
private readonly Counter<int> _requests =
meterFactory.Create("MyApp").CreateCounter<int>("myapp.requests");
public void RequestHandled() => _requests.Add(1);
}
Activity の null チェックを省略しない
// BAD — NullReferenceException when no listener is attached
using var activity = source.StartActivity("Work");
activity.SetTag("key", "value");
// GOOD — null-safe
activity?.SetTag("key", "value");
カーディナリティの高いメトリクスタグを使用しない
// BAD — unbounded cardinality causes memory explosion in collectors
_counter.Add(1, new("request.id", Guid.NewGuid().ToString()));
_counter.Add(1, new("user.id", userId));
// GOOD — low-cardinality dimensions only
_counter.Add(1, new("http.method", "GET"), new("http.status_code", 200));
UseOtlpExporter と AddOtlpExporter を併用しない
// BAD — throws NotSupportedException at runtime
builder.Services.AddOpenTelemetry()
.UseOtlpExporter()
.WithTracing(t => t.AddOtlpExporter());
// GOOD — use one approach
builder.Services.AddOpenTelemetry().UseOtlpExporter();
カスタムソースの登録を忘れない
// BAD — activities silently dropped (no listener registered)
var source = new ActivitySource("MyApp.Custom");
using var activity = source.StartActivity("Work"); // null!
// GOOD — register in the tracing builder
otel.WithTracing(t => t.AddSource("MyApp.Custom"));
otel.WithMetrics(m => m.AddMeter("MyApp.Custom"));
選択ガイド
| シナリオ | 推奨事項 |
|---|---|
| 完全なオブザーバビリティのセットアップ | 3つのシグナルすべてを指定した AddOpenTelemetry() と UseOtlpExporter() |
| カスタムのビジネスメトリクス | IMeterFactory とシングルトンのメトリクスクラス |
| カスタムのトレーススパン | ActivitySource と StartActivity() |
| ローカル開発用バックエンド | スタンドアロンコンテナーの Aspire Dashboard |
| 本番環境のバックエンド | Grafana/Datadog などへの中継役としての OTel Collector |
| 本番環境でのサンプリング | 10% の比率で OTEL_TRACES_SAMPLER=parentbased_traceidratio |
| 高パフォーマンスのログ記録 | [LoggerMessage] ソースジェネレーター |
| メトリクスタグのカーディナリティ | 計測器あたり最大約 1000 の組み合わせ |
| 環境設定 | OTEL_* 環境変数(appsettings.json 経由でも使用可能) |
コメント
Markdown 対応