Skip to content
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

Add health checks module #77

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions complete/Api/Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.StackExchange.Redis.OutputCaching" Version="9.1.0" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ServiceDefaults\ServiceDefaults.csproj" />
Expand Down
1 change: 0 additions & 1 deletion complete/Api/Data/NwsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Api.Data;
using Api.Diagnostics;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;

namespace Api
{
Expand Down
17 changes: 13 additions & 4 deletions complete/Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using Api.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -21,6 +18,11 @@
builder.Services.AddOpenTelemetry()
.WithMetrics(m => m.AddMeter("NwsManagerMetrics"));

// Add health check services for redis cache and external service
builder.Services.AddHealthChecks()
.AddRedis("localhost:6379", name: "redis")
.AddUrlGroup(new Uri("https://api.weather.gov/"), name: "weatherApi");

var app = builder.Build();

app.MapDefaultEndpoints();
Expand All @@ -30,4 +32,11 @@
// Map the endpoints for the API
app.MapApiEndpoints();

// Add health check endpoints for /health and /alive
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});

app.Run();
34 changes: 18 additions & 16 deletions complete/MyWeatherHub/MyWeatherHub.csproj
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="9.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ServiceDefaults\ServiceDefaults.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.1.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Npgsql" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="9.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ServiceDefaults\ServiceDefaults.csproj" />
</ItemGroup>
</Project>
11 changes: 10 additions & 1 deletion complete/MyWeatherHub/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using MyWeatherHub;
using MyWeatherHub.Components;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Npgsql.EntityFrameworkCore.PostgreSQL;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -17,6 +19,13 @@

builder.AddNpgsqlDbContext<MyWeatherContext>(connectionName: "weatherdb");

// Add health check services for database
builder.Services.AddHealthChecks()
.AddNpgSql(
builder.Configuration.GetConnectionString("weatherdb"),

Check warning on line 25 in complete/MyWeatherHub/Program.cs

View workflow job for this annotation

GitHub Actions / Build Projects (complete)

Possible null reference argument for parameter 'connectionString' in 'IHealthChecksBuilder NpgSqlHealthCheckBuilderExtensions.AddNpgSql(IHealthChecksBuilder builder, string connectionString, string healthQuery = "SELECT 1;", Action<NpgsqlConnection>? configure = null, string? name = null, HealthStatus? failureStatus = null, IEnumerable<string>? tags = null, TimeSpan? timeout = null)'.

Check warning on line 25 in complete/MyWeatherHub/Program.cs

View workflow job for this annotation

GitHub Actions / Build Projects (complete)

Possible null reference argument for parameter 'connectionString' in 'IHealthChecksBuilder NpgSqlHealthCheckBuilderExtensions.AddNpgSql(IHealthChecksBuilder builder, string connectionString, string healthQuery = "SELECT 1;", Action<NpgsqlConnection>? configure = null, string? name = null, HealthStatus? failureStatus = null, IEnumerable<string>? tags = null, TimeSpan? timeout = null)'.
name: "database"
);

var app = builder.Build();

app.MapDefaultEndpoints();
Expand All @@ -28,7 +37,7 @@
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
else
else
{
using (var scope = app.Services.CreateScope())
{
Expand Down
47 changes: 47 additions & 0 deletions complete/ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ServiceDiscovery;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using System.Text.Json;

namespace Microsoft.Extensions.Hosting;

Expand Down Expand Up @@ -124,6 +126,51 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app)
Predicate = r => r.Tags.Contains("live")
});
}
else
{
// Considerations for non-development environments
app.MapHealthChecks("/health", new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
exception = e.Value.Exception?.Message,
duration = e.Value.Duration.ToString()
})
});
await context.Response.WriteAsync(result);
}
});

app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live"),
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
exception = e.Value.Exception?.Message,
duration = e.Value.Duration.ToString()
})
});
await context.Response.WriteAsync(result);
}
});
}

return app;
}
Expand Down
158 changes: 158 additions & 0 deletions workshop/10-health-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Health Checks

## Introduction

In this module, we will add health checks to our application. Health checks are used to determine the health of an application and its dependencies. They can be used to monitor the health of the application and its dependencies, and to determine if the application is ready to accept traffic.

## Adding Health Checks

### Step 1: Add Health Check Packages

First, we need to add the necessary packages to our projects. For the API project, open the `complete/Api/Api.csproj` file and add the following package references:

```xml
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="9.0.0" />
</ItemGroup>
```

For the MyWeatherHub project, open the `complete/MyWeatherHub/MyWeatherHub.csproj` file and add the following package references:

```xml
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Npgsql" Version="9.0.0" />
</ItemGroup>
```

### Step 2: Add Health Check Services

Next, we need to add the health check services to our applications.

For the API project, open the `complete/Api/Program.cs` file and add the following code:

```csharp
// Add health check services for redis cache and external service
builder.Services.AddHealthChecks()
.AddRedis("localhost:6379", name: "redis")
.AddUrlGroup(new Uri("https://api.weather.gov/"), name: "weatherApi");
```

For the MyWeatherHub project, open the `complete/MyWeatherHub/Program.cs` file and add the following code:

```csharp
// Add health check services for database
builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("weatherdb"), name: "postgresql");
```

### Step 3: Map Health Check Endpoints

Now, we need to add the health check endpoints to our applications.

The ServiceDefaults project already maps default health check endpoints using the `MapDefaultEndpoints()` extension method. This method is provided as part of the .NET Aspire service defaults and maps the standard `/health` and `/alive` endpoints.

To use these endpoints, simply call the method in your application's `Program.cs` file:

```csharp
app.MapDefaultEndpoints();
```

If you need to add additional health check endpoints, you can add them like this in the Api's `Program.cs` file:

```csharp
// Add health check endpoints for /health and /alive
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
```

### Step 4: Understand the Default Health Check Implementation

The default implementation in ServiceDefaults/Extensions.cs already includes smart behavior for handling health checks in different environments:

```csharp
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment())
{
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks("/health");

// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
else
{
// Considerations for non-development environments
app.MapHealthChecks("/health", new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
exception = e.Value.Exception?.Message,
duration = e.Value.Duration.ToString()
})
});
await context.Response.WriteAsync(result);
}
});

app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live"),
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
exception = e.Value.Exception?.Message,
duration = e.Value.Duration.ToString()
})
});
await context.Response.WriteAsync(result);
}
});
}

return app;
}
```

The implementation includes different approaches for development and production environments:

- In development: Simple endpoints for quick diagnostics
- In production: More detailed JSON output with additional security considerations

## References

For more information on health checks, see the following documentation:

- [Health Checks in .NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/health-checks)
- [Health Checks in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks)

## HealthChecksUI Sample

You can also add a UI for your health checks using the [HealthChecksUI sample](https://github.com/dotnet/aspire-samples/tree/main/samples/HealthChecksUI). This sample shows how to add the UI as a container and provides a link to the sample for those who are interested.