.claude/skills/distributed-logging-correlation/SKILL.md
Use when implementing structured logging with correlation IDs across service boundaries, diagnosing requests that cannot be traced end-to-end across services, or when log messages use string interpolation causing allocations on every call. Covers LoggerMessage source generator for zero-allocation logging, correlation ID middleware, W3C trace context propagation via Activity, OpenTelemetry integration, log scope context, and PII scrubbing rules. Domain: Observability, Logging, Tracing. Level: Intermediate. Tags: logging, correlation-id, opentelemetry, structured-logging, tracing.
npx skillsauth add klod68/littlerae distributed-logging-correlationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Without structured logging and trace correlation:
Combine high-performance structured logging with Activity-based trace propagation for end-to-end observability across services.
public static partial class LogMessages
{
[LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} placed by customer {CustomerId}")]
public static partial void OrderPlaced(ILogger logger, int orderId, int customerId);
[LoggerMessage(Level = LogLevel.Warning, Message = "Retry attempt {Attempt} for order {OrderId}")]
public static partial void RetryAttempt(ILogger logger, int attempt, int orderId);
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to process order {OrderId}")]
public static partial void OrderProcessingFailed(ILogger logger, int orderId, Exception exception);
}
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public async Task PlaceOrderAsync(Order order, CancellationToken ct)
{
LogMessages.OrderPlaced(_logger, order.Id, order.CustomerId);
// ... business logic
}
}
public class CorrelationIdMiddleware
{
private const string CorrelationHeader = "X-Correlation-Id";
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers[CorrelationHeader].FirstOrDefault()
?? Activity.Current?.Id
?? Guid.NewGuid().ToString();
context.Response.Headers[CorrelationHeader] = correlationId;
using (_logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId
}))
{
await _next(context);
}
}
}
public class CorrelationDelegatingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
if (Activity.Current is not null)
{
request.Headers.TryAddWithoutValidation("X-Correlation-Id", Activity.Current.Id);
}
return await base.SendAsync(request, ct);
}
}
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService("OrderService"))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation(o => o.SetDbStatementForText = true)
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter());
public static class Telemetry
{
public static readonly ActivitySource Source = new("OrderService");
}
// Usage
using var activity = Telemetry.Source.StartActivity("ProcessOrder");
activity?.SetTag("order.id", orderId);
activity?.SetTag("order.total", total);
public async Task ProcessOrderAsync(int orderId, CancellationToken ct)
{
using var scope = _logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = orderId,
["Operation"] = "ProcessOrder"
});
LogMessages.OrderProcessingStarted(_logger, orderId);
// All log messages within this scope include OrderId and Operation
}
LoggerMessage source generator to avoid allocation.Console.WriteLine output (but even then, ILogger is better).ILogger<T> instead)._logger.LogInformation($"Order {orderId}") allocates a string on EVERY call, even when the log level is disabled. Use LoggerMessage or structured parameters: _logger.LogInformation("Order {OrderId}", orderId).BeginScope, log messages lack context. Add correlation ID, request ID, and operation name to every scope.Warning as the minimum in production. Information is acceptable for key business events. Never leave Debug or Trace enabled in production.ActivitySource is configured or no listener is active, Activity.Current will be null. Always null-check before using.Information level creates noise and storage costs. Log business events, not implementation details.Exceptions with nested InnerException chains lose context when only the top-level Message is logged.
public static class ExceptionFormatter
{
/// <summary>Formats the full exception chain into a single readable string.</summary>
public static string Format(Exception exception, bool includeStackTrace = false)
{
var messages = new List<string> { exception.Message };
var inner = exception.InnerException;
while (inner is not null)
{
messages.Add($"Inner: {inner.Message}");
inner = inner.InnerException;
}
var formatted = string.Join(" | ", messages);
if (includeStackTrace && !string.IsNullOrWhiteSpace(exception.StackTrace))
formatted += $" | StackTrace: {exception.StackTrace}";
return formatted;
}
/// <summary>Extracts just the root cause message (deepest InnerException).</summary>
public static string GetRootCauseMessage(Exception exception)
{
var current = exception;
while (current.InnerException is not null)
current = current.InnerException;
return current.Message;
}
}
Connection timeout expired | Inner: A connection attempt failed | Inner: No such host is known
InnerException." | " as separator for single-line log compatibility.AggregateException.Flatten() for Task-related exceptions.Error handlers that synchronously write to file or network loggers can throw their own exceptions, masking the original error.
Log exceptions asynchronously with _ = LogAsync(...) (fire-and-forget) and swallow all logging failures. An error handler must never throw.
public static class ErrorLogger
{
/// <summary>Logs exception to file asynchronously. Non-blocking, never throws.</summary>
public static async Task LogToFileAsync(string context, Exception exception, string? logPath = null)
{
try
{
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
var message = $"[{timestamp}] [{context}] {ExceptionFormatter.Format(exception)}";
var path = logPath ?? Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "logs", "errors.log");
var directory = Path.GetDirectoryName(path);
if (directory is not null) Directory.CreateDirectory(directory);
await File.AppendAllTextAsync(path, message + Environment.NewLine)
.ConfigureAwait(false);
}
catch
{
// Swallow ALL logging errors — never throw from an error handler
}
}
}
// Usage: _ = ErrorLogger.LogToFileAsync(context, exception);
await the fire-and-forget call in the error handler — use discard _ =.tools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.