dotnet-structured-logging について
dotnet-structured-loggingは、分散ログパイプラインを設計・運用する.NETチーム向けのスキルです。ログ出力設定ではなく、集約、構造化クエリ、量の管理、PIIの除去、サービス間の相関を扱います。
主な機能
- 01ELK、Seq、Loki、Azure Monitorの構成例
- 02構造化クエリの例
- 03サンプリングとログ量の管理
- 04PIIのマスキングと削除の指針
- 05サービス間相関の実践方法
活用シーン
- 01ログ集約アーキテクチャの選定
- 02構造化ログからサービスエラーを検索
- 03高スループット環境でのログ取り込み量削減
- 04サービスやキューをまたぐイベントの相関
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-structured-loggingインストール後、Claude Code で/dotnet-structured-loggingを入力して読み込まれたか確認します。
詳細
dotnet-structured-logging
.NET 分散システム向けのログパイプライン設計と運用について説明します。ログ集約アーキテクチャ(ELK、Seq、Grafana Loki)、各プラットフォームでの構造化クエリパターン、ログのサンプリングとボリューム管理戦略、PII のスクラブとデストラクチャリングのポリシー、単一サービスのログスコープを超えたサービス間の相関を扱います。このスキルでは、ログ出力後のパイプライン、クエリ、運用のレイヤーを扱います。
対象外: ログ出力の仕組み(Serilog/NLog/MEL の設定、ソース生成された LoggerMessage、エンリッチャー、単一サービスのログスコープ、シンクの登録、OTel ログのエクスポート)については、[skill:dotnet-observability] を参照してください。アプリケーションの設定とオプションパターンについては、[skill:dotnet-csharp-configuration] を参照してください。分散トレーシングの設定とトレースコンテキストの伝播については、[skill:dotnet-observability] を参照してください。
相互参照:ログ出力、Serilog/MEL の設定、OpenTelemetry ログのエクスポートについては [skill:dotnet-observability] を、ログパイプラインの設定に使用する appsettings.json の構成パターンについては [skill:dotnet-csharp-configuration] を参照してください。
ログ集約アーキテクチャ
アーキテクチャの選択肢
| プラットフォーム | 取り込み | ストレージ | クエリ | 最適な用途 |
|---|---|---|---|---|
| ELK(Elasticsearch、Logstash、Kibana) | Logstash / Filebeat | Elasticsearch | Kibana の KQL | 大規模環境、柔軟なスキーマ、全文検索 |
| Seq | HTTP API / Serilog sink | 組み込み | Seq のシグナル式 | .NET ネイティブ、開発者に使いやすい、構造化クエリ |
| Grafana Loki | Promtail / OTel Collector | Loki(ラベルインデックス方式) | LogQL | コスト効率に優れ、Grafana エコシステムに対応、ラベルベースのクエリ |
| Azure Monitor | OTel Collector / Application Insights SDK | Log Analytics ワークスペース | KQL(Kusto) | Azure ネイティブ、統合されたアラート、コスト管理 |
推奨されるパイプラインパターン
パターン 1:中央ルーターとしての OTel Collector
App (OTLP) --> OTel Collector --> Elasticsearch / Loki / Azure Monitor
|
+--> Sampling / filtering / PII scrub
OpenTelemetry Collector は、ベンダーに依存しないログルーターとして機能します。アプリケーションは OTLP 経由でログを出力し、Collector がフィルタリング、サンプリング、エンリッチメントを行い、1 つ以上のバックエンドにルーティングします。これにより、アプリケーションをバックエンドの選択から切り離せます。
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
processors:
batch:
timeout: 5s
send_batch_size: 1024
filter:
logs:
exclude:
match_type: strict
bodies:
- "Health check endpoint hit"
exporters:
elasticsearch:
endpoints: ["https://es-cluster:9200"]
logs_index: "app-logs"
loki:
endpoint: "http://loki:3100/loki/api/v1/push"
service:
pipelines:
logs:
receivers: [otlp]
processors: [batch, filter]
exporters: [elasticsearch, loki]
パターン 2:直接シンク(小規模なデプロイ)
App (Serilog) --> Seq / Elasticsearch sink
小規模なシステムや開発環境では、Serilog シンクから集約プラットフォームに直接書き込みます。これにより OTel Collector は不要になりますが、アプリケーションがバックエンドに依存します。
.NET アプリケーションの OTLP 設定
.NET アプリケーション側の OTLP ログエクスポートの設定(builder.Logging.AddOpenTelemetry())については、[skill:dotnet-observability] を参照してください。OTLP エンドポイントは環境変数(OTEL_EXPORTER_OTLP_ENDPOINT)で設定し、アプリケーションコードがバックエンドに依存しないようにします。
構造化クエリパターン
構造化ログでは、各プロパティがクエリ可能なフィールドとして保存されます。クエリ構文はプラットフォームによって異なりますが、プロパティ名、値、重大度、時間範囲でフィルタリングするという考え方は共通しています。
Kibana KQL(Elasticsearch / ELK)
# Find errors for a specific order
level: "Error" AND OrderId: "abc-123"
# Find slow operations (custom Duration property)
Duration > 5000 AND ServiceName: "order-api"
# Wildcard on message template
message: "Failed to process*" AND NOT level: "Debug"
# Time-scoped with correlation
TraceId: "0af7651916cd43dd8448eb211c80319c" AND @timestamp >= "2025-01-15T10:00:00"
Seq シグナル式
# Find errors for a specific order
@Level = 'Error' and OrderId = 'abc-123'
# Find slow operations
Duration > 5000 and Application = 'order-api'
# Free-text search combined with structured filter
@Message like '%timeout%' and @Level in ['Warning', 'Error']
# Correlation across services
TraceId = '0af7651916cd43dd8448eb211c80319c'
Seq のシグナルは、アラートをトリガーする保存済みクエリです。「支払い失敗が 1 分間に 10 件超」など、繰り返し発生するパターンに対してシグナルを定義し、通知チャネルを紐付けます。
Grafana LogQL(Loki)
# Filter by labels then regex on log line
{service_name="order-api"} |= "Error" | json | OrderId="abc-123"
# Structured field extraction and filtering
{service_name="order-api"} | json | Duration > 5000
# Count errors per service over time (for dashboards)
sum(rate({service_name=~".+"} |= "Error" [5m])) by (service_name)
Azure Monitor KQL(Kusto)
// Find errors for a specific order
traces
| where severityLevel >= 3
| where customDimensions.OrderId == "abc-123"
| order by timestamp desc
// Slow operations
traces
| where toint(customDimensions.Duration) > 5000
| where cloud_RoleName == "order-api"
// Cross-service correlation
union traces, exceptions
| where operation_Id == "0af7651916cd43dd8448eb211c80319c"
| order by timestamp asc
ログのサンプリングとボリューム管理
高スループットのシステムでは、1 分間に何百万ものログイベントが生成されることがあります。サンプリングを行わないと、ストレージコストが増大し、クエリのパフォーマンスが急速に低下します。
サンプリング戦略
| 戦略 | 仕組み | 適用場面 |
|---|---|---|
| ヘッドベース | 処理前にサンプリングするかどうかを決定します | リクエストごとに一貫性を保つ場合。実装が簡単です |
| テールベース | 処理後にサンプリングするかどうかを決定します | すべてのエラーや低速リクエストを保持し、通常のログを破棄する場合 |
| レベルベース | 重大度に応じてサンプリングします | Warning 以上は常に保持し、Debug/Info はサンプリングする場合 |
| 動的 | ログ量に応じてサンプリング率を調整します | 設定を変更せずにトラフィックの急増に対応する場合 |
OTel Collector のログフィルタリング
OTel Collector の filter プロセッサは、ログレコードがエクスポーターに到達する前に、パイプラインレベルでログレコードを破棄します。ノイズの多い低重大度のログを除外し、ストレージ容量を削減するために使用します。
注:tail_sampling プロセッサが処理するのはログではなくトレース(スパン)です。ログの量を管理するには、代わりに filter プロセッサと transform プロセッサを使用してください。
processors:
filter:
logs:
exclude:
match_type: regexp
# Drop Debug and Trace logs at the collector level
severity_texts: ["DEBUG", "TRACE"]
exclude:
match_type: strict
# Exclude health check noise
bodies:
- "Health check endpoint hit"
transform:
log_statements:
- context: log
conditions:
# Keep all Warning+ logs unconditionally
- severity_number >= SEVERITY_NUMBER_WARN
statements: []
Serilog によるアプリケーションレベルのサンプリング
// Serilog.Expressions package for conditional log filtering
builder.Host.UseSerilog((context, loggerConfiguration) =>
{
loggerConfiguration
.ReadFrom.Configuration(context.Configuration)
// Drop health check logs entirely
.Filter.ByExcluding("RequestPath = '/health/ready'")
// Sample Debug logs at 10%
.Filter.ByExcluding(
"@Level = 'Debug' and Hash(@i) % 10 != 0");
});
主要なパッケージ:
<PackageReference Include="Serilog.Expressions" Version="5.*" />
ログ量管理のチェックリスト
- インデックス/ストリームごとに保持ポリシーを設定する(例:Info は 30 日間、Error は 90 日間)
- ログレベルのフィルタリングを使用して、ノイズの多いフレームワークカテゴリを発生源で抑制する
- リクエストログからヘルスチェックのエンドポイントを除外する
- インデックスライフサイクル管理を適用する(Elasticsearch の ILM、Loki の保持ポリシー)
- 取り込みレートを監視し、ストレージコストに対する予算アラートを設定する
PII のスクラブとデストラクチャリングポリシー
本番環境のログに個人を特定できる情報(PII)を含めてはいけません。GDPR、HIPAA、SOC 2 では、機密データをマスキングするか、ログストレージから除外することが求められています。
エンリッチャーによるプロパティレベルのマスキング
// Enricher that masks known-sensitive properties on every log event
public sealed class PiiMaskingEnricher : ILogEventEnricher
{
private static readonly HashSet<string> s_sensitiveKeys = new(
StringComparer.OrdinalIgnoreCase)
{
"Email", "PhoneNumber", "IpAddress",
"CreditCard", "SSN", "Password"
};
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory)
{
var propertiesToMask = logEvent.Properties
.Where(p => s_sensitiveKeys.Contains(p.Key))
.Select(p => p.Key)
.ToList();
foreach (var key in propertiesToMask)
{
logEvent.AddOrUpdateProperty(
factory.CreateProperty(key, "***REDACTED***"));
}
}
}
// Registration
loggerConfiguration.Enrich.With<PiiMaskingEnricher>();
OTel Collector の属性処理
processors:
attributes:
actions:
# Mask email addresses using regex
- key: user.email
action: update
value: "***@redacted.com"
# Remove sensitive attributes entirely
- key: http.request.header.authorization
action: delete
- key: user.password
action: delete
PII スクラブのチェックリスト
- PII フィールドを特定する -- メールアドレス、電話番号、IP アドレス、SSN、クレジットカード、認証トークン、Cookie
- できるだけ早い段階で適用する -- クエリ時ではなく、エンリッチャーまたは OTel プロセッサで適用する
- ログテンプレートを監査する -- 構造化ログテンプレートで、PII を名前付きプロパティとして取り込まないことを確認する
- コンプライアンスチームとテストする -- スクラブのルールが規制要件に適合していることを検証する
- PII が正当に必要な監査ログには、別の保持ポリシーを使用する(保存時の暗号化を適用)
サービス間の相関
分散システムでは、1 つのユーザーリクエストが複数のサービスを経由する場合があります。相関を利用すると、すべてのサービスをまたいでリクエストを追跡し、イベントの全体的な時系列を再構成できます。
W3C Trace Context による相関
主な相関メカニズムは W3C の traceparent ヘッダーです。OpenTelemetry の計装が構成されている場合、HttpClient を通じて自動的に伝播します([skill:dotnet-observability] を参照)。トレース対象のリクエスト内で出力されたすべてのログイベントには、TraceId プロパティと SpanId プロパティが含まれます。
// Query all logs for a distributed operation across services
// In Seq:
TraceId = '0af7651916cd43dd8448eb211c80319c'
// In Kibana:
TraceId: "0af7651916cd43dd8448eb211c80319c"
// In Azure Monitor:
traces | where operation_Id == "0af7651916cd43dd8448eb211c80319c"
カスタム相関 ID
トレースコンテキストでは不十分な場合(例:メッセージキューをまたぐ非同期ワークフロー、バッチジョブ、外部システムからのコールバック)は、カスタム相関 ID を追加します。
// Propagate a business correlation ID through Serilog LogContext
public sealed class CorrelationIdMiddleware(RequestDelegate next)
{
private const string CorrelationHeader = "X-Correlation-Id";
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers[CorrelationHeader]
.FirstOrDefault() ?? Guid.NewGuid().ToString("N");
context.Response.Headers[CorrelationHeader] = correlationId;
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await next(context);
}
}
}
// Registration
app.UseMiddleware<CorrelationIdMiddleware>();
メッセージキューの相関
非同期メッセージング(Azure Service Bus、RabbitMQ)では、メッセージプロパティを通じて相関情報を伝播します。
// Producer -- attach correlation to message
var message = new ServiceBusMessage(payload)
{
CorrelationId = Activity.Current?.TraceId.ToString()
?? Guid.NewGuid().ToString("N"),
ApplicationProperties =
{
["BusinessCorrelationId"] = orderId.ToString()
}
};
// Consumer -- restore correlation in log scope
processor.ProcessMessageAsync += async args =>
{
using var scope = logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = args.Message.CorrelationId,
["BusinessCorrelationId"] =
args.Message.ApplicationProperties["BusinessCorrelationId"]
});
logger.LogInformation("Processing message {MessageId}", args.Message.MessageId);
await ProcessAsync(args.Message, args.CancellationToken);
};
相関のベストプラクティス
| プラクティス | 理由 |
|---|---|
ログ出力には常に TraceId を含める | 可観測性プラットフォームでログとトレースを結合できます |
ビジネスフローには CorrelationId を使用する | トレースコンテキストがリセットされる非同期処理の切れ目をまたいで維持されます |
| 相関 ID をメッセージヘッダーに保存する | キューを通じたエンドツーエンドのトレースが可能になります |
| エラーレスポンスに相関情報を含める | サポートチームが完全なトレースを検索できます |
Serilog の LogContext.PushProperty または MEL の BeginScope を使用する | スコープ内のすべてのログイベントに自動的に付加されます |
エージェントが陥りやすい落とし穴
- ログの出力とログパイプラインを混同しないでください -- このスキルでは、パイプライン、クエリ、運用について扱います。Serilog/MEL の設定、エンリッチャー、シンクの登録、ソース生成される LoggerMessage については、[スキル:dotnet-observability] を参照してください。
- 本番環境のログに PII を保存しないでください -- パイプラインレベルでマスキング用エンリッチャーや OTel プロセッサルールを適用してください。保存後の墨消しではコンプライアンス要件を満たせません。
- 高スループットのサービスでログのサンプリングを省略しないでください -- 毎秒数千件のリクエストを処理するサービスで Debug/Info ログをサンプリングせずに出力すると、ストレージが圧迫され、クエリのパフォーマンスが低下します。エラーと低速なリクエストをすべて保持するには、テールベースのサンプリングを使用してください。
- アプリケーションコードに集約プラットフォームのエンドポイントをハードコードしないでください -- 環境変数(
OTEL_EXPORTER_OTLP_ENDPOINT)または設定を使用し、同じイメージを複数の環境で動作させてください。 - ビジネス上の相関に TraceId だけを頼らないでください -- 非同期境界(メッセージキュー、スケジュールされたジョブ)でトレースコンテキストはリセットされます。これらの境界をまたぐワークフローには、明示的なビジネス相関 ID を追加してください。
- 保持ポリシーを忘れないでください -- 保持ポリシーのないログは無期限に蓄積し、コストを増加させ、クエリを遅くします。重要度ごとに保持期間を設定してください(例:Info は 30 日、Error は 90 日)。