dotnet-observability について
本番監視や診断に取り組む .NET 開発者向けのオブザーバビリティガイドです。OpenTelemetry のトレースとメトリクス、構造化ログ、ヘルスチェックをまとめています。
主な機能
- 01OpenTelemetry の設定とエクスポート
- 02分散トレースの計装
- 03標準・カスタムメトリクス
- 04構造化ログの実践
- 05ヘルスチェックのパターン
活用シーン
- 01HTTP サービスにトレースとメトリクスを追加
- 02構造化されたアプリケーションログを記録
- 03ヘルスチェックでトラフィック受け入れ可否を公開
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-observabilityインストール後、Claude Code で/dotnet-observabilityを入力して読み込まれたか確認します。
詳細
dotnet-observability
OpenTelemetry、構造化ログ、ヘルスチェック、カスタムメトリクスを使用した、.NET アプリケーション向けの最新のオブザーバビリティです。オブザーバビリティの三本柱(トレース、メトリクス、ログ)、Microsoft.Extensions.Diagnostics および System.Diagnostics との統合、本番環境に対応したヘルスチェックのパターンを扱います。
対象外: DI コンテナーの仕組みとサービスのライフタイムについては、[skill:dotnet-csharp-dependency-injection] を参照してください。Async/await のパターンについては、[skill:dotnet-csharp-async-patterns] を参照してください。オブザーバビリティ出力のテストについては、統合テストでテレメトリを検証する [skill:dotnet-integration-testing] を参照してください。テレメトリ収集の CI/CD パイプライン統合については、[skill:dotnet-gha-patterns] および [skill:dotnet-ado-patterns] を参照してください。ミドルウェアパイプラインのパターン(リクエストログミドルウェア、例外処理ミドルウェア)については、[skill:dotnet-middleware-patterns] を参照してください。
関連項目:[skill:dotnet-csharp-dependency-injection](サービスの登録)、[skill:dotnet-csharp-async-patterns](バックグラウンドエクスポーターの非同期パターン)、[skill:dotnet-resilience](Polly のテレメトリ統合)、[skill:dotnet-middleware-patterns](リクエスト/例外ログミドルウェア)。
OpenTelemetry のセットアップ
OpenTelemetry は .NET における標準のオブザーバビリティフレームワークです。.NET SDK には System.Diagnostics.Activity(トレース)と System.Diagnostics.Metrics(メトリクス)のネイティブサポートが含まれており、OpenTelemetry がこれらを収集してエクスポートします。
パッケージの概要
| パッケージ | 用途 |
|---|---|
OpenTelemetry.Extensions.Hosting | ホストとの統合、ライフサイクル管理 |
OpenTelemetry.Instrumentation.AspNetCore | HTTP サーバーのトレース/メトリクスを自動計測 |
OpenTelemetry.Instrumentation.Http | HttpClient のトレース/メトリクスを自動計測 |
OpenTelemetry.Instrumentation.Runtime | GC、スレッドプール、アセンブリのメトリクス |
OpenTelemetry.Exporter.OpenTelemetryProtocol | コレクター向け OTLP エクスポーター(gRPC/HTTP) |
OpenTelemetry.Exporter.Console | ローカル開発向けコンソールエクスポーター |
コアスタックをインストールします。
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.*" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.*" />
Aspire のサービス既定値との統合
.NET Aspire を使用する場合、ServiceDefaults プロジェクトで OpenTelemetry が自動的に構成されます。Aspire アプリではこちらが推奨される方法です。この構成を手動で重複して設定しないでください。
// ServiceDefaults/Extensions.cs (generated by Aspire)
public static IHostApplicationBuilder AddServiceDefaults(
this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
// ... other defaults
return builder;
}
Aspire を使用しないアプリでは、以下のように OpenTelemetry を明示的に構成します。
完全な構成(Aspire を使用しない場合)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(
serviceName: builder.Environment.ApplicationName,
serviceVersion: typeof(Program).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ?? "unknown"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource("MyApp.*") // Custom ActivitySources
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("MyApp.*") // Custom Meters
.AddOtlpExporter());
環境変数による OTLP の構成
OTLP エクスポーターは標準の環境変数を読み取るため、環境間でコードを変更する必要はありません。
# Collector endpoint (gRPC default)
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# Or HTTP/protobuf
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# Resource attributes
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.namespace=myapp
# Service name (overrides code-based configuration)
OTEL_SERVICE_NAME=order-api
分散トレース
.NET のトレースの仕組み
.NET では、ネイティブのトレースプリミティブとして System.Diagnostics.Activity を使用します。OpenTelemetry はこれらをスパンに対応付けます。
| .NET の概念 | OpenTelemetry の概念 |
|---|---|
ActivitySource | トレーサー |
Activity | スパン |
Activity.SetTag | スパン属性 |
Activity.AddEvent | スパンイベント |
Activity.SetStatus | スパンステータス |
カスタムトレース
public sealed class OrderService
{
// One ActivitySource per logical component, named after the namespace
private static readonly ActivitySource s_activitySource = new("MyApp.Orders");
public async Task<Order> CreateOrderAsync(
CreateOrderRequest request,
CancellationToken ct)
{
using var activity = s_activitySource.StartActivity(
"CreateOrder",
ActivityKind.Internal);
activity?.SetTag("order.customer_id", request.CustomerId);
activity?.SetTag("order.line_count", request.Lines.Count);
var order = new Order { /* ... */ };
activity?.AddEvent(new ActivityEvent("OrderValidated"));
await _db.Orders.AddAsync(order, ct);
await _db.SaveChangesAsync(ct);
activity?.SetTag("order.id", order.Id);
activity?.SetStatus(ActivityStatusCode.Ok);
return order;
}
}
トレースコンテキストの伝播
W3C Trace Context は .NET の既定の伝播形式です。HttpClient を使用すれば、HTTP の境界を越えて自動的に機能します。
// Trace context is automatically propagated via traceparent/tracestate headers
// when using HttpClient with OpenTelemetry.Instrumentation.Http.
// No manual propagation needed for HTTP-based communication.
メッセージベースの通信(キュー、イベントバス)では、コンテキストを明示的に伝播させます。
// Producer: inject context into message headers
var propagator = Propagators.DefaultTextMapPropagator;
var carrier = new Dictionary<string, string>();
var currentActivity = Activity.Current;
if (currentActivity is not null)
{
propagator.Inject(
new PropagationContext(currentActivity.Context, Baggage.Current),
carrier,
(dict, key, value) => dict[key] = value);
}
// Attach carrier as message headers
// Consumer: extract context from message headers
var parentContext = propagator.Extract(
default,
messageHeaders,
(headers, key) => headers.TryGetValue(key, out var value)
? [value] : []);
using var activity = s_activitySource.StartActivity(
"ProcessMessage",
ActivityKind.Consumer,
parentContext.ActivityContext);
メトリクス
組み込みメトリクス
OpenTelemetry の計測を構成すると、ASP.NET Core と HttpClient がメトリクスを自動的に出力します。
| メーター | 主なメトリクス |
|---|---|
Microsoft.AspNetCore.Hosting | http.server.request.duration、http.server.active_requests |
Microsoft.AspNetCore.Routing | aspnetcore.routing.match_attempts |
System.Net.Http | http.client.request.duration、http.client.active_requests |
System.Runtime | process.runtime.dotnet.gc.collections.count、process.runtime.dotnet.threadpool.threads.count |
カスタムメトリクス
アプリケーション固有のメトリクスには System.Diagnostics.Metrics を使用します。
public sealed class OrderMetrics
{
// One Meter per logical component
private readonly Counter<long> _ordersCreated;
private readonly Histogram<double> _orderProcessingDuration;
private readonly UpDownCounter<long> _activeOrders;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MyApp.Orders");
_ordersCreated = meter.CreateCounter<long>(
"myapp.orders.created",
unit: "{order}",
description: "Number of orders created");
_orderProcessingDuration = meter.CreateHistogram<double>(
"myapp.orders.processing_duration",
unit: "s",
description: "Time to process an order");
_activeOrders = meter.CreateUpDownCounter<long>(
"myapp.orders.active",
unit: "{order}",
description: "Number of orders currently being processed");
}
public void RecordOrderCreated(string region)
{
_ordersCreated.Add(1, new KeyValuePair<string, object?>("region", region));
}
public void RecordProcessingDuration(double seconds)
{
_orderProcessingDuration.Record(seconds);
}
public void IncrementActiveOrders() => _activeOrders.Add(1);
public void DecrementActiveOrders() => _activeOrders.Add(-1);
}
メトリクスクラスを DI に登録します。
builder.Services.AddSingleton<OrderMetrics>();
メトリクスの命名規則
OpenTelemetry のセマンティック規約に従ってください。
- 区切り文字にドットを使い、小文字で記述します:
myapp.orders.created - 仕様の標準単位を使います:
s(秒)、ms(ミリ秒)、By(バイト)、{request}(無次元) - アプリケーション/サービス名をプレフィックスにします:
myapp.* - メトリクス全体で一貫したタグ名を使います:
region、status、order.type
構造化ログ
Microsoft.Extensions.Logging(組み込み)
組み込みのログ記録フレームワークは、構造化ログをネイティブにサポートしています。高パフォーマンスなログ記録には、コンパイル時ソースジェネレーターを使ってください。
public static partial class Log
{
[LoggerMessage(
Level = LogLevel.Information,
Message = "Order {OrderId} created for customer {CustomerId} with {LineCount} items, total {Total:C}")]
public static partial void OrderCreated(
this ILogger logger,
string orderId,
string customerId,
int lineCount,
decimal total);
[LoggerMessage(
Level = LogLevel.Warning,
Message = "Order {OrderId} processing exceeded threshold: {Duration}ms")]
public static partial void OrderProcessingSlow(
this ILogger logger,
string orderId,
double duration);
[LoggerMessage(
Level = LogLevel.Error,
Message = "Failed to process order {OrderId}")]
public static partial void OrderProcessingFailed(
this ILogger logger,
Exception exception,
string orderId);
}
// Usage
logger.OrderCreated(order.Id, order.CustomerId, order.Lines.Count, order.Total);
ソース生成ログを使う理由
- 無効なログレベルでは割り当てがゼロです(呼び出し箇所でチェックされます)
- メッセージテンプレートとパラメーターがコンパイル時に検証されます
- デフォルトで構造化されます。パラメーターはログイベント内の名前付きプロパティになります
LoggerMessage.Define(レガシー/.NET 6 より前)
ソースジェネレーターが使えるようになる前は(.NET 5 以前)、LoggerMessage.Defineを使うことで、同じ割り当てゼロのメリットを得られます。この方法は最新の .NET でも引き続き使用でき、partial クラスではないクラスや、古いフレームワークを対象にする場合に便利です。
public static class LogMessages
{
private static readonly Action<ILogger, string, int, Exception?> s_orderCreated =
LoggerMessage.Define<string, int>(
LogLevel.Information,
new EventId(1, nameof(OrderCreated)),
"Order {OrderId} created with {LineCount} items");
public static void OrderCreated(
ILogger logger, string orderId, int lineCount)
=> s_orderCreated(logger, orderId, lineCount, null);
private static readonly Action<ILogger, string, Exception?> s_orderFailed =
LoggerMessage.Define<string>(
LogLevel.Error,
new EventId(2, nameof(OrderFailed)),
"Failed to process order {OrderId}");
public static void OrderFailed(
ILogger logger, string orderId, Exception exception)
=> s_orderFailed(logger, orderId, exception);
}
.NET 6 以降を対象にする新しいコードでは、[LoggerMessage]ソースジェネレーターを優先してください。LoggerMessage.Defineは、ソースジェネレーターを利用できない場合にのみ使ってください。
メッセージテンプレート:推奨事項と避けるべきこと
メッセージテンプレートでは、構造化プロパティになる名前付きプレースホルダーを使います。これは構造化ログの基本です。規則に従わないと、ログのインデックス作成や検索ができなくなります。
// CORRECT: structured message template with named placeholders
logger.LogInformation("Order {OrderId} shipped to {City}", orderId, city);
// WRONG: string interpolation -- bypasses structured logging entirely
logger.LogInformation($"Order {orderId} shipped to {city}");
// WRONG: string concatenation -- same problem
logger.LogInformation("Order " + orderId + " shipped to " + city);
// WRONG: ToString() in template -- loses type information
logger.LogInformation("Order {OrderId} shipped at {Time}",
orderId, DateTime.UtcNow.ToString("o")); // pass DateTime directly
// CORRECT: pass objects directly, let the formatter handle rendering
logger.LogInformation("Order {OrderId} shipped at {ShippedAt}",
orderId, DateTime.UtcNow);
ログレベルのベストプラクティス
| レベル | 使用する場面 | 例 |
|---|---|---|
Trace | 詳細な診断情報(メソッドの開始/終了、変数の値) | Entering ProcessOrder with {OrderId} |
Debug | 開発時に役立つアプリの内部状態 | Cache hit for product {ProductId} |
Information | 通常のアプリケーションフロー、ビジネスイベント | Order {OrderId} created successfully |
Warning | 処理を妨げない予期しない状況 | Retry {Attempt} for external API call |
Error | 現在の処理に影響する失敗 | Failed to save order {OrderId} |
Critical | 直ちに対応が必要なアプリケーション全体の障害 | Database connection pool exhausted |
ログのフィルタリング(Microsoft.Extensions.Logging)
appsettings.jsonでログレベルのフィルタリングを設定し、アプリケーションログを希望するレベルに保ちながら、ノイズの多いフレームワークのログを抑制します。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"System.Net.Http.HttpClient": "Warning",
"MyApp": "Debug"
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}
主なフィルタリング規則:
- 最も具体的なカテゴリが優先されます。より具体的な上書き設定がない場合、
MyApp.OrdersにはMyAppの設定が適用されます - プロバイダーレベルの上書き設定。上記の
Consoleセクションは、コンソールプロバイダーのデフォルト設定のみを上書きします - 環境ごとの上書き設定。本番環境に影響を与えずにローカルで
Debug/Traceを有効にするには、appsettings.Development.jsonを使います
相関関係のためのログスコープ
public async Task<Order> ProcessOrderAsync(
string orderId,
CancellationToken ct)
{
using var scope = _logger.BeginScope(
new Dictionary<string, object>
{
["OrderId"] = orderId,
["CorrelationId"] = Activity.Current?.TraceId.ToString() ?? ""
});
// All log messages within this scope include OrderId and CorrelationId
_logger.LogInformation("Starting order processing");
// ...
}
Serilog の統合
高度なシンク(Elasticsearch、Seq、Datadog)を利用する場合、Serilog が標準的な構造化ログライブラリです。
| パッケージ | 目的 |
|---|---|
Serilog.AspNetCore | UseSerilog()によるホスト統合とUseSerilogRequestLogging() |
Serilog.Settings.Configuration | appsettings.json のバインドに使うReadFrom.Configuration() |
Serilog.Sinks.OpenTelemetry | OTLP シンクのWriteTo.OpenTelemetry() |
Serilog.Formatting.Compact | 構造化されたコンソール出力のためのRenderedCompactJsonFormatter |
Serilog.Enrichers.Environment | Enrich.WithMachineName()とEnrich.WithEnvironmentName() |
// Program.cs
builder.Host.UseSerilog((context, loggerConfiguration) =>
{
loggerConfiguration
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.WriteTo.Console(new RenderedCompactJsonFormatter())
.WriteTo.OpenTelemetry(options =>
{
options.Endpoint = context.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]
?? "http://localhost:4317";
options.Protocol = OtlpProtocol.Grpc;
});
});
// Use Serilog request logging instead of the built-in one
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
};
});
appsettings.jsonで設定します。
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"System.Net.Http.HttpClient": "Warning"
}
}
}
}
MS.Extensions.Logging と Serilog の選び方
| シナリオ | 推奨 |
|---|---|
| コンソールと OTLP エクスポートのみ | Microsoft.Extensions.Logging+OpenTelemetry エクスポーター |
| Elasticsearch、Seq、Datadog のシンクが必要 | Serilog |
| .NET Aspire アプリケーション | 組み込みのログ機能を使います(Aspire は OTLP を自動的に設定します) |
| 高スループットで割り当てを最小限にする | ソース生成のLoggerMessage(どちらでも動作します) |
ヘルスチェック
ヘルスチェックを使うと、オーケストレーター(Kubernetes、Docker、ロードバランサー)が、アプリケーションでトラフィックを受け入れる準備ができているかどうかを判断できます。
ヘルスチェックパッケージ
組み込みの Microsoft.Extensions.Diagnostics.HealthChecks パッケージは、コアフレームワークを提供します。Xabaril/AspNetCore.Diagnostics.HealthChecks のコミュニティパッケージを使うと、プロバイダー固有のチェックを追加できます。
| パッケージ | 拡張メソッド |
|---|---|
AspNetCore.HealthChecks.Npgsql | .AddNpgSql() |
AspNetCore.HealthChecks.Redis | .AddRedis() |
AspNetCore.HealthChecks.Uris | .AddUrlGroup() |
AspNetCore.HealthChecks.UI.Client | UIResponseWriter.WriteHealthCheckUIResponse |
基本的なヘルスチェック
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddNpgSql(
builder.Configuration.GetConnectionString("DefaultConnection")!,
name: "database",
tags: ["ready"])
.AddRedis(
builder.Configuration.GetConnectionString("Redis")!,
name: "redis",
tags: ["ready"])
.AddUrlGroup(
new Uri("https://api.external.com/health"),
name: "external-api",
tags: ["ready"]);
var app = builder.Build();
// Liveness: is the process running? (don't check dependencies)
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
// Readiness: can the process serve traffic? (check dependencies)
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
カスタムヘルスチェック
public sealed class DiskSpaceHealthCheck(
IOptions<DiskSpaceOptions> options) : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken ct = default)
{
var drive = new DriveInfo(options.Value.DrivePath);
var freeSpaceMb = drive.AvailableFreeSpace / (1024 * 1024);
var data = new Dictionary<string, object>
{
["FreeSpaceMB"] = freeSpaceMb,
["DrivePath"] = options.Value.DrivePath
};
if (freeSpaceMb < options.Value.MinimumFreeSpaceMb)
{
return Task.FromResult(HealthCheckResult.Unhealthy(
$"Low disk space: {freeSpaceMb}MB remaining", data: data));
}
return Task.FromResult(HealthCheckResult.Healthy(
$"Disk space OK: {freeSpaceMb}MB free", data: data));
}
}
// Registration
builder.Services.AddHealthChecks()
.AddCheck<DiskSpaceHealthCheck>("disk-space", tags: ["ready"]);
ライブネスとレディネスの違い
| チェック | 目的 | 失敗時のアクション | 例 |
|---|---|---|---|
ライブネス (/health/live) | プロセスは正常ですか? | コンテナーを再起動する | セルフチェック、デッドロックの検出 |
レディネス (/health/ready) | プロセスはトラフィックを受け入れられますか? | ロードバランサーから除外する | データベース、Redis、外部 API |
重要: ライブネスチェックには、依存関係のチェックを含めないでください。データベースがダウンしている場合、アプリを再起動してもデータベースは復旧しません。依存関係の問題で失敗するライブネスチェックは、再起動の連鎖を引き起こします。
ヘルスチェックの公開
HealthCheckPublisherOptions は、定期的な評価スケジュールを制御します。結果を監視システムに送信するには、IHealthCheckPublisher の実装を登録します。
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy());
// Configure periodic evaluation schedule
builder.Services.Configure<HealthCheckPublisherOptions>(options =>
{
options.Delay = TimeSpan.FromSeconds(5); // Initial delay before first run
options.Period = TimeSpan.FromSeconds(30); // Interval between evaluations
});
// Register a publisher to push results (e.g., to logs, metrics, or external systems)
builder.Services.AddSingleton<IHealthCheckPublisher, LoggingHealthCheckPublisher>();
ヘルス状態を記録する最小限のパブリッシャー:
public sealed class LoggingHealthCheckPublisher(
ILogger<LoggingHealthCheckPublisher> logger) : IHealthCheckPublisher
{
public Task PublishAsync(
HealthReport report, CancellationToken ct)
{
logger.LogInformation(
"Health check: {Status} ({TotalDuration}ms)",
report.Status,
report.TotalDuration.TotalMilliseconds);
return Task.CompletedTask;
}
}
まとめ:本番環境の構成
本番環境の .NET API における、オブザーバビリティの完全なセットアップ:
var builder = WebApplication.CreateBuilder(args);
// 1. OpenTelemetry -- traces, metrics, logs
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(builder.Environment.ApplicationName))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource("MyApp.*")
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("MyApp.*")
.AddOtlpExporter());
// 2. Structured logging with OpenTelemetry export
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeScopes = true;
logging.IncludeFormattedMessage = true;
logging.AddOtlpExporter();
});
// 3. Health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddNpgSql(
builder.Configuration.GetConnectionString("DefaultConnection")!,
name: "database",
tags: ["ready"]);
// 4. Custom application metrics
builder.Services.AddSingleton<OrderMetrics>();
var app = builder.Build();
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.Run();
主な原則
- 標準として OpenTelemetry を使用する -- あらゆるバックエンド(Prometheus、Grafana、Datadog、Azure Monitor、AWS X-Ray)で機能する、ベンダー中立の計装を提供します
- DI から
IMeterFactoryを使用する --Meterインスタンスを直接作成しないでください。ファクトリーは DI のライフサイクルや OpenTelemetry の登録と連携します - ホットパスではソース生成された
LoggerMessageを使用する -- ログレベルが無効な場合、アロケーションはゼロです - ライブネスとレディネスを分離する -- ライブネスチェックには依存関係のヘルスチェックを含めず、レディネスチェックには含めます
- 環境変数で構成する -- OTLP エンドポイント、サービス名、リソース属性をコードにハードコードしないでください
- トレースコンテキストでログを拡充する --
TraceIdとSpanIdを使った構造化ログにより、ログとトレースを関連付けられます - メトリクス名とスパン名には、OpenTelemetry のセマンティック規約に従う
エージェントが注意すべき点
IMeterFactoryを使わずに、DI に登録されたサービス内でnewを使ってMeterやActivitySourceを作成しないでください -- ファクトリーの外で作成された計測器は、OpenTelemetry SDK によって収集されません。MeterインスタンスにはIMeterFactory.Create()を使用してください。ActivitySourceは静的で、.AddSource()を通じて登録します。- ライブネスエンドポイントに依存関係のチェックを追加しないでください -- データベースの障害でアプリを再起動してはいけません。依存関係をチェックするのはレディネスエンドポイントのみです。
ILogger.LogInformation("message: " + value)や、文字列補間$"message: {value}"を使用しないでください -- 構造化ログのテンプレートを使用してください:ILogger.LogInformation("message: {Value}", value)。文字列連結や文字列補間は構造化ログを迂回するため、ログのインデックス作成を妨げます。- 本番環境用の OTLP エンドポイントをコード内で構成しないでください -- 環境変数(
OTEL_EXPORTER_OTLP_ENDPOINT)を使用すれば、同じイメージを複数の環境で使用できます。 - カスタム
ActivitySource名を.AddSource("MyApp.*")で登録し忘れないでください -- 登録されていないソースは警告なく無視され、トレースも生成されません。
参考資料
- OpenTelemetry .NET のドキュメント
- OpenTelemetry による .NET の可観測性
- ASP.NET Core のヘルスチェック
- System.Diagnostics.Metrics
- .NET での高パフォーマンスなログ記録
- .NET でのログ記録:ログのフィルタリング
- Serilog OpenTelemetry シンク
クレジット
Aaronontheweb/dotnet-skills をベースにしています(MIT ライセンス)。