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

Application Status health check when performed graceful shutdown #1365

Merged
merged 11 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
14 changes: 14 additions & 0 deletions AspNetCore.Diagnostics.HealthChecks.sln
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecks.Aws.Sns", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecks.Aws.Sns.Tests", "test\HealthChecks.Aws.Sns.Tests\HealthChecks.Aws.Sns.Tests.csproj", "{CB1A7B68-E24A-4729-9401-606F3A914586}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.ApplicationStatus", "src\HealthChecks.ApplicationStatus\HealthChecks.ApplicationStatus.csproj", "{88739521-A9BC-49E1-BB98-E9D63109C231}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecks.ApplicationStatus.Tests", "test\HealthChecks.ApplicationStatus.Tests\HealthChecks.ApplicationStatus.Tests.csproj", "{403776CB-7229-4063-85C7-C34428BDAA8F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -825,6 +829,14 @@ Global
{CB1A7B68-E24A-4729-9401-606F3A914586}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB1A7B68-E24A-4729-9401-606F3A914586}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB1A7B68-E24A-4729-9401-606F3A914586}.Release|Any CPU.Build.0 = Release|Any CPU
{88739521-A9BC-49E1-BB98-E9D63109C231}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88739521-A9BC-49E1-BB98-E9D63109C231}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88739521-A9BC-49E1-BB98-E9D63109C231}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88739521-A9BC-49E1-BB98-E9D63109C231}.Release|Any CPU.Build.0 = Release|Any CPU
{403776CB-7229-4063-85C7-C34428BDAA8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{403776CB-7229-4063-85C7-C34428BDAA8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{403776CB-7229-4063-85C7-C34428BDAA8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{403776CB-7229-4063-85C7-C34428BDAA8F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -939,6 +951,8 @@ Global
{EFA76A2C-CA0E-42BC-8215-AEEB16414947} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE}
{AE41DB38-93BC-48A7-8841-163E5E13CE8D} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4}
{CB1A7B68-E24A-4729-9401-606F3A914586} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE}
{88739521-A9BC-49E1-BB98-E9D63109C231} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4}
{403776CB-7229-4063-85C7-C34428BDAA8F} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2B8C62A1-11B6-469F-874C-A02443256568}
Expand Down
1 change: 1 addition & 0 deletions build/dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
</PropertyGroup>

<PropertyGroup Label="Health Checks Package Versions">
<HealthCheckApplicationStatus>6.0.0</HealthCheckApplicationStatus>
<HealthCheckArangoDb>6.0.2</HealthCheckArangoDb>
<HealthCheckAWSS3>6.0.2</HealthCheckAWSS3>
<HealthCheckAWSSecretsManager>6.0.0</HealthCheckAWSSecretsManager>
Expand Down
30 changes: 30 additions & 0 deletions src/HealthChecks.ApplicationStatus/ApplicationStatusHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;

namespace HealthChecks.ApplicationStatus;

public class ApplicationStatusHealthCheck : IHealthCheck, IDisposable
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly IHostApplicationLifetime _lifetime;
private readonly CancellationTokenRegistration _ctRegistration;
private bool IsApplicationRunning { get; set; } = true;
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
public ApplicationStatusHealthCheck(IHostApplicationLifetime lifetime)
{
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(IHostApplicationLifetime));
_ctRegistration = _lifetime.ApplicationStopping.Register(OnStopping);
}

private void OnStopping()
{
IsApplicationRunning = false;
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
}

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

return Task.FromResult(IsApplicationRunning ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy());
}

public void Dispose() => _ctRegistration.Dispose();
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;

namespace HealthChecks.ApplicationStatus.DependencyInjection;
sungam3r marked this conversation as resolved.
Show resolved Hide resolved

public static class ApplicationStatusHealthCheckBuilderExtensions
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
{
private const string NAME = "applicationstatus";

/// <summary>
/// Add a health check for Application Status.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="lifetime">Represents an object for intercepting application lifetime events.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'applicationstatus' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddApplicationStatus(
this IHealthChecksBuilder builder,
IHostApplicationLifetime lifetime,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
if (lifetime == null)
{
throw new ArgumentNullException(nameof(IHostApplicationLifetime));
}

builder.Services
.TryAddSingleton(sp => new ApplicationStatusHealthCheck(lifetime));
sungam3r marked this conversation as resolved.
Show resolved Hide resolved

OlegOLK marked this conversation as resolved.
Show resolved Hide resolved
return AddApplicationStatusCheck(builder, name, failureStatus, tags, timeout);
}

/// <summary>
/// Add a health check for Application Status.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'applicationstatus' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddApplicationStatus(
this IHealthChecksBuilder builder,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
builder.Services
.TryAddSingleton(sp => new ApplicationStatusHealthCheck(sp.GetRequiredService<IHostApplicationLifetime>()));

return AddApplicationStatusCheck(builder, name, failureStatus, tags, timeout);
}

/// <summary>
/// Add a health check for Application Status from DI container.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'applicationstatus' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
private static IHealthChecksBuilder AddApplicationStatusCheck(
this IHealthChecksBuilder builder,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp => sp.GetRequiredService<ApplicationStatusHealthCheck>(),
failureStatus,
tags,
timeout));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetFrameworkVersion);$(NetStandardVersion)</TargetFrameworks>
<PackageTags>$(PackageTags);ApplicationStatus</PackageTags>
<Description>HealthChecks.ApplicationStatus is the health check package for detection graceful shutdown.</Description>
<Version>$(HealthCheckApplicationStatus)</Version>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
</ItemGroup>

</Project>

25 changes: 25 additions & 0 deletions src/HealthChecks.ApplicationStatus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ArangoDb Health Check

This health check verifies that application is up and runnning based on `IHostApplicationLifetime`.
If application received stop signal, eg: SIGTERM in docker container - then health status will be unhealthy and
application won't be able to receive new requests.

## Example Usage

With all of the following examples, you can additionally add the following parameters:

- `name`: The health check name. Default if not specified is `applicationstatus`.
- `failureStatus`: The `HealthStatus` that should be reported when the health check fails. Default is `HealthStatus.Unhealthy`.
- `tags`: A list of tags that can be used to filter sets of health checks.
- `timeout`: A `System.TimeSpan` representing the timeout of the check.

### Basic

```csharp
public void ConfigureServices(IServiceCollection services, IHost)
{
services
.AddHealthChecks()
.AddCheck<ApplicationStatusHealthCheck>();
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using HealthChecks.ApplicationStatus;
using HealthChecks.ApplicationStatus.DependencyInjection;
using HealthChecks.ApplicationStatus.Tests;
using Microsoft.Extensions.Hosting;

namespace HealthChecks.ArangoDb.Tests.DependencyInjection;

public class applicationstatus_registration_should
{
[Fact]
public void add_health_check_no_direct_service_argument_when_properly_configured()
{
var services = new ServiceCollection();
services
.AddSingleton<IHostApplicationLifetime, TestHostApplicationLifeTime>()
.AddHealthChecks()
.AddApplicationStatus();

using var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();

var registration = options.Value.Registrations.First();
var check = registration.Factory(serviceProvider);

registration.Name.Should().Be("applicationstatus");
check.GetType().Should().Be(typeof(ApplicationStatusHealthCheck));
}

[Fact]
public void add_named_health_check_no_direct_service_argument_when_properly_configured()
{
var services = new ServiceCollection();
services
.AddSingleton<IHostApplicationLifetime, TestHostApplicationLifeTime>()
.AddHealthChecks()
.AddApplicationStatus(name: "custom-status");

using var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();

var registration = options.Value.Registrations.First();
var check = registration.Factory(serviceProvider);

registration.Name.Should().Be("custom-status");
check.GetType().Should().Be(typeof(ApplicationStatusHealthCheck));
}

[Fact]
public void add_health_check_when_properly_configured()
{
var services = new ServiceCollection();
services.AddHealthChecks()
.AddApplicationStatus(new TestHostApplicationLifeTime());

using var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();

var registration = options.Value.Registrations.First();
var check = registration.Factory(serviceProvider);

registration.Name.Should().Be("applicationstatus");
check.GetType().Should().Be(typeof(ApplicationStatusHealthCheck));
}

[Fact]
public void add_named_health_check_when_properly_configured()
{
var services = new ServiceCollection();
services.AddHealthChecks()
.AddApplicationStatus(new TestHostApplicationLifeTime(), name: "custom-status");

using var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();

var registration = options.Value.Registrations.First();
var check = registration.Factory(serviceProvider);

registration.Name.Should().Be("custom-status");
check.GetType().Should().Be(typeof(ApplicationStatusHealthCheck));
}

[Fact]
public void add_named_health_check_several_times_only_one_health_check_in_ioc()
{
var services = new ServiceCollection();
services.AddHealthChecks()
.AddApplicationStatus(new TestHostApplicationLifeTime(), name: "custom-status-0")
.AddApplicationStatus(new TestHostApplicationLifeTime(), name: "custom-status-1");

using var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();

options.Value.Registrations.Count.Should().Be(2);
var registration = services
.Where(x => x.ServiceType.Equals(typeof(ApplicationStatusHealthCheck)));

registration.Count().ShouldBe(1);
registration.First().Lifetime.ShouldBe(ServiceLifetime.Singleton);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace HealthChecks.ApplicationStatus.Tests.Functional;

public class applicationstatus_healthcheck_should
{
[Fact]
public async Task be_healthy_if_application_is_not_stopped()
{
var sut = new ApplicationStatusHealthCheck(new TestHostApplicationLifeTime());
var result = await sut.CheckHealthAsync(new HealthCheckContext()).ConfigureAwait(false);
result.Should().Be(HealthCheckResult.Healthy());
}

[Fact]
public async Task be_unhealthy_if_application_is_stopped()
{
var lifetime = new TestHostApplicationLifeTime();
var sut = new ApplicationStatusHealthCheck(lifetime);
lifetime.StopApplication();

var result = await sut.CheckHealthAsync(new HealthCheckContext()).ConfigureAwait(false);

result.Should().Be(HealthCheckResult.Unhealthy());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(NetFrameworkVersion)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\HealthChecks.ApplicationStatus\HealthChecks.ApplicationStatus.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace HealthChecks.ApplicationStatus
{
public class ApplicationStatusHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck, System.IDisposable
{
public ApplicationStatusHealthCheck(Microsoft.Extensions.Hosting.IHostApplicationLifetime lifetime) { }
public System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default) { }
public void Dispose() { }
}
}
namespace HealthChecks.ApplicationStatus.DependencyInjection
{
public static class ApplicationStatusHealthCheckBuilderExtensions
{
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddApplicationStatus(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddApplicationStatus(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, Microsoft.Extensions.Hosting.IHostApplicationLifetime lifetime, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
}
}
Loading