Add abstractions for Health Checks and a simple middleware and service to run them (#408)
This commit is contained in:
parent
a89d01b75e
commit
a30befae0f
|
|
@ -34,3 +34,6 @@ launchSettings.json
|
|||
.testPublish/
|
||||
global.json
|
||||
korebuild-lock.txt
|
||||
|
||||
# Rider and friends
|
||||
.idea/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26228.9
|
||||
VisualStudioVersion = 15.0.26923.0
|
||||
MinimumVisualStudioVersion = 15.0.26730.03
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{509A6F36-AD80-4A18-B5B1-717D38DFF29D}"
|
||||
EndProject
|
||||
|
|
@ -44,6 +44,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Diagnostics.EFCore.Function
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Diagnostics.FunctionalTests", "test\Diagnostics.FunctionalTests\Diagnostics.FunctionalTests.csproj", "{C142A666-D932-4E0D-8D18-5B08944C1077}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions", "src\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj", "{0D103C24-B9E8-468A-B113-509FCFEF7B45}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks", "src\Microsoft.Extensions.Diagnostics.HealthChecks\Microsoft.Extensions.Diagnostics.HealthChecks.csproj", "{F285F000-9342-4A01-9706-BAB2B97B4F97}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Diagnostics.HealthChecks", "src\Microsoft.AspNetCore.Diagnostics.HealthChecks\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj", "{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecksSample", "samples\HealthChecksSample\HealthChecksSample.csproj", "{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests", "test\Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests\Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj", "{E718CE19-23CC-427F-B06E-B866E9A35924}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.Tests", "test\Microsoft.Extensions.Diagnostics.HealthChecks.Tests\Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj", "{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -260,6 +272,78 @@ Global
|
|||
{C142A666-D932-4E0D-8D18-5B08944C1077}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{C142A666-D932-4E0D-8D18-5B08944C1077}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C142A666-D932-4E0D-8D18-5B08944C1077}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|x86.Build.0 = Release|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|x86.Build.0 = Release|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -283,5 +367,14 @@ Global
|
|||
{AA3661A1-CE8D-4597-ADFD-A5A30834E5D0} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
|
||||
{043C5272-D7F7-4DB1-830F-5DC93CC0878E} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
|
||||
{C142A666-D932-4E0D-8D18-5B08944C1077} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
|
||||
{0D103C24-B9E8-468A-B113-509FCFEF7B45} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D}
|
||||
{F285F000-9342-4A01-9706-BAB2B97B4F97} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D}
|
||||
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D}
|
||||
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7} = {ACAA0157-A8C4-4152-93DE-90CCDF304087}
|
||||
{E718CE19-23CC-427F-B06E-B866E9A35924} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
|
||||
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D915AA7B-4ADE-4BAC-AF65-1E800D3F3580}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Diagnostics.HealthChecks\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HealthChecksSample
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
BuildWebHost(args).Run();
|
||||
}
|
||||
|
||||
public static IWebHost BuildWebHost(string[] args) =>
|
||||
new WebHostBuilder()
|
||||
.ConfigureLogging(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HealthChecksSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHealthChecks();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
|
||||
app.Run(async (context) =>
|
||||
{
|
||||
await context.Response.WriteAsync("Hello World!");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IApplicationBuilder"/> extension methods for the <see cref="HealthCheckMiddleware"/>.
|
||||
/// </summary>
|
||||
public static class HealthCheckAppBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a middleware that provides a REST API for requesting health check status.
|
||||
/// </summary>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
|
||||
/// <param name="path">The path on which to provide the API.</param>
|
||||
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
|
||||
public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path)
|
||||
{
|
||||
app = app ?? throw new ArgumentNullException(nameof(app));
|
||||
|
||||
return app.UseMiddleware<HealthCheckMiddleware>(Options.Create(new HealthCheckOptions()
|
||||
{
|
||||
Path = path
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
||||
{
|
||||
public class HealthCheckMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly HealthCheckOptions _healthCheckOptions;
|
||||
private readonly IHealthCheckService _healthCheckService;
|
||||
|
||||
public HealthCheckMiddleware(RequestDelegate next, IOptions<HealthCheckOptions> healthCheckOptions, IHealthCheckService healthCheckService)
|
||||
{
|
||||
_next = next;
|
||||
_healthCheckOptions = healthCheckOptions.Value;
|
||||
_healthCheckService = healthCheckService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process an individual request.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (context.Request.Path == _healthCheckOptions.Path)
|
||||
{
|
||||
// Get results
|
||||
var result = await _healthCheckService.CheckHealthAsync(context.RequestAborted);
|
||||
|
||||
// Map status to response code
|
||||
switch (result.Status)
|
||||
{
|
||||
case HealthCheckStatus.Failed:
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
break;
|
||||
case HealthCheckStatus.Unhealthy:
|
||||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||
break;
|
||||
case HealthCheckStatus.Degraded:
|
||||
// Degraded doesn't mean unhealthy so we return 200, but the content will contain more details
|
||||
context.Response.StatusCode = StatusCodes.Status200OK;
|
||||
break;
|
||||
case HealthCheckStatus.Healthy:
|
||||
context.Response.StatusCode = StatusCodes.Status200OK;
|
||||
break;
|
||||
default:
|
||||
// This will only happen when we change HealthCheckStatus and we don't update this.
|
||||
Debug.Fail($"Unrecognized HealthCheckStatus value: {result.Status}");
|
||||
throw new InvalidOperationException($"Unrecognized HealthCheckStatus value: {result.Status}");
|
||||
}
|
||||
|
||||
// Render results to JSON
|
||||
var json = new JObject(
|
||||
new JProperty("status", result.Status.ToString()),
|
||||
new JProperty("results", new JObject(result.Results.Select(pair =>
|
||||
new JProperty(pair.Key, new JObject(
|
||||
new JProperty("status", pair.Value.Status.ToString()),
|
||||
new JProperty("description", pair.Value.Description),
|
||||
new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value))))))))));
|
||||
await context.Response.WriteAsync(json.ToString(Formatting.None));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains options for the <see cref="HealthCheckMiddleware"/>.
|
||||
/// </summary>
|
||||
public class HealthCheckOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path at which the Health Check results will be available.
|
||||
/// </summary>
|
||||
public PathString Path { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core middleware for returning the results of Health Checks in the application</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>diagnostics;healthchecks</PackageTags>
|
||||
<Description>ASP.NET Core middleware for returning the results of Health Checks in the application
|
||||
|
||||
</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>diagnostics;healthchecks</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Extensions.Diagnostics.HealthChecks\Microsoft.Extensions.Diagnostics.HealthChecks.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of a health check.
|
||||
/// </summary>
|
||||
public struct HealthCheckResult
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
|
||||
|
||||
private string _description;
|
||||
private IReadOnlyDictionary<string, object> _data;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="HealthCheckStatus"/> value indicating the status of the component that was checked.
|
||||
/// </summary>
|
||||
public HealthCheckStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value is expected to be 'null' if <see cref="Status"/> is <see cref="HealthCheckStatus.Healthy"/>.
|
||||
/// </remarks>
|
||||
public Exception Exception { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable description of the status of the component that was checked.
|
||||
/// </summary>
|
||||
public string Description => _description ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets additional key-value pairs describing the health of the component.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> Data => _data ?? _emptyReadOnlyDictionary;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthCheckResult"/> with the specified <paramref name="status"/>, <paramref name="exception"/>,
|
||||
/// <paramref name="description"/>, and <paramref name="data"/>.
|
||||
/// </summary>
|
||||
/// <param name="status">A <see cref="HealthCheckStatus"/> value indicating the status of the component that was checked.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public HealthCheckResult(HealthCheckStatus status, Exception exception, string description, IReadOnlyDictionary<string, object> data)
|
||||
{
|
||||
if (status == HealthCheckStatus.Unknown)
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(HealthCheckStatus.Unknown)}' is not a valid value for the 'status' parameter.", nameof(status));
|
||||
}
|
||||
|
||||
Status = status;
|
||||
Exception = exception;
|
||||
_description = description;
|
||||
_data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
|
||||
public static HealthCheckResult Unhealthy()
|
||||
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: string.Empty, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
public static HealthCheckResult Unhealthy(string description)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public static HealthCheckResult Unhealthy(string description, IReadOnlyDictionary<string, object> data)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: data);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
public static HealthCheckResult Unhealthy(Exception exception)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description: string.Empty, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
public static HealthCheckResult Unhealthy(string description, Exception exception)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public static HealthCheckResult Unhealthy(string description, Exception exception, IReadOnlyDictionary<string, object> data)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
|
||||
public static HealthCheckResult Healthy()
|
||||
=> new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: string.Empty, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
public static HealthCheckResult Healthy(string description)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public static HealthCheckResult Healthy(string description, IReadOnlyDictionary<string, object> data)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: data);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
|
||||
public static HealthCheckResult Degraded()
|
||||
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
public static HealthCheckResult Degraded(string description)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public static HealthCheckResult Degraded(string description, IReadOnlyDictionary<string, object> data)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: data);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
|
||||
public static HealthCheckResult Degraded(Exception exception)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
public static HealthCheckResult Degraded(string description, Exception exception)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public static HealthCheckResult Degraded(string description, Exception exception, IReadOnlyDictionary<string, object> data)
|
||||
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the status of a health check result.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values of this enum or ordered from least healthy to most healthy. So <see cref="HealthCheckStatus.Degraded"/> is
|
||||
/// greater than <see cref="HealthCheckStatus.Unhealthy"/> but less than <see cref="HealthCheckStatus.Healthy"/>.
|
||||
/// </remarks>
|
||||
public enum HealthCheckStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// This value should not be returned by a health check. It is used to represent an uninitialized value.
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// This value should not be returned by a health check. It is used to indicate that an unexpected exception was
|
||||
/// thrown when running the health check.
|
||||
/// </summary>
|
||||
Failed = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the health check determined that the component was unhealthy.
|
||||
/// </summary>
|
||||
Unhealthy = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the health check determined that the component was in a degraded state.
|
||||
/// </summary>
|
||||
Degraded = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the health check determined that the component was healthy.
|
||||
/// </summary>
|
||||
Healthy = 4,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a health check, which can be used to check the status of a component in the application, such as a backend service, database or some internal
|
||||
/// state.
|
||||
/// </summary>
|
||||
public interface IHealthCheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the health check, which should indicate the component being checked.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Runs the health check, returning the status of the component being checked.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
|
||||
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
|
||||
Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Abstractions for defining health checks in .NET applications
|
||||
|
||||
Commonly Used Types
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck
|
||||
</Description>
|
||||
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>diagnostics;healthchecks</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the results of multiple health checks.
|
||||
/// </summary>
|
||||
public class CompositeHealthCheckResult
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The keys in this dictionary map to the name of the health check, the values are the <see cref="HealthCheckResult"/>
|
||||
/// returned when <see cref="IHealthCheck.CheckHealthAsync(System.Threading.CancellationToken)"/> was called for that health check.
|
||||
/// </remarks>
|
||||
public IReadOnlyDictionary<string, HealthCheckResult> Results { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="HealthCheckStatus"/> representing the aggregate status of all the health checks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value is determined by taking the "worst" result of all the results. So if any result is <see cref="HealthCheckStatus.Failed"/>,
|
||||
/// this value is <see cref="HealthCheckStatus.Failed"/>. If no result is <see cref="HealthCheckStatus.Failed"/> but any result is
|
||||
/// <see cref="HealthCheckStatus.Unhealthy"/>, this value is <see cref="HealthCheckStatus.Unhealthy"/>, etc.
|
||||
/// </remarks>
|
||||
public HealthCheckStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="CompositeHealthCheckResult"/> from the specified results.
|
||||
/// </summary>
|
||||
/// <param name="results">A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.</param>
|
||||
public CompositeHealthCheckResult(IReadOnlyDictionary<string, HealthCheckResult> results)
|
||||
{
|
||||
Results = results;
|
||||
Status = CalculateAggregateStatus(results.Values);
|
||||
}
|
||||
|
||||
private HealthCheckStatus CalculateAggregateStatus(IEnumerable<HealthCheckResult> results)
|
||||
{
|
||||
// This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list
|
||||
var currentValue = HealthCheckStatus.Healthy;
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (currentValue > result.Status)
|
||||
{
|
||||
currentValue = result.Status;
|
||||
}
|
||||
|
||||
if (currentValue == HealthCheckStatus.Failed)
|
||||
{
|
||||
// Game over, man! Game over!
|
||||
// (We hit the worst possible status, so there's no need to keep iterating)
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple implementation of <see cref="IHealthCheck"/> which uses a provided delegate to
|
||||
/// implement the check.
|
||||
/// </summary>
|
||||
public class HealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly Func<CancellationToken, Task<HealthCheckResult>> _check;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the health check, which should indicate the component being checked.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of <see cref="HealthCheck"/> from the specified <paramref name="name"/> and <paramref name="check"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the health check, which should indicate the component being checked.</param>
|
||||
/// <param name="check">A delegate which provides the code to execute when the health check is run.</param>
|
||||
public HealthCheck(string name, Func<CancellationToken, Task<HealthCheckResult>> check)
|
||||
{
|
||||
Name = name;
|
||||
_check = check;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the health check, returning the status of the component being checked.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
|
||||
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
|
||||
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default) => _check(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
internal class HealthCheckLogScope : IReadOnlyList<KeyValuePair<string, object>>
|
||||
{
|
||||
public string HealthCheckName { get; }
|
||||
|
||||
int IReadOnlyCollection<KeyValuePair<string, object>>.Count { get; } = 1;
|
||||
|
||||
KeyValuePair<string, object> IReadOnlyList<KeyValuePair<string, object>>.this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index == 0)
|
||||
{
|
||||
return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="HealthCheckLogScope"/> with the provided name.
|
||||
/// </summary>
|
||||
/// <param name="healthCheckName">The name of the health check being executed.</param>
|
||||
public HealthCheckLogScope(string healthCheckName)
|
||||
{
|
||||
HealthCheckName = healthCheckName;
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
|
||||
{
|
||||
yield return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IHealthCheckService"/>.
|
||||
/// </summary>
|
||||
public class HealthCheckService : IHealthCheckService
|
||||
{
|
||||
private readonly ILogger<HealthCheckService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing all the health checks registered in the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The key maps to the <see cref="IHealthCheck.Name"/> property of the health check, and the value is the <see cref="IHealthCheck"/>
|
||||
/// instance itself.
|
||||
/// </remarks>
|
||||
public IReadOnlyDictionary<string, IHealthCheck> Checks { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a <see cref="HealthCheckService"/> from the provided collection of <see cref="IHealthCheck"/> instances.
|
||||
/// </summary>
|
||||
/// <param name="healthChecks">The <see cref="IHealthCheck"/> instances that have been registered in the application.</param>
|
||||
public HealthCheckService(IEnumerable<IHealthCheck> healthChecks) : this(healthChecks, NullLogger<HealthCheckService>.Instance) { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a <see cref="HealthCheckService"/> from the provided collection of <see cref="IHealthCheck"/> instances, and the provided logger.
|
||||
/// </summary>
|
||||
/// <param name="healthChecks">The <see cref="IHealthCheck"/> instances that have been registered in the application.</param>
|
||||
/// <param name="logger">A <see cref="ILogger{T}"/> that can be used to log events that occur during health check operations.</param>
|
||||
public HealthCheckService(IEnumerable<IHealthCheck> healthChecks, ILogger<HealthCheckService> logger)
|
||||
{
|
||||
healthChecks = healthChecks ?? throw new ArgumentNullException(nameof(logger));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
// Scan the list for duplicate names to provide a better error if there are duplicates.
|
||||
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var duplicates = new List<string>();
|
||||
foreach (var check in healthChecks)
|
||||
{
|
||||
if (!names.Add(check.Name))
|
||||
{
|
||||
duplicates.Add(check.Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicates.Count > 0)
|
||||
{
|
||||
throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicates)}", nameof(healthChecks));
|
||||
}
|
||||
|
||||
Checks = healthChecks.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
foreach (var check in Checks)
|
||||
{
|
||||
_logger.LogDebug("Health check '{healthCheckName}' has been registered", check.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs all the health checks in the application and returns the aggregated status.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
|
||||
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
|
||||
/// </returns>
|
||||
public Task<CompositeHealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default) =>
|
||||
CheckHealthAsync(Checks.Values, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the provided health checks and returns the aggregated status
|
||||
/// </summary>
|
||||
/// <param name="checks">The <see cref="IHealthCheck"/> instances to be run.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
|
||||
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
|
||||
/// </returns>
|
||||
public async Task<CompositeHealthCheckResult> CheckHealthAsync(IEnumerable<IHealthCheck> checks, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = new Dictionary<string, HealthCheckResult>(Checks.Count, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var check in checks)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
// If the health check does things like make Database queries using EF or backend HTTP calls,
|
||||
// it may be valuable to know that logs it generates are part of a health check. So we start a scope.
|
||||
using (_logger.BeginScope(new HealthCheckLogScope(check.Name)))
|
||||
{
|
||||
HealthCheckResult result;
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("Running health check: {healthCheckName}", check.Name);
|
||||
result = await check.CheckHealthAsync(cancellationToken);
|
||||
_logger.LogTrace("Health check '{healthCheckName}' completed with status '{healthCheckStatus}'", check.Name, result.Status);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// We don't log this as an error because a health check failing shouldn't bring down the active task.
|
||||
_logger.LogError(ex, "Health check '{healthCheckName}' threw an unexpected exception", check.Name);
|
||||
result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null);
|
||||
}
|
||||
|
||||
// This can only happen if the result is default(HealthCheckResult)
|
||||
if (result.Status == HealthCheckStatus.Unknown)
|
||||
{
|
||||
// This is different from the case above. We throw here because a health check is doing something specifically incorrect.
|
||||
var exception = new InvalidOperationException($"Health check '{check.Name}' returned a result with a status of Unknown");
|
||||
_logger.LogError(exception, "Health check '{healthCheckName}' returned a result with a status of Unknown", check.Name);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
results[check.Name] = result;
|
||||
}
|
||||
}
|
||||
return new CompositeHealthCheckResult(results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for registering <see cref="IHealthCheckService"/> in an <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
public static class HealthCheckServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="IHealthCheckService"/> to the container, using the provided delegate to register
|
||||
/// health checks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This operation is idempotent - multiple invocations will still only result in a single
|
||||
/// <see cref="IHealthCheckService"/> instance in the <see cref="IServiceCollection"/>. It can be invoked
|
||||
/// multiple times in order to get access to the <see cref="IHealthChecksBuilder"/> in multiple places.
|
||||
/// </remarks>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the <see cref="IHealthCheckService"/> to.</param>
|
||||
/// <returns>An instance of <see cref="IHealthChecksBuilder"/> from which health checks can be registered.</returns>
|
||||
public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
|
||||
{
|
||||
services.TryAdd(ServiceDescriptor.Singleton<IHealthCheckService, HealthCheckService>());
|
||||
return new HealthChecksBuilder(services);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
internal class HealthChecksBuilder : IHealthChecksBuilder
|
||||
{
|
||||
public IServiceCollection Services { get; }
|
||||
|
||||
public HealthChecksBuilder(IServiceCollection services)
|
||||
{
|
||||
Services = services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for registering <see cref="IHealthCheck"/> instances in an <see cref="IHealthChecksBuilder"/>.
|
||||
/// </summary>
|
||||
public static class HealthChecksBuilderAddCheckExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add the check to.</param>
|
||||
/// <param name="name">The name of the health check, which should indicate the component being checked.</param>
|
||||
/// <param name="check">A delegate which provides the code to execute when the health check is run.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func<CancellationToken, Task<HealthCheckResult>> check)
|
||||
{
|
||||
builder.Services.AddSingleton<IHealthCheck>(services => new HealthCheck(name, check));
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add the check to.</param>
|
||||
/// <param name="name">The name of the health check, which should indicate the component being checked.</param>
|
||||
/// <param name="check">A delegate which provides the code to execute when the health check is run.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func<Task<HealthCheckResult>> check) =>
|
||||
builder.AddCheck(name, _ => check());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// A service which can be used to check the status of <see cref="IHealthCheck"/> instances registered in the application.
|
||||
/// </summary>
|
||||
public interface IHealthCheckService
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing all the health checks registered in the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The key maps to the <see cref="IHealthCheck.Name"/> property of the health check, and the value is the <see cref="IHealthCheck"/>
|
||||
/// instance itself.
|
||||
/// </remarks>
|
||||
IReadOnlyDictionary<string, IHealthCheck> Checks { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Runs all the health checks in the application and returns the aggregated status.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
|
||||
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
|
||||
/// </returns>
|
||||
Task<CompositeHealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the provided health checks and returns the aggregated status
|
||||
/// </summary>
|
||||
/// <param name="checks">The <see cref="IHealthCheck"/> instances to be run.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
|
||||
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
|
||||
/// </returns>
|
||||
Task<CompositeHealthCheckResult> CheckHealthAsync(IEnumerable<IHealthCheck> checks,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder used to collect instances of <see cref="IHealthCheck"/> and register them on an <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This type wraps an <see cref="IServiceCollection"/> and provides a place for health check components to attach extension
|
||||
/// methods for registering themselves in the <see cref="IServiceCollection"/>.
|
||||
/// </remarks>
|
||||
public interface IHealthChecksBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IServiceCollection"/> into which <see cref="IHealthCheck"/> instances should be registered.
|
||||
/// </summary>
|
||||
IServiceCollection Services { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Description>Components for performing health checks in .NET applications
|
||||
|
||||
Commonly Used Types:
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
|
||||
</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>diagnostics;healthchecks</PackageTags>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
||||
{
|
||||
public class HealthCheckMiddlewareTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("/frob")]
|
||||
[InlineData("/health/")] // Match is exact, for now at least
|
||||
public async Task IgnoresRequestThatDoesNotMatchPath(string requestPath)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks();
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync(requestPath);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/health")]
|
||||
[InlineData("/Health")]
|
||||
[InlineData("/HEALTH")]
|
||||
public async Task ReturnsEmptyHealthyRequestIfNoHealthChecksRegistered(string requestPath)
|
||||
{
|
||||
var expectedJson = JsonConvert.SerializeObject(new
|
||||
{
|
||||
status = "Healthy",
|
||||
results = new { }
|
||||
}, Formatting.None);
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks();
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync(requestPath);
|
||||
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedJson, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsResultsFromHealthChecks()
|
||||
{
|
||||
var expectedJson = JsonConvert.SerializeObject(new
|
||||
{
|
||||
status = "Unhealthy",
|
||||
results = new
|
||||
{
|
||||
Foo = new
|
||||
{
|
||||
status = "Healthy",
|
||||
description = "Good to go!",
|
||||
data = new { }
|
||||
},
|
||||
Bar = new
|
||||
{
|
||||
status = "Degraded",
|
||||
description = "Feeling a bit off.",
|
||||
data = new { someUsefulAttribute = 42 }
|
||||
},
|
||||
Baz = new
|
||||
{
|
||||
status = "Unhealthy",
|
||||
description = "Not feeling good at all",
|
||||
data = new { }
|
||||
},
|
||||
Boz = new
|
||||
{
|
||||
status = "Unhealthy",
|
||||
description = string.Empty,
|
||||
data = new { }
|
||||
},
|
||||
},
|
||||
}, Formatting.None);
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("Good to go!")))
|
||||
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Feeling a bit off.", new Dictionary<string, object>()
|
||||
{
|
||||
{ "someUsefulAttribute", 42 }
|
||||
})))
|
||||
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Unhealthy("Not feeling good at all", new Exception("Bad times"))))
|
||||
.AddCheck("Boz", () => Task.FromResult(HealthCheckResult.Unhealthy(new Exception("Very bad times"))));
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health");
|
||||
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedJson, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StatusCodeIs200IfNoChecks()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks();
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StatusCodeIs200IfAllChecksHealthy()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
|
||||
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
|
||||
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StatusCodeIs200IfCheckIsDegraded()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
|
||||
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Not so great.")))
|
||||
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StatusCodeIs503IfCheckIsUnhealthy()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
|
||||
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad.")))
|
||||
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health");
|
||||
|
||||
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StatusCodeIs500IfCheckIsFailed()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
|
||||
.AddCheck("Bar", () => Task.FromResult(new HealthCheckResult(HealthCheckStatus.Failed, null, null, null)))
|
||||
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health");
|
||||
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
|
||||
<RootNamespace>Microsoft.AspNetCore.Diagnostics.HealthChecks</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Diagnostics.HealthChecks\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks.Tests
|
||||
{
|
||||
public class CompositeHealthCheckResultTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(HealthCheckStatus.Healthy)]
|
||||
[InlineData(HealthCheckStatus.Degraded)]
|
||||
[InlineData(HealthCheckStatus.Unhealthy)]
|
||||
[InlineData(HealthCheckStatus.Failed)]
|
||||
public void Status_MatchesWorstStatusInResults(HealthCheckStatus statusValue)
|
||||
{
|
||||
var result = new CompositeHealthCheckResult(new Dictionary<string, HealthCheckResult>()
|
||||
{
|
||||
{"Foo", HealthCheckResult.Healthy() },
|
||||
{"Bar", HealthCheckResult.Healthy() },
|
||||
{"Baz", new HealthCheckResult(statusValue, exception: null, description: null, data: null) },
|
||||
{"Quick", HealthCheckResult.Healthy() },
|
||||
{"Quack", HealthCheckResult.Healthy() },
|
||||
{"Quock", HealthCheckResult.Healthy() },
|
||||
});
|
||||
|
||||
Assert.Equal(statusValue, result.Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public class HealthCheckServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_BuildsDictionaryOfChecks()
|
||||
{
|
||||
// Arrange
|
||||
var fooCheck = new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
var barCheck = new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
var bazCheck = new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
var checks = new[] { fooCheck, barCheck, bazCheck };
|
||||
|
||||
// Act
|
||||
var service = new HealthCheckService(checks);
|
||||
|
||||
// Assert
|
||||
Assert.Same(fooCheck, service.Checks["Foo"]);
|
||||
Assert.Same(barCheck, service.Checks["Bar"]);
|
||||
Assert.Same(bazCheck, service.Checks["Baz"]);
|
||||
Assert.Equal(3, service.Checks.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ThrowsUsefulExceptionForDuplicateNames()
|
||||
{
|
||||
// Arrange
|
||||
var checks = new[]
|
||||
{
|
||||
new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())),
|
||||
new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())),
|
||||
new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())),
|
||||
new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())),
|
||||
new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())),
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<ArgumentException>(() => new HealthCheckService(checks));
|
||||
|
||||
// Assert
|
||||
Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAsync_RunsAllChecksAndAggregatesResultsAsync()
|
||||
{
|
||||
const string DataKey = "Foo";
|
||||
const string DataValue = "Bar";
|
||||
const string DegradedMessage = "I'm not feeling so good";
|
||||
const string UnhealthyMessage = "Halp!";
|
||||
const string HealthyMessage = "Everything is A-OK";
|
||||
var exception = new Exception("Things are pretty bad!");
|
||||
|
||||
// Arrange
|
||||
var data = new Dictionary<string, object>()
|
||||
{
|
||||
{ DataKey, DataValue }
|
||||
};
|
||||
|
||||
var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
|
||||
var degradedCheck = new HealthCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
|
||||
var unhealthyCheck = new HealthCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
|
||||
|
||||
var service = new HealthCheckService(new[]
|
||||
{
|
||||
healthyCheck,
|
||||
degradedCheck,
|
||||
unhealthyCheck,
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results.Results,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(healthyCheck.Name, actual.Key);
|
||||
Assert.Equal(HealthyMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
Assert.Collection(actual.Value.Data, item =>
|
||||
{
|
||||
Assert.Equal(DataKey, item.Key);
|
||||
Assert.Equal(DataValue, item.Value);
|
||||
});
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(degradedCheck.Name, actual.Key);
|
||||
Assert.Equal(DegradedMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
Assert.Empty(actual.Value.Data);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(unhealthyCheck.Name, actual.Key);
|
||||
Assert.Equal(UnhealthyMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthCheckStatus.Unhealthy, actual.Value.Status);
|
||||
Assert.Same(exception, actual.Value.Exception);
|
||||
Assert.Empty(actual.Value.Data);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAsync_RunsProvidedChecksAndAggregatesResultsAsync()
|
||||
{
|
||||
const string DataKey = "Foo";
|
||||
const string DataValue = "Bar";
|
||||
const string DegradedMessage = "I'm not feeling so good";
|
||||
const string UnhealthyMessage = "Halp!";
|
||||
const string HealthyMessage = "Everything is A-OK";
|
||||
var exception = new Exception("Things are pretty bad!");
|
||||
|
||||
// Arrange
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ DataKey, DataValue }
|
||||
};
|
||||
|
||||
var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
|
||||
var degradedCheck = new HealthCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
|
||||
var unhealthyCheck = new HealthCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
|
||||
|
||||
var service = new HealthCheckService(new[]
|
||||
{
|
||||
healthyCheck,
|
||||
degradedCheck,
|
||||
unhealthyCheck,
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync(new[]
|
||||
{
|
||||
service.Checks["HealthyCheck"]
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results.Results,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(healthyCheck.Name, actual.Key);
|
||||
Assert.Equal(HealthyMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
Assert.Collection(actual.Value.Data, item =>
|
||||
{
|
||||
Assert.Equal(DataKey, item.Key);
|
||||
Assert.Equal(DataValue, item.Value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckerToFailedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var thrownException = new InvalidOperationException("Whoops!");
|
||||
var faultedException = new InvalidOperationException("Ohnoes!");
|
||||
var service = new HealthCheckService(new[]
|
||||
{
|
||||
new HealthCheck("Throws", ct => throw thrownException),
|
||||
new HealthCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException)),
|
||||
new HealthCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())),
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results.Results,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Throws", actual.Key);
|
||||
Assert.Equal(thrownException.Message, actual.Value.Description);
|
||||
Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status);
|
||||
Assert.Same(thrownException, actual.Value.Exception);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Faults", actual.Key);
|
||||
Assert.Equal(faultedException.Message, actual.Value.Description);
|
||||
Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status);
|
||||
Assert.Same(faultedException, actual.Value.Exception);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Succeeds", actual.Key);
|
||||
Assert.Empty(actual.Value.Description);
|
||||
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_SetsUpALoggerScopeForEachCheck()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
var check = new HealthCheck("TestScope", cancellationToken =>
|
||||
{
|
||||
Assert.Collection(sink.Scopes,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(actual.LoggerName, typeof(HealthCheckService).FullName);
|
||||
Assert.Collection((IEnumerable<KeyValuePair<string, object>>)actual.Scope,
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("HealthCheckName", item.Key);
|
||||
Assert.Equal("TestScope", item.Value);
|
||||
});
|
||||
});
|
||||
return Task.FromResult(HealthCheckResult.Healthy());
|
||||
});
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var service = new HealthCheckService(new[] { check }, loggerFactory.CreateLogger<HealthCheckService>());
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results.Results, actual =>
|
||||
{
|
||||
Assert.Equal("TestScope", actual.Key);
|
||||
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult()
|
||||
{
|
||||
// Arrange
|
||||
var service = new HealthCheckService(new[]
|
||||
{
|
||||
new HealthCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))),
|
||||
});
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.CheckHealthAsync());
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public class HealthChecksBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
|
||||
// Act
|
||||
var healthCheckService = services.BuildServiceProvider().GetRequiredService<IHealthCheckService>();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(healthCheckService.Checks,
|
||||
actual => Assert.Equal("Foo", actual.Key),
|
||||
actual => Assert.Equal("Bar", actual.Key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
|
||||
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Extensions.Diagnostics.HealthChecks\Microsoft.Extensions.Diagnostics.HealthChecks.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public class ServiceCollectionExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Act
|
||||
services.AddHealthChecks();
|
||||
services.AddHealthChecks();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(services,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
|
||||
Assert.Equal(typeof(IHealthCheckService), actual.ServiceType);
|
||||
Assert.Equal(typeof(HealthCheckService), actual.ImplementationType);
|
||||
Assert.Null(actual.ImplementationInstance);
|
||||
Assert.Null(actual.ImplementationFactory);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue