
logging-observability
Production-grade logging and observability patterns for ASP.NET Core Razor Pages. Covers structured logging with Serilog, correlation IDs, health checks, request logging, OpenTelemetry integration, and diagnostic best practices. Use when setting up structured logging in ASP.NET Core applications, implementing distributed tracing with OpenTelemetry, or configuring health checks and observability.
- Installs
- 542
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/wshaddix/dotnet-skills --skill logging-observability- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads logging-observability automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
You are a senior .NET architect specializing in observability. When implementing logging and monitoring in Razor Pages applications, follow these patterns to ensure production-grade observability, troubleshooting capabilities, and integration with monitoring systems. Target .NET 8+ with nullable reference types enabled.
Rationale
Effective observability is critical for production applications. Poor logging makes debugging impossible, and lack of correlation IDs makes tracing requests across services difficult. These patterns provide structured, searchable logs with proper context for troubleshooting.
Core Principles
- Structured Logging: Use structured formats (JSON) for machine parsing
- Correlation IDs: Every request gets a unique ID for end-to-end tracing
- Contextual Enrichment: Logs include relevant context (user, endpoint, duration)
- Log Levels: Use appropriate levels (Debug, Info, Warning, Error, Fatal)
- External Sinks: Send logs to centralized systems (Seq, Datadog, CloudWatch)
Pattern 1: Serilog Configuration
NuGet Packages
<PackageReference Include="Serilog.AspNetCore" Version="8.0.*" />
<PackageReference Include="Serilog.Expressions" Version="4.0.*" />
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.*" /> <!-- Optional -->
Bootstrap Logger (Program.cs Start)
using Serilog;
using Serilog.Debugging;
// Enable Serilog self-logging for diagnostics
SelfLog.Enable(msg => Console.Error.WriteLine($"[SERILOG] {msg}"));
// Create bootstrap logger for startup errors
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console(formatProvider: CultureInfo.CurrentCulture)
.CreateBootstrapLogger();
try
{
Log.Information("Starting web application...");
var builder = WebApplication.CreateBuilder(args);
// ... configure services ...
var app = builder.Build();
await app.RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
await Log.CloseAndFlushAsync();
}
Service Registration
public static class LoggingServiceRegistration
{
public static IServiceCollection ConfigureSerilog(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddSerilog((services, lc) => lc
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.WriteTo.Console(formatter: new ExpressionTemplate(
"[{@t:hh:mm:ss.fff tt} {@l:u3}] {SourceContext} - {CorrelationId} - {@m}\n{@x}",
theme: TemplateTheme.Code))
.WriteTo.Seq(
configuration["Seq:ServerUrl"] ?? "http://localhost:5341",
apiKey: configuration["Seq:ApiKey"],
formatProvider: CultureInfo.CurrentCulture)
);
return services;
}
}
appsettings.json Configuration
{
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.Seq"],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
"Properties": {
"Application": "MyApp"
}
}
}
Pattern 2: Correlation ID Middleware
public class RequestContextLoggingMiddleware
{
private readonly RequestDelegate _next;
private const string CorrelationHeaderName = "X-Correlation-Id";
public RequestContextLoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
var correlationId = GetCorrelationId(httpContext);
// Add to response headers
httpContext.Response.OnStarting(() =>
{
httpContext.Response.Headers[CorrelationHeaderName] = correlationId;
return Task.CompletedTask;
});
using (LogContext.PushProperty("CorrelationId", correlationId))
using (LogContext.PushProperty("RequestPath", httpContext.Request.Path))
using (LogContext.PushProperty("RequestMethod", httpContext.Request.Method))
{
await _next.Invoke(httpContext);
}
}
private static string GetCorrelationId(HttpContext httpContext)
{
httpContext.Request.Headers.TryGetValue(
CorrelationHeaderName, out var correlationId);
return correlationId.FirstOrDefault() ?? httpContext.TraceIdentifier;
}
}
// Extension method for easy registration
public static class RequestContextLoggingExtensions
{
public static IApplicationBuilder UseRequestContextLogging(
this IApplicationBuilder app)
{
return app.UseMiddleware<RequestContextLoggingMiddleware>();
}
}
Registration
// Program.cs
var app = builder.Build();
// Place early in pipeline
app.UseRequestContextLogging();
app.UseSerilogRequestLogging(); // Logs each request
Pattern 3: Request Logging with Serilog
// Program.cs
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("UserId", httpContext.User.Identity?.Name ?? "anonymous");
diagnosticContext.Set("ClientIp", httpContext.Connection.RemoteIpAddress?.ToString());
diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].ToString());
};
options.GetLevel = (httpContext, elapsed, ex) =>
{
// Log 5xx as Error, slow requests as Warning
if (ex != null || httpContext.Response.StatusCode > 499)
return LogEventLevel.Error;
if (elapsed > 1000)
return LogEventLevel.Warning;
return LogEventLevel.Information;
};
});
Custom Request Logging (PageModel)
public class OrderDetailsModel(ILogger<OrderDetailsModel> logger) : PageModel
{
public async Task OnGetAsync(Guid orderId)
{
// Push contextual properties
using (logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = orderId,
["UserId"] = User.Identity?.Name ?? "anonymous"
}))
{
logger.LogInformation("Loading order details");
try
{
var order = await _orderService.GetAsync(orderId);
if (order == null)
{
logger.LogWarning("Order not found");
return NotFound();
}
logger.LogInformation("Order loaded successfully");
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to load order");
throw;
}
}
}
}
Pattern 4: MediatR Pipeline Logging
public class RequestLoggingBehavior<TRequest, TResponse>(ILogger<RequestLoggingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private static readonly string RequestName = typeof(TRequest).Name;
private static readonly TimeSpan SlowRequestThreshold = TimeSpan.FromMilliseconds(500);
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
logger.LogInformation("Handling {RequestName}", RequestName);
var stopwatch = Stopwatch.StartNew();
try
{
var response = await next(cancellationToken);
stopwatch.Stop();
var elapsed = stopwatch.Elapsed;
if (elapsed > SlowRequestThreshold)
{
logger.LogWarning(
"Handled {RequestName} successfully in {ElapsedMs}ms (exceeds {ThresholdMs}ms threshold)",
RequestName,
elapsed.TotalMilliseconds,
SlowRequestThreshold.TotalMilliseconds);
}
else
{
logger.LogInformation(
"Handled {RequestName} successfully in {ElapsedMs}ms",
RequestName,
elapsed.TotalMilliseconds);
}
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
logger.LogError(
ex,
"Error handling {RequestName} after {ElapsedMs}ms",
RequestName,
stopwatch.Elapsed.TotalMilliseconds);
throw;
}
}
}
Registration
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<Program>();
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(RequestLoggingBehavior<,>));
});
Pattern 5: Health Checks
Basic Configuration
// Program.cs
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>(
name: "database",
tags: new[] { "db", "critical" })
.AddCheck<EmailServiceHealthCheck>(
name: "email-service",
tags: new[] { "external", "email" })
.AddRedis(
name: "redis",
tags: new[] { "cache", "distributed" });
var app = builder.Build();
// Simple health endpoint
app.MapHealthChecks("/up");
// Detailed health with authentication
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = HealthCheckResponseWriter.WriteResponse,
AllowCachingResponses = false
}).RequireAuthorization("HealthCheckPolicy");
Custom Health Check
public class EmailServiceHealthCheck(IEmailServiceAgent emailService) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var canConnect = await emailService.PingAsync(cancellationToken);
if (canConnect)
{
return HealthCheckResult.Healthy("Email service is accessible");
}
return HealthCheckResult.Unhealthy("Cannot connect to email service");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"Email service health check failed", ex);
}
}
}
Custom Response Writer
public static class HealthCheckResponseWriter
{
public static Task WriteResponse(
HttpContext context,
HealthReport report)
{
context.Response.ContentType = "application/json";
var response = new
{
Status = report.Status.ToString(),
TotalDuration = report.TotalDuration.TotalMilliseconds,
Checks = report.Entries.Select(e => new
{
Name = e.Key,
Status = e.Value.Status.ToString(),
Duration = e.Value.Duration.TotalMilliseconds,
Exception = e.Value.Exception?.Message,
Data = e.Value.Data
})
};
return context.Response.WriteAsJsonAsync(response);
}
}
Pattern 6: LoggerMessage Pattern (High Performance)
For hot paths, use compile-time logging to avoid string interpolation overhead.
public static partial class LoggerMessages
{
// Information
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Information,
Message = "User {UserId} signed up successfully")]
public static partial void UserSignedUp(ILogger logger, string userId);
// Warning
[LoggerMessage(
EventId = 2001,
Level = LogLevel.Warning,
Message = "Slow database query detected: {QueryName} took {ElapsedMs}ms")]
public static partial void SlowQueryDetected(
ILogger logger, string queryName, long elapsedMs);
// Error
[LoggerMessage(
EventId = 3001,
Level = LogLevel.Error,
Message = "Failed to send email to {EmailAddress}")]
public static partial void EmailSendFailed(
ILogger logger, Exception ex, string emailAddress);
// With scopes
[LoggerMessage(
EventId = 1002,
Level = LogLevel.Information,
Message = "Order {OrderId} processed")]
public static partial void OrderProcessed(ILogger logger, Guid orderId);
}
// Usage
public class SignUpHandler(ILogger<SignUpHandler> logger)
{
public async Task<SignUpResponse> Handle(SignUpRequest request)
{
// ... process signup ...
LoggerMessages.UserSignedUp(logger, user.Id);
return new SignUpResponse { Success = true };
}
}
Pattern 7: OpenTelemetry Integration
Configuration
// NuGet: OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource("MyApp") // Custom activity source
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://localhost:4317");
});
})
.WithMetrics(metrics =>
{
metrics
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddPrometheusExporter();
});
var app = builder.Build();
app.UseOpenTelemetryPrometheusScrapingEndpoint();
Custom Activities
public class OrderProcessingService
{
private static readonly ActivitySource ActivitySource = new("MyApp.Orders");
public async Task ProcessOrderAsync(Order order)
{
using var activity = ActivitySource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", order.Id);
activity?.SetTag("order.total", order.Total);
try
{
activity?.AddEvent(new ActivityEvent("ValidatingOrder"));
await ValidateOrderAsync(order);
activity?.AddEvent(new ActivityEvent("ChargingPayment"));
await ChargePaymentAsync(order);
activity?.AddEvent(new ActivityEvent("SendingConfirmation"));
await SendConfirmationAsync(order);
activity?.SetStatus(ActivityStatusCode.Ok);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
}
Pattern 8: Razor Pages Error Logging
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel(ILogger<ErrorModel> logger) : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string? ErrorMessage { get; set; }
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
// Log the error that brought us here
var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error != null)
{
var ex = exceptionHandlerPathFeature.Error;
var path = exceptionHandlerPathFeature.Path;
logger.LogError(ex,
"Unhandled exception at {Path}. RequestId: {RequestId}",
path, RequestId);
// Only show details in development
if (HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
{
ErrorMessage = ex.ToString();
}
}
}
}
Anti-Patterns
Static Logger Access
// ❌ BAD: Static logger access
public class OrderService
{
private static readonly Logger Logger = Log.ForContext<OrderService>();
}
// ✅ GOOD: Inject ILogger<T>
public class OrderService(ILogger<OrderService> logger)
{
}
String Interpolation in Logs
// ❌ BAD: String interpolation evaluated even if log level is disabled
_logger.LogInformation($"Processing order {orderId} for user {userId}");
// ✅ GOOD: Structured logging with parameters
_logger.LogInformation("Processing order {OrderId} for user {UserId}", orderId, userId);
// ✅ BETTER: Use LoggerMessage for hot paths
[LoggerMessage(EventId = 1001, Level = LogLevel.Information, Message = "Processing order {OrderId} for user {UserId}")]
public static partial void ProcessingOrder(ILogger logger, Guid orderId, string userId);
Catching and Swallowing Exceptions
// ❌ BAD: Silent failures
try
{
await _service.DoWorkAsync();
}
catch (Exception ex)
{
// Nothing logged!
}
// ✅ GOOD: Always log exceptions
catch (Exception ex)
{
_logger.LogError(ex, "Failed to complete work");
throw; // Re-throw if you can't handle it
}
References
- Serilog: https://serilog.net/
- Serilog.AspNetCore: https://github.com/serilog/serilog-aspnetcore
- Health Checks: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks
- OpenTelemetry: https://opentelemetry.io/docs/instrumentation/net/
- LoggerMessage Source Generator: https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator
FAQ
What does the logging-observability skill do?
Production-grade logging and observability patterns for ASP.NET Core Razor Pages. Covers structured logging with Serilog, correlation IDs, health checks, request logging, OpenTelemetry integration, and diagnostic best practices. Use when setting up structured logging in ASP.NET Core applications, implementing distributed tracing with OpenTelemetry, or configuring health checks and observability.
How do I install logging-observability?
Run npx skills add https://github.com/wshaddix/dotnet-skills --skill logging-observability in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/logging-observability/ (personal) or .claude/skills/logging-observability/ (project).
Which AI agents can use logging-observability?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is logging-observability safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains logging-observability?
It is published by wshaddix in wshaddix/dotnet-skills. It has 542 recorded installs.
More from wshaddix/dotnet-skills
dotnet-xunit
wshaddix/dotnet-skills
Writing xUnit tests. v3 Fact/Theory, fixtures, parallelism, IAsyncLifetime, v2 compatibility.
dotnet-testing-strategy
wshaddix/dotnet-skills
Deciding how to test .NET code. Unit vs integration vs E2E decision tree, test doubles.
modern-csharp-coding-standards
wshaddix/dotnet-skills
Write modern, high-performance C code using records, pattern matching, value objects, async/await, Span /Memory , and best-practice API design patterns. Emphasizes functional-style programming with C 12+ features. Use when writing new C code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.
csharp-wolverinefx
wshaddix/dotnet-skills
Build .NET applications with WolverineFX for messaging, HTTP services, and event sourcing. Use when implementing command handlers, message handlers, HTTP endpoints with WolverineFx.HTTP, transactional outbox patterns, event sourcing with Marten, CQRS architectures, cascading messages, batch message processing, or configuring transports like RabbitMQ, Azure Service Bus, or Amazon SQS.
testcontainers
wshaddix/dotnet-skills
Patterns for using Testcontainers in .NET integration tests to spin up real dependencies like databases and message queues. Use when writing integration tests that require real databases, testing with message brokers like RabbitMQ or Kafka, or isolating test dependencies with Docker containers.
microsoft-extensions-dependency-injection
wshaddix/dotnet-skills
Dependency injection patterns and best practices using Microsoft.Extensions.DependencyInjection for .NET applications. Use when configuring DI containers in .NET, choosing between service lifetimes (Singleton, Scoped, Transient), or implementing decorator patterns and service interception.
Popular devops & cloud skills
microsoft-foundry
microsoft/azure-skills
Build, deploy, evaluate, optimize, fine-tune, and manage Microsoft Foundry agents, models, and resources end to end. USE FOR: foundry, azd ai agent, azd provision/deploy, hosted agent scaffold/develop/run/deploy/troubleshoot, prompt agent create, managed harness agent create, GitHub Copilot harness agent, create agent, update agent, add tool to agent, invoke agent, agent insights, evaluate agent, batch eval, continuous eval, continuous monitoring, agent CI/CD, optimize prompt, improve prompt, optimize agent instructions, Agent Optimizer scaffold, dataset curation from traces, deploy model, model fine-tuning (SFT/DPO/RFT), Foundry project, RBAC, role assignment, permissions, quota, capacity, region, deployment failure, AI Services, create Foundry resource, knowledge index, customize deployment, onboard, availability, training-data, grader, distillation, large file upload. DO NOT USE FOR: Azure Functions, App Service, general Azure deploy (use azure-deploy), general Azure prep (use azure
azure-diagnostics
microsoft/azure-skills
Debug Azure production issues on Azure using AppLens, Azure Monitor, resource health, and safe triage. WHEN: debug production issues, troubleshoot app service, app service high CPU, app service deployment failure, troubleshoot container apps, container app 503 or target port mismatch, troubleshoot functions, function telemetry missing or stopped after a config/connection-string change, troubleshoot AKS, VM RDP, Linux SSH, VM black screen, can't connect to VM, reset VM password, NSG or firewall blocking, kubectl cannot connect, kube-system/CoreDNS failures, pod pending, crashloop, node not ready, upgrade failures, analyze logs, KQL, insights, image pull failures, cold start issues, health probe failures, resource health, root cause of errors, troubleshoot event hubs, troubleshoot service bus, messaging SDK error, AMQP connection failure, message lock lost, lock renewal during long-running processing, service bus dead letter.
azure-prepare
microsoft/azure-skills
Prepare azd-based Azure projects for deployment: generates azure.yaml, infrastructure (Bicep/Terraform), and Dockerfiles for the Azure Developer CLI (azd) workflow. USE ONLY when the user explicitly wants to use azd as the deployment tool, or the project already has an azure.yaml file. DO NOT USE FOR: non-azd deployments, Python App Service code-only deploys (use python-appservice-deploy), or cross-cloud migration (use azure-cloud-migrate). WHEN: prepare app for azd, create azure.yaml, set up azd infrastructure, modernize app for Azure with azd, deploy with azd, function app, timer trigger, service bus trigger, event-driven function, managed identity, generate Bicep, generate Terraform, create and deploy to Azure.
azure-ai
microsoft/azure-skills
Use for Azure AI: Search, Speech, OpenAI, Document Intelligence. Helps with search, vector/hybrid search, speech-to-text, text-to-speech, transcription, OCR. WHEN: AI Search, query search, vector search, hybrid search, semantic search, speech-to-text, text-to-speech, transcribe, OCR, convert text to speech.
azure-deploy
microsoft/azure-skills
Execute Azure deployments for ALREADY-PREPARED applications that have existing .azure/deployment-plan.md and infrastructure files. DO NOT use this skill when the user asks to CREATE a new application — use azure-prepare instead. This skill runs azd up, azd deploy, terraform apply, and az deployment commands with built-in error recovery. Requires .azure/deployment-plan.md from azure-prepare and validated status from azure-validate. WHEN: "run azd up", "run azd deploy", "execute deployment", "push to production", "push to cloud", "go live", "ship it", "bicep deploy", "terraform apply", "publish to Azure", "launch on Azure". DO NOT USE WHEN: "create and deploy", "build and deploy", "create a new app", "set up infrastructure", "create and deploy to Azure using Terraform" — use azure-prepare for these.
azure-validate
microsoft/azure-skills
Pre-deployment validation for Azure readiness. Run deep checks on configuration, infrastructure (Bicep or Terraform), RBAC role assignments, managed identity permissions, and prerequisites before deploying. WHEN: validate my app, check deployment readiness, run preflight checks, verify configuration, check if ready to deploy, validate azure.yaml, validate Bicep, test before deploying, troubleshoot deployment errors, validate Azure Functions, validate function app, validate serverless deployment, verify RBAC roles, check role assignments, review managed identity permissions, what-if analysis, validate Container Apps deployment.