Add filtering to Health Checks middleware
This allows each middleware to be configured with a specific set of checks (by name). See the comments in the sample for how this is frequently used. This is also addresses aspnet/Home#2575 - or at least the part that we plan to do. We think that any sort of built-in system of metadata or tags is vast overkill, and doesn't really align with the primary usage of health checks. We're providing building blocks, and these can be put together to build more complicated things like what's described in aspnet/Home#2575.
This commit is contained in:
parent
47f427d5ac
commit
64124e9c85
|
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace HealthChecksSample
|
||||
{
|
||||
// Pass in `--scenario liveness` at the command line to run this sample.
|
||||
public class LivenessProbeStartup
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// Registers required services for health checks
|
||||
services
|
||||
.AddHealthChecks()
|
||||
.AddCheck("identity", () => Task.FromResult(HealthCheckResult.Healthy()))
|
||||
.AddCheck(new SlowDependencyHealthCheck());
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
{
|
||||
// This will register the health checks middleware twice:
|
||||
// - at /health/ready for 'readiness'
|
||||
// - at /health/live for 'liveness'
|
||||
//
|
||||
// Using a separate liveness and readiness check is useful in an environment like Kubernetes
|
||||
// when an application needs to do significant work before accepting requests. Using separate
|
||||
// checks allows the orchestrator to distinguish whether the application is functioning but
|
||||
// not yet ready or if the application has failed to start.
|
||||
//
|
||||
// For instance the liveness check will do a quick set of checks to determine if the process
|
||||
// is functioning correctly.
|
||||
//
|
||||
// The readiness check might do a set of more expensive or time-consuming checks to determine
|
||||
// if all other resources are responding.
|
||||
//
|
||||
// See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/ for
|
||||
// more details about readiness and liveness probes in Kubernetes.
|
||||
//
|
||||
// In this example, the liveness check will us an 'identity' check that always returns healthy.
|
||||
//
|
||||
// In this example, the readiness check will run all registered checks, include a check with an
|
||||
// long initialization time (15 seconds).
|
||||
|
||||
|
||||
// The readiness check uses all of the registered health checks (default)
|
||||
app.UseHealthChecks("/health/ready", new HealthCheckOptions()
|
||||
{
|
||||
// This sample is using detailed status to make more apparent which checks are being run - any
|
||||
// output format will work with liveness and readiness checks.
|
||||
ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson,
|
||||
});
|
||||
|
||||
// The liveness check uses an 'identity' health check that always returns healty
|
||||
app.UseHealthChecks("/health/live", new HealthCheckOptions()
|
||||
{
|
||||
// Filters the set of health checks run by this middleware
|
||||
HealthCheckNames =
|
||||
{
|
||||
"identity",
|
||||
},
|
||||
|
||||
// This sample is using detailed status to make more apparent which checks are being run - any
|
||||
// output format will work with liveness and readiness checks.
|
||||
ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson,
|
||||
});
|
||||
|
||||
app.Run(async (context) =>
|
||||
{
|
||||
await context.Response.WriteAsync("Go to /health/ready to see the readiness status");
|
||||
await context.Response.WriteAsync(Environment.NewLine);
|
||||
await context.Response.WriteAsync("Go to /health/live to see the liveness status");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ namespace HealthChecksSample
|
|||
{ "basic", typeof(BasicStartup) },
|
||||
{ "detailed", typeof(DetailedStatusStartup) },
|
||||
{ "writer", typeof(CustomWriterStartup) },
|
||||
{ "liveness", typeof(LivenessProbeStartup) },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace HealthChecksSample
|
||||
{
|
||||
// Simulates a health check for an application dependency that takes a while to initialize.
|
||||
// This is part of the readiness/liveness probe sample.
|
||||
public class SlowDependencyHealthCheck : IHealthCheck
|
||||
{
|
||||
public static readonly string HealthCheckName = "slow_dependency";
|
||||
|
||||
private readonly Task _task;
|
||||
|
||||
public SlowDependencyHealthCheck()
|
||||
{
|
||||
_task = Task.Delay(15 * 1000);
|
||||
}
|
||||
|
||||
public string Name => HealthCheckName;
|
||||
|
||||
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if (_task.IsCompleted)
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready"));
|
||||
}
|
||||
|
||||
return Task.FromResult(HealthCheckResult.Unhealthy("Dependency is still initializing"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
// 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.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
|
@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
|||
private readonly RequestDelegate _next;
|
||||
private readonly HealthCheckOptions _healthCheckOptions;
|
||||
private readonly IHealthCheckService _healthCheckService;
|
||||
private readonly IHealthCheck[] _checks;
|
||||
|
||||
public HealthCheckMiddleware(
|
||||
RequestDelegate next,
|
||||
|
|
@ -38,6 +41,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
|||
_next = next;
|
||||
_healthCheckOptions = healthCheckOptions.Value;
|
||||
_healthCheckService = healthCheckService;
|
||||
|
||||
_checks = FilterHealthChecks(_healthCheckService.Checks, healthCheckOptions.Value.HealthCheckNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -53,7 +58,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
|||
}
|
||||
|
||||
// Get results
|
||||
var result = await _healthCheckService.CheckHealthAsync(httpContext.RequestAborted);
|
||||
var result = await _healthCheckService.CheckHealthAsync(_checks, httpContext.RequestAborted);
|
||||
|
||||
// Map status to response code - this is customizable via options.
|
||||
if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode))
|
||||
|
|
@ -73,5 +78,41 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
|||
await _healthCheckOptions.ResponseWriter(httpContext, result);
|
||||
}
|
||||
}
|
||||
|
||||
private static IHealthCheck[] FilterHealthChecks(
|
||||
IReadOnlyDictionary<string, IHealthCheck> checks,
|
||||
ISet<string> names)
|
||||
{
|
||||
// If there are no filters then include all checks.
|
||||
if (names.Count == 0)
|
||||
{
|
||||
return checks.Values.ToArray();
|
||||
}
|
||||
|
||||
// Keep track of what we don't find so we can report errors.
|
||||
var notFound = new HashSet<string>(names, StringComparer.OrdinalIgnoreCase);
|
||||
var matches = new List<IHealthCheck>();
|
||||
|
||||
foreach (var kvp in checks)
|
||||
{
|
||||
if (!notFound.Remove(kvp.Key))
|
||||
{
|
||||
// This check was excluded
|
||||
continue;
|
||||
}
|
||||
|
||||
matches.Add(kvp.Value);
|
||||
}
|
||||
|
||||
if (notFound.Count > 0)
|
||||
{
|
||||
var message =
|
||||
$"The following health checks were not found: '{string.Join(", ", notFound)}'. " +
|
||||
$"Registered health checks: '{string.Join(", ", checks.Keys)}'.";
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
return matches.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,16 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
|||
/// </summary>
|
||||
public class HealthCheckOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a set of health check names used to filter the set of health checks run.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="HealthCheckNames"/> is empty, the <see cref="HealthCheckMiddleware"/> will run all
|
||||
/// registered health checks - this is the default behavior. To run a subset of health checks,
|
||||
/// add the names of the desired health checks.
|
||||
/// </remarks>
|
||||
public ISet<string> HealthCheckNames { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public IDictionary<HealthCheckStatus, int> ResultStatusCodes { get; } = new Dictionary<HealthCheckStatus, int>()
|
||||
{
|
||||
{ HealthCheckStatus.Healthy, StatusCodes.Status200OK },
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Net;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
||||
|
|
@ -58,5 +59,69 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
|||
|
||||
// Ignoring the body since it contains a bunch of statistics
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LivenessProbeStartup_Liveness()
|
||||
{
|
||||
var expectedJson = JsonConvert.SerializeObject(new
|
||||
{
|
||||
status = "Healthy",
|
||||
results = new
|
||||
{
|
||||
identity = new
|
||||
{
|
||||
status = "Healthy",
|
||||
description = "",
|
||||
data = new { }
|
||||
},
|
||||
},
|
||||
}, Formatting.Indented);
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.UseStartup<HealthChecksSample.LivenessProbeStartup>();
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health/live");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
|
||||
Assert.Equal(expectedJson, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LivenessProbeStartup_Readiness()
|
||||
{
|
||||
var expectedJson = JsonConvert.SerializeObject(new
|
||||
{
|
||||
status = "Unhealthy",
|
||||
results = new
|
||||
{
|
||||
identity = new
|
||||
{
|
||||
status = "Healthy",
|
||||
description = "",
|
||||
data = new { }
|
||||
},
|
||||
slow_dependency = new
|
||||
{
|
||||
status = "Unhealthy",
|
||||
description = "Dependency is still initializing",
|
||||
data = new { }
|
||||
},
|
||||
},
|
||||
}, Formatting.Indented);
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.UseStartup<HealthChecksSample.LivenessProbeStartup>();
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health/ready");
|
||||
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
||||
Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
|
||||
Assert.Equal(expectedJson, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -346,5 +346,67 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
|
|||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanFilterChecks()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health", new HealthCheckOptions()
|
||||
{
|
||||
HealthCheckNames =
|
||||
{
|
||||
"Baz",
|
||||
"FOO",
|
||||
},
|
||||
});
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
|
||||
// Will get filtered out
|
||||
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("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);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanFilterChecks_ThrowsForMissingCheck()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health", new HealthCheckOptions()
|
||||
{
|
||||
HealthCheckNames =
|
||||
{
|
||||
"Bazzzzzz",
|
||||
"FOO",
|
||||
},
|
||||
});
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
|
||||
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!")))
|
||||
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
|
||||
});
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => new TestServer(builder));
|
||||
Assert.Equal(
|
||||
"The following health checks were not found: 'Bazzzzzz'. Registered health checks: 'Foo, Bar, Baz'.",
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue