Redesign HealthStatus (again) (#520)

* Redesign HealthStatus (again)

This change brings back the ability to return Healthy/Degraded/Unhealthy
in a HealthCheckResult. We tried making this pass/fail in 2.2.0-preview3
and folks writing health checks for their own use pointed out (rightly
so) that it was too limited.

It's still possible for the app developer to configure the failure
status of a health check, but it requires the health check author to
cooperate.

I also got rid of HealthStatus.Failed since it raises more questions
than it answers. It's really not clear that it's valuable for a health
check for behave different when throwing an unhandled exception.

We would still recommend that a health check library handle exceptions
that they know about and return `context.Registration.FailureStatus`.
This commit is contained in:
Ryan Nowak 2018-10-31 12:51:14 -07:00 committed by GitHub
parent 4c94bc272b
commit c802d5ef5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 158 additions and 155 deletions

View File

@ -51,11 +51,11 @@ namespace HealthChecksSample
}
catch (DbException ex)
{
return HealthCheckResult.Failed(exception: ex);
return new HealthCheckResult(status: context.Registration.FailureStatus, exception: ex);
}
}
return HealthCheckResult.Passed();
return HealthCheckResult.Healthy();
}
}
}

View File

@ -65,13 +65,15 @@ namespace HealthChecksSample
{ "Gen2Collections", GC.CollectionCount(2) },
};
// Report failure if the allocated memory is >= the threshold. Negated because true == success
var result = !(allocated >= options.Threshold);
// Report failure if the allocated memory is >= the threshold.
//
// Using context.Registration.FailureStatus means that the application developer can configure
// how they want failures to appear.
var result = allocated >= options.Threshold ? context.Registration.FailureStatus : HealthStatus.Healthy;
return Task.FromResult(new HealthCheckResult(
result,
description: "reports degraded status if allocated bytes >= 1gb",
exception: null,
data: data));
}
}

View File

@ -21,10 +21,12 @@ namespace HealthChecksSample
{
if (_task.IsCompleted)
{
return Task.FromResult(HealthCheckResult.Passed("Dependency is ready"));
return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready"));
}
return Task.FromResult(HealthCheckResult.Failed("Dependency is still initializing"));
return Task.FromResult(new HealthCheckResult(
status: context.Registration.FailureStatus,
description: "Dependency is still initializing"));
}
}
}

View File

@ -33,9 +33,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{ HealthStatus.Healthy, StatusCodes.Status200OK },
{ HealthStatus.Degraded, StatusCodes.Status200OK },
{ HealthStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable },
// This means that a health check failed, so 500 is appropriate. This is an error.
{ HealthStatus.Failed, StatusCodes.Status500InternalServerError },
};
/// <summary>

View File

@ -14,16 +14,16 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
/// <summary>
/// Creates a new <see cref="HealthCheckResult"/> with the specified values for <paramref name="result"/>, <paramref name="exception"/>,
/// <paramref name="description"/>, and <paramref name="data"/>.
/// Creates a new <see cref="HealthCheckResult"/> with the specified values for <paramref name="status"/>,
/// <paramref name="exception"/>, <paramref name="description"/>, and <paramref name="data"/>.
/// </summary>
/// <param name="result">A value indicating the pass/fail status of the component that was checked.</param>
/// <param name="status">A value indicating the status of the component that was checked.</param>
/// <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 HealthCheckResult(bool result, string description, Exception exception, IReadOnlyDictionary<string, object> data)
public HealthCheckResult(HealthStatus status, string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
{
Result = result;
Status = status;
Description = description;
Exception = exception;
Data = data ?? _emptyReadOnlyDictionary;
@ -45,33 +45,44 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public Exception Exception { get; }
/// <summary>
/// Gets a value indicating the pass/fail status of the component that was checked. If <c>true</c>, then the component
/// is considered to have passed health validation. A <c>false</c> value will be mapped to the configured
/// <see cref="HealthStatus"/> by the health check system.
/// Gets a value indicating the status of the component that was checked.
/// </summary>
public bool Result { get; }
public HealthStatus Status { get; }
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a passing component.
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a passing component.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
public static HealthCheckResult Passed(string description = null, IReadOnlyDictionary<string, object> data = null)
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
public static HealthCheckResult Healthy(string description = null, IReadOnlyDictionary<string, object> data = null)
{
return new HealthCheckResult(result: true, description, exception: null, data);
return new HealthCheckResult(status: HealthStatus.Healthy, description, exception: null, data);
}
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an failing component.
/// Creates a <see cref="HealthCheckResult"/> representing a degraded component.
/// </summary>
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status. Optional.</param>
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
/// <returns>A <see cref="HealthCheckResult"/> representing an failing component.</returns>
public static HealthCheckResult Failed(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
/// <returns>A <see cref="HealthCheckResult"/> representing a degraged component.</returns>
public static HealthCheckResult Degraded(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
{
return new HealthCheckResult(result: false, description, exception, data);
return new HealthCheckResult(status: HealthStatus.Degraded, description, exception: null, data);
}
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
/// </summary>
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status. Optional.</param>
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
public static HealthCheckResult Unhealthy(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
{
return new HealthCheckResult(status: HealthStatus.Unhealthy, description, exception, data);
}
}
}

View File

@ -54,7 +54,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
currentValue = entry.Status;
}
if (currentValue == HealthStatus.Failed)
if (currentValue == HealthStatus.Unhealthy)
{
// Game over, man! Game over!
// (We hit the worst possible status, so there's no need to keep iterating)

View File

@ -52,8 +52,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public Exception Exception { get; }
/// <summary>
/// Gets the health status of the component that was checked. The <see cref="Status"/> is based on the pass/fail value of
/// <see cref="HealthCheckResult.Passed"/> and the configured value of <see cref="HealthCheckRegistration.FailureStatus"/>.
/// Gets the health status of the component that was checked.
/// </summary>
public HealthStatus Status { get; }
}

View File

@ -8,10 +8,6 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
/// </summary>
/// <remarks>
/// <para>
/// A health status is derived the pass/fail result of an <see cref="IHealthCheck"/> (<see cref="HealthCheckResult.Result"/>)
/// and the corresponding value of <see cref="HealthCheckRegistration.FailureStatus"/>.
/// </para>
/// <para>
/// A status of <see cref="Unhealthy"/> should be considered the default value for a failing health check. Application
/// developers may configure a health check to report a different status as desired.
/// </para>
@ -23,23 +19,19 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public enum HealthStatus
{
/// <summary>
/// Indicates that an unexpected exception was thrown when running the health check.
/// Indicates that the health check determined that the component was unhealthy, or an unhandled
/// exception was thrown while executing the health check.
/// </summary>
Failed = 0,
/// <summary>
/// Indicates that the health check determined that the component was unhealthy.
/// </summary>
Unhealthy = 1,
Unhealthy = 0,
/// <summary>
/// Indicates that the health check determined that the component was in a degraded state.
/// </summary>
Degraded = 2,
Degraded = 1,
/// <summary>
/// Indicates that the health check determined that the component was healthy.
/// </summary>
Healthy = 3,
Healthy = 2,
}
}

View File

@ -47,10 +47,10 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
if (await testQuery(_dbContext, cancellationToken))
{
return HealthCheckResult.Passed();
return HealthCheckResult.Healthy();
}
return HealthCheckResult.Failed();
return HealthCheckResult.Unhealthy();
}
}
}

View File

@ -76,7 +76,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var duration = stopwatch.GetElapsedTime();
entry = new HealthReportEntry(
status: result.Result ? HealthStatus.Healthy : registration.FailureStatus,
status: result.Status,
description: result.Description,
duration: duration,
exception: result.Exception,
@ -91,7 +91,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
var duration = stopwatch.GetElapsedTime();
entry = new HealthReportEntry(
status: HealthStatus.Failed,
status: HealthStatus.Unhealthy,
description: ex.Message,
duration: duration,
exception: ex,
@ -212,10 +212,6 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
case HealthStatus.Unhealthy:
_healthCheckEndUnhealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
break;
case HealthStatus.Failed:
_healthCheckEndFailed(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
break;
}
}

View File

@ -19,10 +19,6 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <param name="check">A delegate that provides the health check implementation.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
@ -30,7 +26,6 @@ namespace Microsoft.Extensions.DependencyInjection
this IHealthChecksBuilder builder,
string name,
Func<HealthCheckResult> check,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null)
{
if (builder == null)
@ -49,7 +44,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
var instance = new DelegateHealthCheck((ct) => Task.FromResult(check()));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags));
}
/// <summary>
@ -57,10 +52,6 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <param name="check">A delegate that provides the health check implementation.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
@ -68,7 +59,6 @@ namespace Microsoft.Extensions.DependencyInjection
this IHealthChecksBuilder builder,
string name,
Func<CancellationToken, HealthCheckResult> check,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null)
{
if (builder == null)
@ -87,7 +77,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct)));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags));
}
/// <summary>
@ -95,10 +85,6 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <param name="check">A delegate that provides the health check implementation.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
@ -106,7 +92,6 @@ namespace Microsoft.Extensions.DependencyInjection
this IHealthChecksBuilder builder,
string name,
Func<Task<HealthCheckResult>> check,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null)
{
if (builder == null)
@ -125,7 +110,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
var instance = new DelegateHealthCheck((ct) => check());
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags));
}
/// <summary>
@ -133,10 +118,6 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <param name="check">A delegate that provides the health check implementation.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
@ -144,7 +125,6 @@ namespace Microsoft.Extensions.DependencyInjection
this IHealthChecksBuilder builder,
string name,
Func<CancellationToken, Task<HealthCheckResult>> check,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null)
{
if (builder == null)
@ -163,7 +143,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
var instance = new DelegateHealthCheck((ct) => check(ct));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags));
}
}
}

View File

@ -110,9 +110,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!"))
.AddCheck("Bar", () => HealthCheckResult.Passed("A-ok!"))
.AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!"));
.AddCheck("Foo", () => HealthCheckResult.Healthy("A-ok!"))
.AddCheck("Bar", () => HealthCheckResult.Healthy("A-ok!"))
.AddCheck("Baz", () => HealthCheckResult.Healthy("A-ok!"));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -135,9 +135,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!"))
.AddCheck("Bar", () => HealthCheckResult.Failed("Not so great."), failureStatus: HealthStatus.Degraded)
.AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!"));
.AddCheck("Foo", () => HealthCheckResult.Healthy("A-ok!"))
.AddCheck("Bar", () => HealthCheckResult.Degraded("Not so great."))
.AddCheck("Baz", () => HealthCheckResult.Healthy("A-ok!"));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -160,9 +160,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
}
[Fact]
public async Task StatusCodeIs500IfCheckIsFailed()
public async Task StatusCodeIs503IfCheckHasUnhandledException()
{
var builder = new WebHostBuilder()
.Configure(app =>
@ -185,18 +185,18 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddAsyncCheck("Bar", () => throw null)
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
.AddAsyncCheck("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);
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Failed", await response.Content.ReadAsStringAsync());
Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync());
}
[Fact]
@ -223,9 +223,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -252,9 +252,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -357,10 +357,10 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
// Will get filtered out
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("A-ok!")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();

View File

@ -14,9 +14,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
public class DbContextHealthCheckTest
{
// Just testing pass here since it would be complicated to simulate a failure. All of that logic lives in EF anyway.
// Just testing healthy here since it would be complicated to simulate a failure. All of that logic lives in EF anyway.
[Fact]
public async Task CheckAsync_DefaultTest_Pass()
public async Task CheckAsync_DefaultTest_Healthy()
{
// Arrange
var services = CreateServices();
@ -29,12 +29,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
// Assert
Assert.True(result.Result, "Health check passed");
Assert.Equal(HealthStatus.Healthy, result.Status);
}
}
[Fact]
public async Task CheckAsync_CustomTest_Pass()
public async Task CheckAsync_CustomTest_Healthy()
{
// Arrange
var services = CreateServices(async (c, ct) =>
@ -56,19 +56,18 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
// Assert
Assert.True(result.Result, "Health check passed");
Assert.Equal(HealthStatus.Healthy, result.Status);
}
}
[Fact]
public async Task CheckAsync_CustomTest_Fail()
public async Task CheckAsync_CustomTest_Degraded()
{
// Arrange
var services = CreateServices(async (c, ct) =>
{
return 0 < await c.Blogs.CountAsync();
});
}, failureStatus: HealthStatus.Degraded);
using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
@ -79,17 +78,41 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
// Assert
Assert.False(result.Result, "Health check failed");
Assert.Equal(HealthStatus.Unhealthy, result.Status);
}
}
private static IServiceProvider CreateServices(Func<TestDbContext, CancellationToken, Task<bool>> testQuery = null)
[Fact]
public async Task CheckAsync_CustomTest_Unhealthy()
{
// Arrange
var services = CreateServices(async (c, ct) =>
{
return 0 < await c.Blogs.CountAsync();
}, failureStatus: HealthStatus.Unhealthy);
using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var registration = Assert.Single(services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value.Registrations);
var check = ActivatorUtilities.CreateInstance<DbContextHealthCheck<TestDbContext>>(scope.ServiceProvider);
// Act
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
// Assert
Assert.Equal(HealthStatus.Unhealthy, result.Status);
}
}
private static IServiceProvider CreateServices(
Func<TestDbContext, CancellationToken, Task<bool>> testQuery = null,
HealthStatus failureStatus = HealthStatus.Unhealthy)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDbContext<TestDbContext>(o => o.UseInMemoryDatabase("Test"));
var builder = serviceCollection.AddHealthChecks();
builder.AddDbContextCheck<TestDbContext>("test", HealthStatus.Degraded, new[] { "tag1", "tag2", }, testQuery);
builder.AddDbContextCheck<TestDbContext>("test", failureStatus, new[] { "tag1", "tag2", }, testQuery);
return serviceCollection.BuildServiceProvider();
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
@ -26,11 +27,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
serviceCollection.AddLogging();
serviceCollection.AddOptions();
serviceCollection.AddHealthChecks()
.AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())))
.AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())))
.AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())))
.AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())))
.AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())));
.AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())));
var services = serviceCollection.BuildServiceProvider();
@ -63,16 +64,25 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var service = CreateHealthChecksService(b =>
{
b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data)));
b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded);
b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception)));
b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(results.Entries,
Assert.Collection(
results.Entries.OrderBy(kvp => kvp.Key),
actual =>
{
Assert.Equal("DegradedCheck", actual.Key);
Assert.Equal(DegradedMessage, actual.Value.Description);
Assert.Equal(HealthStatus.Degraded, actual.Value.Status);
Assert.Null(actual.Value.Exception);
Assert.Empty(actual.Value.Data);
},
actual =>
{
Assert.Equal("HealthyCheck", actual.Key);
@ -86,14 +96,6 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
});
},
actual =>
{
Assert.Equal("DegradedCheck", actual.Key);
Assert.Equal(DegradedMessage, actual.Value.Description);
Assert.Equal(HealthStatus.Degraded, actual.Value.Status);
Assert.Null(actual.Value.Exception);
Assert.Empty(actual.Value.Data);
},
actual =>
{
Assert.Equal("UnhealthyCheck", actual.Key);
Assert.Equal(UnhealthyMessage, actual.Value.Description);
@ -121,9 +123,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var service = CreateHealthChecksService(b =>
{
b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data)));
b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded);
b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception)));
b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
});
// Act
@ -201,7 +203,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
insideCheck.SetResult(null);
await Task.Delay(10000, ct);
return HealthCheckResult.Failed();
return HealthCheckResult.Unhealthy();
});
});
@ -218,7 +220,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
}
[Fact]
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToFailedResultAsync()
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToUnhealthyResultAsync()
{
// Arrange
var thrownException = new InvalidOperationException("Whoops!");
@ -228,7 +230,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
b.AddAsyncCheck("Throws", ct => throw thrownException);
b.AddAsyncCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException));
b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Passed()));
b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy()));
});
// Act
@ -241,14 +243,14 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
Assert.Equal("Throws", actual.Key);
Assert.Equal(thrownException.Message, actual.Value.Description);
Assert.Equal(HealthStatus.Failed, actual.Value.Status);
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
Assert.Same(thrownException, actual.Value.Exception);
},
actual =>
{
Assert.Equal("Faults", actual.Key);
Assert.Equal(faultedException.Message, actual.Value.Description);
Assert.Equal(HealthStatus.Failed, actual.Value.Status);
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
Assert.Same(faultedException, actual.Value.Exception);
},
actual =>
@ -278,7 +280,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
Assert.Equal("TestScope", item.Value);
});
});
return Task.FromResult(HealthCheckResult.Passed());
return Task.FromResult(HealthCheckResult.Healthy());
});
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
@ -398,7 +400,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
return Task.FromResult(HealthCheckResult.Passed());
return Task.FromResult(HealthCheckResult.Healthy());
}
}
@ -410,7 +412,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
{ "name", context.Registration.Name },
};
return Task.FromResult(HealthCheckResult.Passed(data: data));
return Task.FromResult(HealthCheckResult.Healthy(data: data));
}
}
}

View File

@ -20,7 +20,7 @@ namespace Microsoft.Extensions.DependencyInjection
// Arrange
var instance = new DelegateHealthCheck((_) =>
{
return Task.FromResult(HealthCheckResult.Passed());
return Task.FromResult(HealthCheckResult.Healthy());
});
var services = CreateServices();
@ -112,9 +112,9 @@ namespace Microsoft.Extensions.DependencyInjection
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, check: () =>
services.AddHealthChecks().AddCheck("test", tags: new[] { "tag", }, check: () =>
{
return HealthCheckResult.Passed();
return HealthCheckResult.Healthy();
});
var serviceProvider = services.BuildServiceProvider();
@ -125,7 +125,7 @@ namespace Microsoft.Extensions.DependencyInjection
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
@ -137,8 +137,8 @@ namespace Microsoft.Extensions.DependencyInjection
var services = CreateServices();
services.AddHealthChecks().AddCheck("test", (_) =>
{
return HealthCheckResult.Passed();
}, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
return HealthCheckResult.Degraded();
}, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
@ -148,7 +148,7 @@ namespace Microsoft.Extensions.DependencyInjection
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
@ -160,8 +160,8 @@ namespace Microsoft.Extensions.DependencyInjection
var services = CreateServices();
services.AddHealthChecks().AddAsyncCheck("test", () =>
{
return Task.FromResult(HealthCheckResult.Passed());
}, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
return Task.FromResult(HealthCheckResult.Healthy());
}, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
@ -171,7 +171,7 @@ namespace Microsoft.Extensions.DependencyInjection
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
@ -183,8 +183,8 @@ namespace Microsoft.Extensions.DependencyInjection
var services = CreateServices();
services.AddHealthChecks().AddAsyncCheck("test", (_) =>
{
return Task.FromResult(HealthCheckResult.Passed());
}, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
return Task.FromResult(HealthCheckResult.Unhealthy());
}, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
@ -194,7 +194,7 @@ namespace Microsoft.Extensions.DependencyInjection
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
@ -205,10 +205,10 @@ namespace Microsoft.Extensions.DependencyInjection
var services = new ServiceCollection();
services
.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed()));
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy()));
services
.AddHealthChecks()
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Passed()));
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy()));
// Act
var options = services.BuildServiceProvider().GetRequiredService<IOptions<HealthCheckServiceOptions>>();

View File

@ -448,8 +448,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
serviceCollection.AddOptions();
serviceCollection.AddLogging();
serviceCollection.AddHealthChecks()
.AddCheck("one", () => { return HealthCheckResult.Passed(); })
.AddCheck("two", () => { return HealthCheckResult.Passed(); });
.AddCheck("one", () => { return HealthCheckResult.Healthy(); })
.AddCheck("two", () => { return HealthCheckResult.Healthy(); });
// Choosing big values for tests to make sure that we're not dependent on the defaults.
// All of the tests that rely on the timer will set their own values for speed.

View File

@ -13,7 +13,6 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
[InlineData(HealthStatus.Healthy)]
[InlineData(HealthStatus.Degraded)]
[InlineData(HealthStatus.Unhealthy)]
[InlineData(HealthStatus.Failed)]
public void Status_MatchesWorstStatusInResults(HealthStatus status)
{
var result = new HealthReport(new Dictionary<string, HealthReportEntry>()