mcpField
D
dotnet-observability

dotnet-observability

github.com/wshaddixクラウド・DevOps8680 回インストール35 週間前に更新
リポジトリ

dotnet-observability について

本番監視や診断に取り組む .NET 開発者向けのオブザーバビリティガイドです。OpenTelemetry のトレースとメトリクス、構造化ログ、ヘルスチェックをまとめています。

主な機能

  1. 01OpenTelemetry の設定とエクスポート
  2. 02分散トレースの計装
  3. 03標準・カスタムメトリクス
  4. 04構造化ログの実践
  5. 05ヘルスチェックのパターン

活用シーン

  1. 01HTTP サービスにトレースとメトリクスを追加
  2. 02構造化されたアプリケーションログを記録
  3. 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.AspNetCoreHTTP サーバーのトレース/メトリクスを自動計測
OpenTelemetry.Instrumentation.HttpHttpClient のトレース/メトリクスを自動計測
OpenTelemetry.Instrumentation.RuntimeGC、スレッドプール、アセンブリのメトリクス
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.Hostinghttp.server.request.duration、http.server.active_requests
Microsoft.AspNetCore.Routingaspnetcore.routing.match_attempts
System.Net.Httphttp.client.request.duration、http.client.active_requests
System.Runtimeprocess.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.AspNetCoreUseSerilog()によるホスト統合とUseSerilogRequestLogging()
Serilog.Settings.Configurationappsettings.json のバインドに使うReadFrom.Configuration()
Serilog.Sinks.OpenTelemetryOTLP シンクのWriteTo.OpenTelemetry()
Serilog.Formatting.Compact構造化されたコンソール出力のためのRenderedCompactJsonFormatter
Serilog.Enrichers.EnvironmentEnrich.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.ClientUIResponseWriter.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 のセマンティック規約に従う

エージェントが注意すべき点

  1. IMeterFactory を使わずに、DI に登録されたサービス内で new を使って Meter や ActivitySource を作成しないでください -- ファクトリーの外で作成された計測器は、OpenTelemetry SDK によって収集されません。Meter インスタンスには IMeterFactory.Create() を使用してください。ActivitySource は静的で、.AddSource() を通じて登録します。
  2. ライブネスエンドポイントに依存関係のチェックを追加しないでください -- データベースの障害でアプリを再起動してはいけません。依存関係をチェックするのはレディネスエンドポイントのみです。
  3. ILogger.LogInformation("message: " + value) や、文字列補間 $"message: {value}" を使用しないでください -- 構造化ログのテンプレートを使用してください:ILogger.LogInformation("message: {Value}", value)。文字列連結や文字列補間は構造化ログを迂回するため、ログのインデックス作成を妨げます。
  4. 本番環境用の OTLP エンドポイントをコード内で構成しないでください -- 環境変数(OTEL_EXPORTER_OTLP_ENDPOINT)を使用すれば、同じイメージを複数の環境で使用できます。
  5. カスタム ActivitySource 名を .AddSource("MyApp.*") で登録し忘れないでください -- 登録されていないソースは警告なく無視され、トレースも生成されません。

参考資料


クレジット

Aaronontheweb/dotnet-skills をベースにしています(MIT ライセンス)。

コメント