MCPInstall

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
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads logging-observability automatically when relevant.

  3. 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

  1. Structured Logging: Use structured formats (JSON) for machine parsing
  2. Correlation IDs: Every request gets a unique ID for end-to-end tracing
  3. Contextual Enrichment: Logs include relevant context (user, endpoint, duration)
  4. Log Levels: Use appropriate levels (Debug, Info, Warning, Error, Fatal)
  5. 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

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

View all →

dotnet-xunit

wshaddix/dotnet-skills

Writing xUnit tests. v3 Fact/Theory, fixtures, parallelism, IAsyncLifetime, v2 compatibility.

Backend & APIs613 installs

dotnet-testing-strategy

wshaddix/dotnet-skills

Deciding how to test .NET code. Unit vs integration vs E2E decision tree, test doubles.

Testing & QA610 installs

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.

Backend & APIs570 installs

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.

Testing & QA565 installs

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.

Security549 installs

Popular devops & cloud skills

View category →

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

OfficialDevOps & Cloud579.1K installs

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.

OfficialDevOps & Cloud569.8K installs

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.

OfficialDevOps & Cloud569K installs

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.

OfficialDevOps & Cloud568.8K installs

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.

OfficialDevOps & Cloud568.6K installs

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.

OfficialDevOps & Cloud568K installs