-
Notifications
You must be signed in to change notification settings - Fork 87
Add implementing-health-checks skill #263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
20c224b
43359f7
9e40c92
92ec231
8108581
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "name": "dotnet-aspnet", | ||
| "version": "0.1.0", | ||
| "description": "Skills for ASP.NET Core web development: health checks, middleware, authentication, and API patterns.", | ||
| "skills": "./skills/" | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,175 @@ | ||||||||||||||||||
| --- | ||||||||||||||||||
| name: implementing-health-checks | ||||||||||||||||||
| description: Implement ASP.NET Core health checks with liveness, readiness, and startup probes for Kubernetes and load balancer integration. Use when configuring health endpoints, monitoring dependencies, or setting up container orchestration probes. | ||||||||||||||||||
| --- | ||||||||||||||||||
|
|
||||||||||||||||||
| # Implementing Health Checks | ||||||||||||||||||
|
|
||||||||||||||||||
| ## When to Use | ||||||||||||||||||
|
|
||||||||||||||||||
| - Adding health check endpoints to an ASP.NET Core app | ||||||||||||||||||
| - Configuring Kubernetes liveness, readiness, and startup probes | ||||||||||||||||||
| - Monitoring database, cache, or external service availability | ||||||||||||||||||
| - Load balancer health endpoint configuration | ||||||||||||||||||
|
|
||||||||||||||||||
| ## When Not to Use | ||||||||||||||||||
|
|
||||||||||||||||||
| - The app is not ASP.NET Core | ||||||||||||||||||
| - The user wants application performance monitoring (use OpenTelemetry instead) | ||||||||||||||||||
| - The user needs business-level monitoring (use custom metrics) | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Inputs | ||||||||||||||||||
|
|
||||||||||||||||||
| | Input | Required | Description | | ||||||||||||||||||
| |-------|----------|-------------| | ||||||||||||||||||
| | ASP.NET Core project | Yes | The project to add health checks to | | ||||||||||||||||||
| | Dependencies to monitor | No | Database, Redis, message queue, etc. | | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Workflow | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Step 1: Add the health checks packages | ||||||||||||||||||
|
|
||||||||||||||||||
| ```bash | ||||||||||||||||||
| dotnet add package AspNetCore.HealthChecks.SqlServer # for SQL Server | ||||||||||||||||||
| dotnet add package AspNetCore.HealthChecks.Redis # for Redis | ||||||||||||||||||
| dotnet add package AspNetCore.HealthChecks.Uris # for HTTP dependencies | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| > `Microsoft.Extensions.Diagnostics.HealthChecks` is already included in the ASP.NET Core framework — no explicit install needed. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Step 2: Register health checks with SEPARATE liveness and readiness | ||||||||||||||||||
|
|
||||||||||||||||||
| **Critical distinction most implementations get wrong:** | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Liveness** = "Is the process alive?" — Only checks the process isn't deadlocked. Failure → Kubernetes RESTARTS the pod. | ||||||||||||||||||
| - **Readiness** = "Can the process serve traffic?" — Checks dependencies. Failure → Kubernetes STOPS SENDING traffic (but doesn't restart). | ||||||||||||||||||
| - **Startup** = "Has the initial startup completed?" — One-time check. Failure during grace period is expected. | ||||||||||||||||||
|
|
||||||||||||||||||
| ```csharp | ||||||||||||||||||
| builder.Services.AddHealthChecks() | ||||||||||||||||||
| // Liveness checks: ONLY check the process itself, NEVER external dependencies | ||||||||||||||||||
| .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" }) | ||||||||||||||||||
|
|
||||||||||||||||||
| // Readiness checks: check external dependencies | ||||||||||||||||||
| .AddSqlServer( | ||||||||||||||||||
| connectionString: builder.Configuration.GetConnectionString("Default")!, | ||||||||||||||||||
| name: "database", | ||||||||||||||||||
| tags: new[] { "ready" }, | ||||||||||||||||||
| timeout: TimeSpan.FromSeconds(5)) | ||||||||||||||||||
| .AddRedis( | ||||||||||||||||||
| redisConnectionString: builder.Configuration.GetConnectionString("Redis")!, | ||||||||||||||||||
| name: "redis", | ||||||||||||||||||
| tags: new[] { "ready" }, | ||||||||||||||||||
| timeout: TimeSpan.FromSeconds(5)) | ||||||||||||||||||
| .AddUrlGroup( | ||||||||||||||||||
| new Uri("https://api.external-service.com/health"), | ||||||||||||||||||
| name: "external-api", | ||||||||||||||||||
| tags: new[] { "ready" }, | ||||||||||||||||||
| timeout: TimeSpan.FromSeconds(5)); | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Step 3: Map separate health endpoints | ||||||||||||||||||
|
|
||||||||||||||||||
| ```csharp | ||||||||||||||||||
| // Liveness: Kubernetes livenessProbe hits this | ||||||||||||||||||
| app.MapHealthChecks("/healthz/live", new HealthCheckOptions | ||||||||||||||||||
| { | ||||||||||||||||||
| Predicate = check => check.Tags.Contains("live"), | ||||||||||||||||||
| ResponseWriter = WriteMinimalResponse | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Readiness: Kubernetes readinessProbe hits this | ||||||||||||||||||
| // Use minimal response by default; only expose detailed responses on protected/internal endpoints | ||||||||||||||||||
| app.MapHealthChecks("/healthz/ready", new HealthCheckOptions | ||||||||||||||||||
| { | ||||||||||||||||||
| Predicate = check => check.Tags.Contains("ready"), | ||||||||||||||||||
| ResponseWriter = WriteMinimalResponse | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Startup: Kubernetes startupProbe hits this — check process health only (same as liveness) | ||||||||||||||||||
| app.MapHealthChecks("/healthz/startup", new HealthCheckOptions | ||||||||||||||||||
| { | ||||||||||||||||||
| Predicate = check => check.Tags.Contains("live"), | ||||||||||||||||||
| ResponseWriter = WriteMinimalResponse | ||||||||||||||||||
| }); | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Step 4: Write response formatters | ||||||||||||||||||
|
|
||||||||||||||||||
| ```csharp | ||||||||||||||||||
| static Task WriteMinimalResponse(HttpContext context, HealthReport report) | ||||||||||||||||||
| { | ||||||||||||||||||
| context.Response.ContentType = "application/json"; | ||||||||||||||||||
| var result = new { status = report.Status.ToString() }; | ||||||||||||||||||
| return context.Response.WriteAsJsonAsync(result); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| static Task WriteDetailedResponse(HttpContext context, HealthReport report) | ||||||||||||||||||
| { | ||||||||||||||||||
| context.Response.ContentType = "application/json"; | ||||||||||||||||||
| var result = new | ||||||||||||||||||
| { | ||||||||||||||||||
| status = report.Status.ToString(), | ||||||||||||||||||
| checks = report.Entries.Select(e => new | ||||||||||||||||||
| { | ||||||||||||||||||
| name = e.Key, | ||||||||||||||||||
| status = e.Value.Status.ToString(), | ||||||||||||||||||
| description = e.Value.Description, | ||||||||||||||||||
| duration = e.Value.Duration.TotalMilliseconds | ||||||||||||||||||
| }) | ||||||||||||||||||
| }; | ||||||||||||||||||
| return context.Response.WriteAsJsonAsync(result); | ||||||||||||||||||
| } | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Step 5: Configure Kubernetes probes | ||||||||||||||||||
|
|
||||||||||||||||||
| ```yaml | ||||||||||||||||||
| # In the Kubernetes deployment spec: | ||||||||||||||||||
| containers: | ||||||||||||||||||
| - name: myapp | ||||||||||||||||||
| livenessProbe: | ||||||||||||||||||
| httpGet: | ||||||||||||||||||
| path: /healthz/live | ||||||||||||||||||
| port: 8080 | ||||||||||||||||||
| initialDelaySeconds: 0 # Start checking immediately | ||||||||||||||||||
| periodSeconds: 10 | ||||||||||||||||||
| failureThreshold: 3 # Restart after 3 failures | ||||||||||||||||||
| readinessProbe: | ||||||||||||||||||
| httpGet: | ||||||||||||||||||
| path: /healthz/ready | ||||||||||||||||||
| port: 8080 | ||||||||||||||||||
| initialDelaySeconds: 5 | ||||||||||||||||||
| periodSeconds: 10 | ||||||||||||||||||
| failureThreshold: 3 # Stop traffic after 3 failures | ||||||||||||||||||
| startupProbe: | ||||||||||||||||||
| httpGet: | ||||||||||||||||||
| path: /healthz/startup | ||||||||||||||||||
| port: 8080 | ||||||||||||||||||
| initialDelaySeconds: 0 | ||||||||||||||||||
| periodSeconds: 5 | ||||||||||||||||||
| failureThreshold: 30 # Allow up to 150s for startup | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Step 6: Add health check UI (optional) | ||||||||||||||||||
|
|
||||||||||||||||||
| ```bash | ||||||||||||||||||
| dotnet add package AspNetCore.HealthChecks.UI | ||||||||||||||||||
| dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| ```csharp | ||||||||||||||||||
| builder.Services.AddHealthChecksUI().AddInMemoryStorage(); | ||||||||||||||||||
| app.MapHealthChecksUI(); | ||||||||||||||||||
|
||||||||||||||||||
| app.MapHealthChecksUI(); | |
| // Expose UI on a custom path and require authorization to avoid leaking details | |
| app.MapHealthChecksUI(options => | |
| { | |
| options.UIPath = "/health-ui"; // UI endpoint | |
| options.ApiPath = "/health-ui-api"; // backend API | |
| }).RequireAuthorization(); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,70 @@ | ||||||
| scenarios: | ||||||
| - name: "Add health checks with Kubernetes probes" | ||||||
| prompt: "I need to add health checks to my ASP.NET Core API for Kubernetes deployment. It should check the database and Redis connections and have separate liveness and readiness endpoints." | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prompt is overfitting for the name of skill. I dont imagine myself writing "i need to add health checks for MY ASPNETCORE API", I will simply ask agent "need to add health checks dependant on db" for example. |
||||||
| assertions: | ||||||
| - type: "output_matches" | ||||||
| pattern: "(AddHealthChecks|MapHealthChecks)" | ||||||
| - type: "output_matches" | ||||||
| pattern: "(liveness|readiness|live|ready)" | ||||||
| - type: "output_matches" | ||||||
| pattern: "(healthz|health)" | ||||||
| rubric: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Skill clearly relies on installing specific NuGet package(s), and rubric does not validate that at all. Are those packages really required, or agent can generate code building GET endpoint handlers from scratch? |
||||||
| - "Separated liveness and readiness health checks using tags" | ||||||
| - "Liveness probe does NOT check external dependencies (database, Redis) — only process health" | ||||||
| - "Readiness probe checks database and Redis connectivity" | ||||||
| - "Mapped separate endpoints for liveness and readiness (e.g., /healthz/live and /healthz/ready)" | ||||||
| - "Explained WHY liveness should not check dependencies (restart cascading)" | ||||||
| - "Provided Kubernetes probe YAML configuration or explained probe settings" | ||||||
| timeout: 120 | ||||||
|
|
||||||
| - name: "Health check skill should not activate for monitoring setup" | ||||||
| prompt: "I want to add application performance monitoring with OpenTelemetry to track request latency and error rates." | ||||||
| assertions: | ||||||
| - type: "output_not_matches" | ||||||
| pattern: "\\b(AddHealthChecks|MapHealthChecks)\\s*\\(" | ||||||
| - type: "output_matches" | ||||||
| pattern: "(OpenTelemetry|AddOpenTelemetry|tracing|metrics|exporter)" | ||||||
| rubric: | ||||||
| - "Did NOT suggest health checks for an APM/observability request" | ||||||
| - "Focused on OpenTelemetry setup (tracing, metrics, exporters)" | ||||||
| timeout: 60 | ||||||
|
|
||||||
| - name: "Separate startup probe from liveness" | ||||||
| prompt: "My ASP.NET Core app takes about 60 seconds to warm up and Kubernetes keeps restarting it. I already have a liveness probe at /health. How do I fix this?" | ||||||
| assertions: | ||||||
| - type: "output_matches" | ||||||
| pattern: "(startupProbe|startup)" | ||||||
| - type: "output_matches" | ||||||
| pattern: "(initialDelaySeconds|failureThreshold|periodSeconds)" | ||||||
| rubric: | ||||||
| - "Recommended adding a startup probe to handle slow cold starts" | ||||||
| - "Explained that without a startup probe, the liveness probe kills pods during startup" | ||||||
| - "Suggested appropriate failureThreshold and periodSeconds to cover the 60s warmup" | ||||||
| - "Startup probe should check process health only, not external dependencies" | ||||||
| timeout: 120 | ||||||
|
|
||||||
| - name: "Health checks with load balancer instead of Kubernetes" | ||||||
| prompt: "I'm deploying my ASP.NET Core app behind an Azure Application Gateway. I need a health endpoint that the gateway can hit to determine if the instance is healthy." | ||||||
| assertions: | ||||||
| - type: "output_matches" | ||||||
| pattern: "(MapHealthChecks|AddHealthChecks)" | ||||||
| - type: "output_matches" | ||||||
| pattern: "(health|healthz)" | ||||||
| rubric: | ||||||
| - "Suggested a health check endpoint that a load balancer can use" | ||||||
| - "Checked external dependencies (database, cache) that affect ability to serve traffic" | ||||||
| - "Did not over-engineer with separate liveness/readiness/startup unless context warranted it" | ||||||
| timeout: 120 | ||||||
|
|
||||||
| - name: "Do not confuse health checks with logging middleware" | ||||||
| prompt: "I want to add request logging middleware to my ASP.NET Core API so I can see all incoming HTTP requests and their response times." | ||||||
| assertions: | ||||||
| - type: "output_not_matches" | ||||||
| pattern: "\\b(AddHealthChecks|MapHealthChecks)\\s*\\(" | ||||||
| - type: "output_matches" | ||||||
| pattern: "(UseHttpLogging|UseSerilog|logging|middleware)" | ||||||
|
||||||
| pattern: "(UseHttpLogging|UseSerilog|logging|middleware)" | |
| pattern: "(UseHttpLogging|UseSerilog|request logging|http logging|HTTP logging|logging middleware)" |
Copilot
AI
Mar 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This scenario defines timeout twice. YAML duplicate keys are easy to miss and the last value will win, which can hide edits or cause confusing behavior during evaluation. Remove the duplicate timeout entry so the scenario has a single, unambiguous timeout value.
| timeout: 60 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are packages for SqlServer, Redis and Uris required for the skill about health-checks
for Kubernetes and load balancer integration? Skill has to be very specific, and it feels like this one supports multiple different resources not really following the single idea.