Merge pull request #481 from dotnet-maestro-bot/merge/release/2.2-to-master

[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
Ryan Nowak 2018-09-19 19:12:35 -07:00 committed by GitHub
commit 4cecbc668a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1478 additions and 824 deletions

View File

@ -17,14 +17,10 @@ namespace HealthChecksSample
public void ConfigureServices(IServiceCollection services)
{
// Registers required services for health checks
services.AddHealthChecks();
// This is an example of registering a custom health check as a service.
// All IHealthCheck services will be available to the health check service and
// middleware.
//
// We recommend registering all health checks as Singleton services.
services.AddSingleton<IHealthCheck, GCInfoHealthCheck>();
services.AddHealthChecks()
// Registers a custom health check implementation
.AddGCInfoCheck("GCInfo");
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
@ -45,13 +41,13 @@ namespace HealthChecksSample
});
}
private static Task WriteResponse(HttpContext httpContext, CompositeHealthCheckResult result)
private static Task WriteResponse(HttpContext httpContext, HealthReport result)
{
httpContext.Response.ContentType = "application/json";
var json = new JObject(
new JProperty("status", result.Status.ToString()),
new JProperty("results", new JObject(result.Results.Select(pair =>
new JProperty("results", new JObject(result.Entries.Select(pair =>
new JProperty(pair.Key, new JObject(
new JProperty("status", pair.Value.Status.ToString()),
new JProperty("description", pair.Value.Description),

View File

@ -22,7 +22,7 @@ namespace HealthChecksSample
// Registers required services for health checks
services.AddHealthChecks()
// Add a health check for a SQL database
.AddCheck(new SqlConnectionHealthCheck("MyDatabase", Configuration["ConnectionStrings:DefaultConnection"]));
.AddCheck("MyDatabase", new SqlConnectionHealthCheck(Configuration["ConnectionStrings:DefaultConnection"]));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)

View File

@ -11,20 +11,17 @@ namespace HealthChecksSample
{
public abstract class DbConnectionHealthCheck : IHealthCheck
{
protected DbConnectionHealthCheck(string name, string connectionString)
: this(name, connectionString, testQuery: null)
protected DbConnectionHealthCheck(string connectionString)
: this(connectionString, testQuery: null)
{
}
protected DbConnectionHealthCheck(string name, string connectionString, string testQuery)
protected DbConnectionHealthCheck(string connectionString, string testQuery)
{
Name = name ?? throw new System.ArgumentNullException(nameof(name));
ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
TestQuery = testQuery;
}
public string Name { get; }
protected string ConnectionString { get; }
// This sample supports specifying a query to run as a boolean test of whether the database
@ -36,7 +33,7 @@ namespace HealthChecksSample
protected abstract DbConnection CreateConnection(string connectionString);
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
using (var connection = CreateConnection(ConnectionString))
{
@ -54,11 +51,11 @@ namespace HealthChecksSample
}
catch (DbException ex)
{
return HealthCheckResult.Unhealthy(ex);
return HealthCheckResult.Failed(exception: ex);
}
}
return HealthCheckResult.Healthy();
return HealthCheckResult.Passed();
}
}
}

View File

@ -2,21 +2,58 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
namespace HealthChecksSample
{
// This is an example of a custom health check that implements IHealthCheck.
//
// See CustomWriterStartup to see how this is registered.
// This example also shows a technique for authoring a health check that needs to be registered
// with additional configuration data. This technique works via named options, and is useful
// for authoring health checks that can be disctributed as libraries.
public static class GCInfoHealthCheckBuilderExtensions
{
public static IHealthChecksBuilder AddGCInfoCheck(
this IHealthChecksBuilder builder,
string name,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null,
long? thresholdInBytes = null)
{
// Register a check of type GCInfo
builder.AddCheck<GCInfoHealthCheck>(name, failureStatus ?? HealthStatus.Degraded, tags);
// Configure named options to pass the threshold into the check.
if (thresholdInBytes.HasValue)
{
builder.Services.Configure<GCInfoOptions>(name, options =>
{
options.Threshold = thresholdInBytes.Value;
});
}
return builder;
}
}
public class GCInfoHealthCheck : IHealthCheck
{
public string Name { get; } = "GCInfo";
private readonly IOptionsMonitor<GCInfoOptions> _options;
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public GCInfoHealthCheck(IOptionsMonitor<GCInfoOptions> options)
{
_options = options;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var options = _options.Get(context.Registration.Name);
// This example will report degraded status if the application is using
// more than 1gb of memory.
// more than the configured amount of memory (1gb by default).
//
// Additionally we include some GC info in the reported diagnostics.
var allocated = GC.GetTotalMemory(forceFullCollection: false);
@ -28,14 +65,20 @@ namespace HealthChecksSample
{ "Gen2Collections", GC.CollectionCount(2) },
};
// Report degraded status if the allocated memory is >= 1gb (in bytes)
var status = allocated >= 1024 * 1024 * 1024 ? HealthCheckStatus.Degraded : HealthCheckStatus.Healthy;
// Report failure if the allocated memory is >= the threshold
var result = allocated >= options.Threshold;
return Task.FromResult(new HealthCheckResult(
status,
exception: null,
result,
description: "reports degraded status if allocated bytes >= 1gb",
exception: null,
data: data));
}
}
public class GCInfoOptions
{
// The failure threshold (in bytes)
public long Threshold { get; set; } = 1024L * 1024L * 1024L;
}
}

View File

@ -1,11 +1,9 @@
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
{
@ -17,7 +15,7 @@ namespace HealthChecksSample
// Registers required services for health checks
services
.AddHealthChecks()
.AddCheck(new SlowDependencyHealthCheck());
.AddCheck<SlowDependencyHealthCheck>("Slow", failureStatus: null, tags: new[] { "ready", });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
@ -46,10 +44,13 @@ namespace HealthChecksSample
// long initialization time (15 seconds).
// The readiness check uses all of the registered health checks (default)
app.UseHealthChecks("/health/ready");
// The readiness check uses all registered checks with the 'ready' tag.
app.UseHealthChecks("/health/ready", new HealthCheckOptions()
{
Predicate = (check) => check.Tags.Contains("ready"),
});
// The liveness check uses an 'identity' health check that always returns healthy
// The liveness filters out all checks and just returns success
app.UseHealthChecks("/health/live", new HealthCheckOptions()
{
// Exclude all checks, just return a 200.

View File

@ -17,16 +17,14 @@ namespace HealthChecksSample
_task = Task.Delay(15 * 1000);
}
public string Name => HealthCheckName;
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
if (_task.IsCompleted)
{
return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready"));
return Task.FromResult(HealthCheckResult.Passed("Dependency is ready"));
}
return Task.FromResult(HealthCheckResult.Unhealthy("Dependency is still initializing"));
return Task.FromResult(HealthCheckResult.Failed("Dependency is still initializing"));
}
}
}

View File

@ -1,6 +1,5 @@
using System.Data.Common;
using System.Data.SqlClient;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace HealthChecksSample
{
@ -8,13 +7,13 @@ namespace HealthChecksSample
{
private static readonly string DefaultTestQuery = "Select 1";
public SqlConnectionHealthCheck(string name, string connectionString)
: this(name, connectionString, testQuery: DefaultTestQuery)
public SqlConnectionHealthCheck(string connectionString)
: this(connectionString, testQuery: DefaultTestQuery)
{
}
public SqlConnectionHealthCheck(string name, string connectionString, string testQuery)
: base(name, connectionString, testQuery ?? DefaultTestQuery)
public SqlConnectionHealthCheck(string connectionString, string testQuery)
: base(connectionString, testQuery ?? DefaultTestQuery)
{
}

View File

@ -15,12 +15,12 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
private readonly RequestDelegate _next;
private readonly HealthCheckOptions _healthCheckOptions;
private readonly IHealthCheckService _healthCheckService;
private readonly HealthCheckService _healthCheckService;
public HealthCheckMiddleware(
RequestDelegate next,
IOptions<HealthCheckOptions> healthCheckOptions,
IHealthCheckService healthCheckService)
HealthCheckService healthCheckService)
{
if (next == null)
{
@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode))
{
var message =
$"No status code mapping found for {nameof(HealthCheckStatus)} value: {result.Status}." +
$"No status code mapping found for {nameof(HealthStatus)} value: {result.Status}." +
$"{nameof(HealthCheckOptions)}.{nameof(HealthCheckOptions.ResultStatusCodes)} must contain" +
$"an entry for {result.Status}.";

View File

@ -22,20 +22,20 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
/// registered health checks - this is the default behavior. To run a subset of health checks,
/// provide a function that filters the set of checks.
/// </remarks>
public Func<IHealthCheck, bool> Predicate { get; set; }
public Func<HealthCheckRegistration, bool> Predicate { get; set; }
/// <summary>
/// Gets a dictionary mapping the <see cref="HealthCheckStatus"/> to an HTTP status code applied to the response.
/// Gets a dictionary mapping the <see cref="HealthStatus"/> to an HTTP status code applied to the response.
/// This property can be used to configure the status codes returned for each status.
/// </summary>
public IDictionary<HealthCheckStatus, int> ResultStatusCodes { get; } = new Dictionary<HealthCheckStatus, int>()
public IDictionary<HealthStatus, int> ResultStatusCodes { get; } = new Dictionary<HealthStatus, int>()
{
{ HealthCheckStatus.Healthy, StatusCodes.Status200OK },
{ HealthCheckStatus.Degraded, StatusCodes.Status200OK },
{ HealthCheckStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable },
{ 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.
{ HealthCheckStatus.Failed, StatusCodes.Status500InternalServerError },
{ HealthStatus.Failed, StatusCodes.Status500InternalServerError },
};
/// <summary>
@ -43,8 +43,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
/// </summary>
/// <remarks>
/// The default value is a delegate that will write a minimal <c>text/plain</c> response with the value
/// of <see cref="CompositeHealthCheckResult.Status"/> as a string.
/// of <see cref="HealthReport.Status"/> as a string.
/// </remarks>
public Func<HttpContext, CompositeHealthCheckResult, Task> ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext;
public Func<HttpContext, HealthReport, Task> ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext;
}
}

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
internal static class HealthCheckResponseWriters
{
public static Task WriteMinimalPlaintext(HttpContext httpContext, CompositeHealthCheckResult result)
public static Task WriteMinimalPlaintext(HttpContext httpContext, HealthReport result)
{
httpContext.Response.ContentType = "text/plain";
return httpContext.Response.WriteAsync(result.Status.ToString());

View File

@ -0,0 +1,13 @@
// 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
{
public sealed class HealthCheckContext
{
/// <summary>
/// Gets or sets the <see cref="HealthCheckRegistration"/> of the currently executing <see cref="IHealthCheck"/>.
/// </summary>
public HealthCheckRegistration Registration { get; set; }
}
}

View File

@ -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;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Represent the registration information associated with an <see cref="IHealthCheck"/> implementation.
/// </summary>
/// <remarks>
/// <para>
/// The health check registration is provided as a separate object so that application developers can customize
/// how health check implementations are configured.
/// </para>
/// <para>
/// The registration is provided to an <see cref="IHealthCheck"/> implementation during execution through
/// <see cref="HealthCheckContext.Registration"/>. This allows a health check implementation to access named
/// options or perform other operations based on the registered name.
/// </para>
/// </remarks>
public sealed class HealthCheckRegistration
{
private Func<IServiceProvider, IHealthCheck> _factory;
private string _name;
/// <summary>
/// Creates a new <see cref="HealthCheckRegistration"/> for an existing <see cref="IHealthCheck"/> instance.
/// </summary>
/// <param name="name">The health check name.</param>
/// <param name="instance">The <see cref="IHealthCheck"/> instance.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported upon failure of the health check. 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 for filtering health checks.</param>
public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable<string> tags)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
Name = name;
FailureStatus = failureStatus ?? HealthStatus.Unhealthy;
Tags = new HashSet<string>(tags ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
Factory = (_) => instance;
}
/// <summary>
/// Creates a new <see cref="HealthCheckRegistration"/> for an existing <see cref="IHealthCheck"/> instance.
/// </summary>
/// <param name="name">The health check name.</param>
/// <param name="factory">A delegate used to create the <see cref="IHealthCheck"/> instance.</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 for filtering health checks.</param>
public HealthCheckRegistration(
string name,
Func<IServiceProvider, IHealthCheck> factory,
HealthStatus? failureStatus,
IEnumerable<string> tags)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
Name = name;
FailureStatus = failureStatus ?? HealthStatus.Unhealthy;
Tags = new HashSet<string>(tags ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
Factory = factory;
}
/// <summary>
/// Gets or sets a delegate used to create the <see cref="IHealthCheck"/> instance.
/// </summary>
public Func<IServiceProvider, IHealthCheck> Factory
{
get => _factory;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_factory = value;
}
}
/// <summary>
/// Gets or sets the <see cref="HealthStatus"/> that should be reported upon failure of the health check.
/// </summary>
public HealthStatus FailureStatus { get; set; }
/// <summary>
/// Gets or sets the health check name.
/// </summary>
public string Name
{
get => _name;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_name = value;
}
}
/// <summary>
/// Gets a list of tags that can be used for filtering health checks.
/// </summary>
public ISet<string> Tags { get; }
}
}

View File

@ -13,176 +13,65 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
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.
/// Creates a new <see cref="HealthCheckResult"/> with the specified values for <paramref name="result"/>, <paramref name="exception"/>,
/// <paramref name="description"/>, and <paramref name="data"/>.
/// </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;
/// <param name="result">A value indicating the pass/fail 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)
{
Result = result;
Description = description;
Exception = exception;
Data = data ?? _emptyReadOnlyDictionary;
}
/// <summary>
/// Gets additional key-value pairs describing the health of the component.
/// </summary>
public IReadOnlyDictionary<string, object> Data => _data ?? _emptyReadOnlyDictionary;
public IReadOnlyDictionary<string, object> Data { get; }
/// <summary>
/// Creates a new <see cref="HealthCheckResult"/> with the specified <paramref name="status"/>, <paramref name="exception"/>,
/// <paramref name="description"/>, and <paramref name="data"/>.
/// Gets a human-readable description of the status of the component that was checked.
/// </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));
}
public string Description { get; }
Status = status;
Exception = exception;
_description = description;
_data = data;
/// <summary>
/// Gets an <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).
/// </summary>
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.
/// </summary>
public bool Result { get; }
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a passing 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)
{
return new HealthCheckResult(result: true, description, exception: null, data);
}
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
/// Creates a <see cref="HealthCheckResult"/> representing an failing 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);
/// <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)
{
return new HealthCheckResult(result: false, description, exception, data);
}
}
}

View File

@ -4,38 +4,38 @@
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Represents the status of a health check result.
/// Represents the reported 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"/>.
/// <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>
/// The values of this enum or ordered from least healthy to most healthy. So <see cref="HealthStatus.Degraded"/> is
/// greater than <see cref="HealthStatus.Unhealthy"/> but less than <see cref="HealthStatus.Healthy"/>.
/// </para>
/// </remarks>
public enum HealthCheckStatus
public enum HealthStatus
{
/// <summary>
/// This value should not be returned by a health check. It is used to represent an uninitialized value.
/// Indicates that an unexpected exception was thrown when running the health check.
/// </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,
Failed = 0,
/// <summary>
/// Indicates that the health check determined that the component was unhealthy.
/// </summary>
Unhealthy = 2,
Unhealthy = 1,
/// <summary>
/// Indicates that the health check determined that the component was in a degraded state.
/// </summary>
Degraded = 3,
Degraded = 2,
/// <summary>
/// Indicates that the health check determined that the component was healthy.
/// </summary>
Healthy = 4,
Healthy = 3,
}
}

View File

@ -12,16 +12,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
/// </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="context">A context object associated with the current execution.</param>
/// <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);
Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default);
}
}

View File

@ -1,66 +0,0 @@
// 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;
}
}
}

View File

@ -0,0 +1,149 @@
// 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.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
internal class DefaultHealthCheckService : HealthCheckService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IOptions<HealthCheckServiceOptions> _options;
private readonly ILogger<DefaultHealthCheckService> _logger;
public DefaultHealthCheckService(
IServiceScopeFactory scopeFactory,
IOptions<HealthCheckServiceOptions> options,
ILogger<DefaultHealthCheckService> logger)
{
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// We're specifically going out of our way to do this at startup time. We want to make sure you
// get any kind of health-check related error as early as possible. Waiting until someone
// actually tries to **run** health checks would be real baaaaad.
ValidateRegistrations(_options.Value.Registrations);
}
public override async Task<HealthReport> CheckHealthAsync(
Func<HealthCheckRegistration, bool> predicate,
CancellationToken cancellationToken = default)
{
var registrations = _options.Value.Registrations;
using (var scope = _scopeFactory.CreateScope())
{
var context = new HealthCheckContext();
var entries = new Dictionary<string, HealthReportEntry>(StringComparer.OrdinalIgnoreCase);
foreach (var registration in registrations)
{
if (predicate != null && !predicate(registration))
{
continue;
}
cancellationToken.ThrowIfCancellationRequested();
var healthCheck = registration.Factory(scope.ServiceProvider);
// 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(registration.Name)))
{
var stopwatch = ValueStopwatch.StartNew();
context.Registration = registration;
Log.HealthCheckBegin(_logger, registration);
HealthReportEntry entry;
try
{
var result = await healthCheck.CheckHealthAsync(context, cancellationToken);
entry = new HealthReportEntry(
result.Result ? HealthStatus.Healthy : registration.FailureStatus,
result.Description,
result.Exception,
result.Data);
Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime());
}
catch (Exception ex)
{
entry = new HealthReportEntry(HealthStatus.Failed, ex.Message, ex, data: null);
Log.HealthCheckError(_logger, registration, ex, stopwatch.GetElapsedTime());
}
entries[registration.Name] = entry;
}
}
return new HealthReport(entries);
}
}
private static void ValidateRegistrations(IEnumerable<HealthCheckRegistration> registrations)
{
// Scan the list for duplicate names to provide a better error if there are duplicates.
var duplicateNames = registrations
.GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
if (duplicateNames.Count > 0)
{
throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(registrations));
}
}
private static class Log
{
public static class EventIds
{
public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin");
public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd");
public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError");
}
private static readonly Action<ILogger, string, Exception> _healthCheckBegin = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.HealthCheckBegin,
"Running health check {HealthCheckName}");
private static readonly Action<ILogger, string, double, HealthStatus, Exception> _healthCheckEnd = LoggerMessage.Define<string, double, HealthStatus>(
LogLevel.Debug,
EventIds.HealthCheckEnd,
"Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}");
private static readonly Action<ILogger, string, double, Exception> _healthCheckError = LoggerMessage.Define<string, double>(
LogLevel.Error,
EventIds.HealthCheckError,
"Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms");
public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration)
{
_healthCheckBegin(logger, registration.Name, null);
}
public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration)
{
_healthCheckEnd(logger, registration.Name, duration.TotalMilliseconds, entry.Status, null);
}
public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration)
{
_healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception);
}
}
}
}

View File

@ -11,31 +11,25 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
/// A simple implementation of <see cref="IHealthCheck"/> which uses a provided delegate to
/// implement the check.
/// </summary>
public sealed class HealthCheck : IHealthCheck
internal sealed class DelegateHealthCheck : IHealthCheck
{
private readonly Func<CancellationToken, Task<HealthCheckResult>> _check;
/// <summary>
/// Create an instance of <see cref="HealthCheck"/> from the specified <paramref name="name"/> and <paramref name="check"/>.
/// Create an instance of <see cref="DelegateHealthCheck"/> from the specified delegate.
/// </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)
public DelegateHealthCheck(Func<CancellationToken, Task<HealthCheckResult>> check)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
_check = check ?? throw new ArgumentNullException(nameof(check));
}
/// <summary>
/// Gets the name of the health check, which should indicate the component being checked.
/// </summary>
public string Name { get; }
/// <summary>
/// Runs the health check, returning the status of the component being checked.
/// </summary>
/// <param name="context">A context object associated with the current execution.</param>
/// <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);
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) => _check(cancellationToken);
}
}

View File

@ -7,24 +7,24 @@ using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Provides extension methods for registering <see cref="IHealthCheckService"/> in an <see cref="IServiceCollection"/>.
/// Provides extension methods for registering <see cref="HealthCheckService"/> 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
/// Adds the <see cref="HealthCheckService"/> 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
/// <see cref="HealthCheckService"/> 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>
/// <param name="services">The <see cref="IServiceCollection"/> to add the <see cref="HealthCheckService"/> 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>());
services.TryAdd(ServiceDescriptor.Singleton<HealthCheckService, DefaultHealthCheckService>());
return new HealthChecksBuilder(services);
}
}

View File

@ -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 System;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection
{
internal class HealthChecksBuilder : IHealthChecksBuilder
{
public HealthChecksBuilder(IServiceCollection services)
{
Services = services;
}
public IServiceCollection Services { get; }
public IHealthChecksBuilder Add(HealthCheckRegistration registration)
{
if (registration == null)
{
throw new ArgumentNullException(nameof(registration));
}
Services.Configure<HealthCheckServiceOptions>(options =>
{
options.Registrations.Add(registration);
});
return this;
}
}
}

View File

@ -0,0 +1,191 @@
// 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 Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Provides basic 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"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="instance">An <see cref="IHealthCheck"/> instance.</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>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
public static IHealthChecksBuilder AddCheck(
this IHealthChecksBuilder builder,
string name,
IHealthCheck instance,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
}
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </summary>
/// <typeparam name="T">The health check implementation type.</typeparam>
/// <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>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// This method will use <see cref="ActivatorUtilities.GetServiceOrCreateInstance{T}(IServiceProvider)"/> to create the health check
/// instance when needed. If a service of type <typeparamref name="T"/> is registred in the dependency injection container
/// with any liftime it will be used. Otherwise an instance of type <typeparamref name="T"/> will be constructed with
/// access to services from the dependency injection container.
/// </remarks>
public static IHealthChecksBuilder AddCheck<T>(
this IHealthChecksBuilder builder,
string name,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null) where T : class, IHealthCheck
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.GetServiceOrCreateInstance<T>(s), failureStatus, tags));
}
// NOTE: AddTypeActivatedCheck has overloads rather than default parameters values, because default parameter values don't
// play super well with params.
/// <summary>
/// Adds a new type activated health check with the specified name and implementation.
/// </summary>
/// <typeparam name="T">The health check implementation type.</typeparam>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="args">Additional arguments to provide to the constructor.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
/// </remarks>
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(this IHealthChecksBuilder builder, string name, params object[] args) where T : class, IHealthCheck
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return AddTypeActivatedCheck<T>(builder, name, failureStatus: null, tags: null);
}
/// <summary>
/// Adds a new type activated health check with the specified name and implementation.
/// </summary>
/// <typeparam name="T">The health check implementation type.</typeparam>
/// <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="args">Additional arguments to provide to the constructor.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
/// </remarks>
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(
this IHealthChecksBuilder builder,
string name,
HealthStatus? failureStatus,
params object[] args) where T : class, IHealthCheck
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return AddTypeActivatedCheck<T>(builder, name, failureStatus, tags: null);
}
/// <summary>
/// Adds a new type activated health check with the specified name and implementation.
/// </summary>
/// <typeparam name="T">The health check implementation type.</typeparam>
/// <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="args">Additional arguments to provide to the constructor.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
/// </remarks>
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(
this IHealthChecksBuilder builder,
string name,
HealthStatus? failureStatus,
IEnumerable<string> tags,
params object[] args) where T : class, IHealthCheck
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.CreateInstance<T>(s, args), failureStatus, tags));
}
}
}

View File

@ -0,0 +1,169 @@
// 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;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Provides extension methods for registering delegates with the <see cref="IHealthChecksBuilder"/>.
/// </summary>
public static class HealthChecksBuilderDelegateExtensions
{
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </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>
public static IHealthChecksBuilder AddCheck(
this IHealthChecksBuilder builder,
string name,
Func<HealthCheckResult> check,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (check == null)
{
throw new ArgumentNullException(nameof(check));
}
var instance = new DelegateHealthCheck((ct) => Task.FromResult(check()));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
}
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </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>
public static IHealthChecksBuilder AddCheck(
this IHealthChecksBuilder builder,
string name,
Func<CancellationToken, HealthCheckResult> check,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (check == null)
{
throw new ArgumentNullException(nameof(check));
}
var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct)));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
}
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </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>
public static IHealthChecksBuilder AddAsyncCheck(
this IHealthChecksBuilder builder,
string name,
Func<Task<HealthCheckResult>> check,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (check == null)
{
throw new ArgumentNullException(nameof(check));
}
var instance = new DelegateHealthCheck((ct) => check());
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
}
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </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>
public static IHealthChecksBuilder AddAsyncCheck(
this IHealthChecksBuilder builder,
string name,
Func<CancellationToken, Task<HealthCheckResult>> check,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (check == null)
{
throw new ArgumentNullException(nameof(check));
}
var instance = new DelegateHealthCheck((ct) => check(ct));
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
}
}
}

View File

@ -0,0 +1,24 @@
// 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.Diagnostics.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// A builder used to register health checks.
/// </summary>
public interface IHealthChecksBuilder
{
/// <summary>
/// Adds a <see cref="HealthCheckRegistration"/> for a health check.
/// </summary>
/// <param name="registration">The <see cref="HealthCheckRegistration"/>.</param>
IHealthChecksBuilder Add(HealthCheckRegistration registration);
/// <summary>
/// Gets the <see cref="IServiceCollection"/> into which <see cref="IHealthCheck"/> instances should be registered.
/// </summary>
IServiceCollection Services { get; }
}
}

View File

@ -2,143 +2,60 @@
// 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.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
internal class HealthCheckService : IHealthCheckService
/// <summary>
/// A service which can be used to check the status of <see cref="IHealthCheck"/> instances
/// registered in the application.
/// </summary>
/// <remarks>
/// <para>
/// The default implementation of <see cref="HealthCheckService"/> is registered in the dependency
/// injection container as a singleton service by calling
/// <see cref="HealthCheckServiceCollectionExtensions.AddHealthChecks(IServiceCollection)"/>.
/// </para>
/// <para>
/// The <see cref="IHealthChecksBuilder"/> returned by
/// <see cref="HealthCheckServiceCollectionExtensions.AddHealthChecks(IServiceCollection)"/>
/// provides a convenience API for registering health checks.
/// </para>
/// <para>
/// <see cref="IHealthCheck"/> implementations can be registered through extension methods provided by
/// <see cref="IHealthChecksBuilder"/>.
/// </para>
/// </remarks>
public abstract class HealthCheckService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<HealthCheckService> _logger;
public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger<HealthCheckService> logger)
/// <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="HealthReport"/> containing the results.
/// </returns>
public Task<HealthReport> CheckHealthAsync(CancellationToken cancellationToken = default)
{
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// We're specifically going out of our way to do this at startup time. We want to make sure you
// get any kind of health-check related error as early as possible. Waiting until someone
// actually tries to **run** health checks would be real baaaaad.
using (var scope = _scopeFactory.CreateScope())
{
var healthChecks = scope.ServiceProvider.GetRequiredService<IEnumerable<IHealthCheck>>();
EnsureNoDuplicates(healthChecks);
}
return CheckHealthAsync(predicate: null, cancellationToken);
}
public Task<CompositeHealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default) =>
CheckHealthAsync(predicate: null, cancellationToken);
public async Task<CompositeHealthCheckResult> CheckHealthAsync(
Func<IHealthCheck, bool> predicate,
CancellationToken cancellationToken = default)
{
using (var scope = _scopeFactory.CreateScope())
{
var healthChecks = scope.ServiceProvider.GetRequiredService<IEnumerable<IHealthCheck>>();
var results = new Dictionary<string, HealthCheckResult>(StringComparer.OrdinalIgnoreCase);
foreach (var healthCheck in healthChecks)
{
if (predicate != null && !predicate(healthCheck))
{
continue;
}
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(healthCheck.Name)))
{
HealthCheckResult result;
try
{
Log.HealthCheckBegin(_logger, healthCheck);
var stopwatch = ValueStopwatch.StartNew();
result = await healthCheck.CheckHealthAsync(cancellationToken);
Log.HealthCheckEnd(_logger, healthCheck, result, stopwatch.GetElapsedTime());
}
catch (Exception ex)
{
Log.HealthCheckError(_logger, healthCheck, ex);
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.
throw new InvalidOperationException($"Health check '{healthCheck.Name}' returned a result with a status of Unknown");
}
results[healthCheck.Name] = result;
}
}
return new CompositeHealthCheckResult(results);
}
}
private static void EnsureNoDuplicates(IEnumerable<IHealthCheck> healthChecks)
{
// Scan the list for duplicate names to provide a better error if there are duplicates.
var duplicateNames = healthChecks
.GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
if (duplicateNames.Count > 0)
{
throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(healthChecks));
}
}
private static class Log
{
public static class EventIds
{
public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin");
public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd");
public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError");
}
private static readonly Action<ILogger, string, Exception> _healthCheckBegin = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.HealthCheckBegin,
"Running health check {HealthCheckName}");
private static readonly Action<ILogger, string, double, HealthCheckStatus, Exception> _healthCheckEnd = LoggerMessage.Define<string, double, HealthCheckStatus>(
LogLevel.Debug,
EventIds.HealthCheckEnd,
"Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}");
private static readonly Action<ILogger, string, Exception> _healthCheckError = LoggerMessage.Define<string>(
LogLevel.Error,
EventIds.HealthCheckError,
"Health check {HealthCheckName} threw an unhandled exception");
public static void HealthCheckBegin(ILogger logger, IHealthCheck healthCheck)
{
_healthCheckBegin(logger, healthCheck.Name, null);
}
public static void HealthCheckEnd(ILogger logger, IHealthCheck healthCheck, HealthCheckResult result, TimeSpan duration)
{
_healthCheckEnd(logger, healthCheck.Name, duration.TotalMilliseconds, result.Status, null);
}
public static void HealthCheckError(ILogger logger, IHealthCheck healthCheck, Exception exception)
{
_healthCheckError(logger, healthCheck.Name, exception);
}
}
/// <summary>
/// Runs the provided health checks and returns the aggregated status
/// </summary>
/// <param name="predicate">
/// A predicate that can be used to include health checks based on user-defined criteria.
/// </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="HealthReport"/> containing the results.
/// </returns>
public abstract Task<HealthReport> CheckHealthAsync(
Func<HealthCheckRegistration, bool> predicate,
CancellationToken cancellationToken = default);
}
}

View File

@ -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 System.Collections.Generic;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Options for the default implementation of <see cref="HealthCheckService"/>
/// </summary>
public sealed class HealthCheckServiceOptions
{
/// <summary>
/// Gets the health check registrations.
/// </summary>
public ICollection<HealthCheckRegistration> Registrations { get; } = new List<HealthCheckRegistration>();
}
}

View File

@ -1,17 +0,0 @@
// 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;
}
}
}

View File

@ -1,114 +0,0 @@
// 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)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (check == null)
{
throw new ArgumentNullException(nameof(check));
}
return builder.AddCheck(new HealthCheck(name, check));
}
/// <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)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (check == null)
{
throw new ArgumentNullException(nameof(check));
}
return builder.AddCheck(name, _ => check());
}
/// <summary>
/// Adds a new health check with the provided implementation.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add the check to.</param>
/// <param name="check">An <see cref="IHealthCheck"/> implementation.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, IHealthCheck check)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (check == null)
{
throw new ArgumentNullException(nameof(check));
}
builder.Services.AddSingleton<IHealthCheck>(check);
return builder;
}
/// <summary>
/// Adds a new health check as a transient dependency injected service with the provided type.
/// </summary>
/// <typeparam name="T">The health check implementation type.</typeparam>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// This method will register a transient service of type <see cref="IHealthCheck"/> with the
/// provided implementation type <typeparamref name="T"/>. Using this method to register a health
/// check allows you to register a health check that depends on transient and scoped services.
/// </remarks>
public static IHealthChecksBuilder AddCheck<T>(this IHealthChecksBuilder builder) where T : class, IHealthCheck
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.Add(ServiceDescriptor.Transient(typeof(IHealthCheck), typeof(T)));
return builder;
}
}
}

View File

@ -0,0 +1,60 @@
// 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;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Represents the result of executing a group of <see cref="IHealthCheck"/> instances.
/// </summary>
public sealed class HealthReport
{
/// <summary>
/// Create a new <see cref="HealthReport"/> from the specified results.
/// </summary>
/// <param name="entries">A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.</param>
public HealthReport(IReadOnlyDictionary<string, HealthReportEntry> entries)
{
Entries = entries;
Status = CalculateAggregateStatus(entries.Values);
}
/// <summary>
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.
/// </summary>
/// <remarks>
/// The keys in this dictionary map the name of each executed health check to a <see cref="HealthReportEntry"/> for the
/// result data retruned from the corresponding health check.
/// </remarks>
public IReadOnlyDictionary<string, HealthReportEntry> Entries { get; }
/// <summary>
/// Gets a <see cref="HealthStatus"/> representing the aggregate status of all the health checks. The value of <see cref="Status"/>
/// will be the most servere status reported by a health check. If no checks were executed, the value is always <see cref="HealthStatus.Healthy"/>.
/// </summary>
public HealthStatus Status { get; }
private HealthStatus CalculateAggregateStatus(IEnumerable<HealthReportEntry> entries)
{
// This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list
var currentValue = HealthStatus.Healthy;
foreach (var entry in entries)
{
if (currentValue > entry.Status)
{
currentValue = entry.Status;
}
if (currentValue == HealthStatus.Failed)
{
// Game over, man! Game over!
// (We hit the worst possible status, so there's no need to keep iterating)
return currentValue;
}
}
return currentValue;
}
}
}

View File

@ -0,0 +1,53 @@
// 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 an entry in a <see cref="HealthReport"/>. Corresponds to the result of a single <see cref="IHealthCheck"/>.
/// </summary>
public struct HealthReportEntry
{
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
/// <summary>
/// Creates a new <see cref="HealthReportEntry"/> with the specified values for <paramref name="status"/>, <paramref name="exception"/>,
/// <paramref name="description"/>, and <paramref name="data"/>.
/// </summary>
/// <param name="status">A value indicating the health 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 HealthReportEntry(HealthStatus status, string description, Exception exception, IReadOnlyDictionary<string, object> data)
{
Status = status;
Description = description;
Exception = exception;
Data = data ?? _emptyReadOnlyDictionary;
}
/// <summary>
/// Gets additional key-value pairs describing the health of the component.
/// </summary>
public IReadOnlyDictionary<string, object> Data { get; }
/// <summary>
/// Gets a human-readable description of the status of the component that was checked.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets an <see cref="System.Exception"/> representing the exception that was thrown when checking for status (if any).
/// </summary>
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"/>.
/// </summary>
public HealthStatus Status { get; }
}
}

View File

@ -1,60 +0,0 @@
// 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 service which can be used to check the status of <see cref="IHealthCheck"/> instances
/// registered in the application.
/// </summary>
/// <remarks>
/// <para>
/// The default implementation of <see cref="IHealthCheckService"/> is registered in the dependency
/// injection container as a singleton service by calling
/// <see cref="DependencyInjection.HealthCheckServiceCollectionExtensions.AddHealthChecks(DependencyInjection.IServiceCollection)"/>.
/// </para>
/// <para>
/// The <see cref="IHealthChecksBuilder"/> returned by
/// <see cref="DependencyInjection.HealthCheckServiceCollectionExtensions.AddHealthChecks(DependencyInjection.IServiceCollection)"/>
/// provides a convenience API for registering health checks.
/// </para>
/// <para>
/// The default implementation of <see cref="IHealthCheckService"/> will use all services
/// of type <see cref="IHealthCheck"/> registered in the dependency injection container. <see cref="IHealthCheck"/>
/// implementations may be registered with any service lifetime. The implementation will create a scope
/// for each aggregate health check operation and use the scope to resolve services. The scope
/// created for executing health checks is controlled by the health checks service and does not
/// share scoped services with any other scope in the application.
/// </para>
/// </remarks>
public interface IHealthCheckService
{
/// <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="predicate">
/// A predicate that can be used to include health checks based on user-defined criteria.
/// </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(Func<IHealthCheck, bool> predicate,
CancellationToken cancellationToken = default);
}
}

View File

@ -1,22 +0,0 @@
// 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; }
}
}

View File

@ -3,8 +3,8 @@
<Description>Components for performing health checks in .NET applications
Commonly Used Types:
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
@ -14,6 +14,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.ValueStopwatch.Sources" Version="$(MicrosoftExtensionsValueStopwatchSourcesPackageVersion)" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

View File

@ -1,8 +1,6 @@
// 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;
@ -100,7 +98,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task StatusCodeIs200IfAllChecksHealthy()
{
@ -112,9 +109,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.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!")));
.AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!"))
.AddCheck("Bar", () => HealthCheckResult.Passed("A-ok!"))
.AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!"));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -137,9 +134,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.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!")));
.AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!"))
.AddCheck("Bar", () => HealthCheckResult.Failed("Not so great."), failureStatus: HealthStatus.Degraded)
.AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!"));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -162,9 +159,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.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!")));
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -187,9 +184,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.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!")));
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddAsyncCheck("Bar", () => throw null)
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -225,9 +222,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.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!")));
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -254,9 +251,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.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!")));
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
@ -277,7 +274,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
ResultStatusCodes =
{
[HealthCheckStatus.Healthy] = 201,
[HealthStatus.Healthy] = 201,
}
});
})
@ -308,10 +305,10 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
// Will get filtered out
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!")))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("A-ok!")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();

View File

@ -1,31 +0,0 @@
// 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);
}
}
}

View File

@ -8,11 +8,12 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
public class HealthCheckServiceTests
public class DefaultHealthCheckServiceTest
{
[Fact]
public void Constructor_ThrowsUsefulExceptionForDuplicateNames()
@ -25,22 +26,23 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
serviceCollection.AddLogging();
serviceCollection.AddOptions();
serviceCollection.AddHealthChecks()
.AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())));
.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())));
var services = serviceCollection.BuildServiceProvider();
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
var logger = services.GetRequiredService<ILogger<HealthCheckService>>();
var options = services.GetRequiredService<IOptions<HealthCheckServiceOptions>>();
var logger = services.GetRequiredService<ILogger<DefaultHealthCheckService>>();
// Act
var exception = Assert.Throws<ArgumentException>(() => new HealthCheckService(scopeFactory, logger));
var exception = Assert.Throws<ArgumentException>(() => new DefaultHealthCheckService(scopeFactory, options, logger));
// Assert
Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message);
Assert.StartsWith($"Duplicate health checks were registered with the name(s): Foo, Baz", exception.Message);
}
[Fact]
@ -61,21 +63,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var service = CreateHealthChecksService(b =>
{
b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
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)));
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(results.Results,
Assert.Collection(results.Entries,
actual =>
{
Assert.Equal("HealthyCheck", actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
Assert.Collection(actual.Value.Data, item =>
{
@ -87,7 +89,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
Assert.Equal("DegradedCheck", actual.Key);
Assert.Equal(DegradedMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status);
Assert.Equal(HealthStatus.Degraded, actual.Value.Status);
Assert.Null(actual.Value.Exception);
Assert.Empty(actual.Value.Data);
},
@ -95,7 +97,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
Assert.Equal("UnhealthyCheck", actual.Key);
Assert.Equal(UnhealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Unhealthy, actual.Value.Status);
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
Assert.Same(exception, actual.Value.Exception);
Assert.Empty(actual.Value.Data);
});
@ -119,21 +121,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var service = CreateHealthChecksService(b =>
{
b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
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)));
});
// Act
var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck");
// Assert
Assert.Collection(results.Results,
Assert.Collection(results.Entries,
actual =>
{
Assert.Equal("HealthyCheck", actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
Assert.Collection(actual.Value.Data, item =>
{
@ -144,7 +146,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
}
[Fact]
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckerToFailedResultAsync()
public async Task CheckHealthAsync_SetsRegistrationForEachCheck()
{
// Arrange
var thrownException = new InvalidOperationException("Whoops!");
@ -152,35 +154,79 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var service = CreateHealthChecksService(b =>
{
b.AddCheck("Throws", ct => throw thrownException);
b.AddCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException));
b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy()));
b.AddCheck<NameCapturingCheck>("A");
b.AddCheck<NameCapturingCheck>("B");
b.AddCheck<NameCapturingCheck>("C");
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(results.Results,
Assert.Collection(
results.Entries,
actual =>
{
Assert.Equal("A", actual.Key);
Assert.Collection(
actual.Value.Data,
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "A")));
},
actual =>
{
Assert.Equal("B", actual.Key);
Assert.Collection(
actual.Value.Data,
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "B")));
},
actual =>
{
Assert.Equal("C", actual.Key);
Assert.Collection(
actual.Value.Data,
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "C")));
});
}
[Fact]
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToFailedResultAsync()
{
// Arrange
var thrownException = new InvalidOperationException("Whoops!");
var faultedException = new InvalidOperationException("Ohnoes!");
var service = CreateHealthChecksService(b =>
{
b.AddAsyncCheck("Throws", ct => throw thrownException);
b.AddAsyncCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException));
b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Passed()));
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(
results.Entries,
actual =>
{
Assert.Equal("Throws", actual.Key);
Assert.Equal(thrownException.Message, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status);
Assert.Equal(HealthStatus.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.Equal(HealthStatus.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.Description);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
});
}
@ -190,12 +236,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
// Arrange
var sink = new TestSink();
var check = new HealthCheck("TestScope", cancellationToken =>
var check = new DelegateHealthCheck(cancellationToken =>
{
Assert.Collection(sink.Scopes,
actual =>
{
Assert.Equal(actual.LoggerName, typeof(HealthCheckService).FullName);
Assert.Equal(actual.LoggerName, typeof(DefaultHealthCheckService).FullName);
Assert.Collection((IEnumerable<KeyValuePair<string, object>>)actual.Scope,
item =>
{
@ -203,7 +249,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
Assert.Equal("TestScope", item.Value);
});
});
return Task.FromResult(HealthCheckResult.Healthy());
return Task.FromResult(HealthCheckResult.Passed());
});
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
@ -212,36 +258,20 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Override the logger factory for testing
b.Services.AddSingleton<ILoggerFactory>(loggerFactory);
b.AddCheck(check);
b.AddCheck("TestScope", check);
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(results.Results, actual =>
Assert.Collection(results.Entries, actual =>
{
Assert.Equal("TestScope", actual.Key);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
});
}
[Fact]
public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult()
{
// Arrange
var service = CreateHealthChecksService(b =>
{
b.AddCheck("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);
}
[Fact]
public async Task CheckHealthAsync_CheckCanDependOnTransientService()
{
@ -250,7 +280,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
b.Services.AddTransient<AnotherService>();
b.AddCheck<CheckWithServiceDependency>();
b.AddCheck<CheckWithServiceDependency>("Test");
});
// Act
@ -258,11 +288,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Assert
Assert.Collection(
results.Results,
results.Entries,
actual =>
{
Assert.Equal("Test", actual.Key);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
});
}
@ -274,7 +304,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
b.Services.AddScoped<AnotherService>();
b.AddCheck<CheckWithServiceDependency>();
b.AddCheck<CheckWithServiceDependency>("Test");
});
// Act
@ -282,11 +312,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Assert
Assert.Collection(
results.Results,
results.Entries,
actual =>
{
Assert.Equal("Test", actual.Key);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
});
}
@ -298,7 +328,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
b.Services.AddSingleton<AnotherService>();
b.AddCheck<CheckWithServiceDependency>();
b.AddCheck<CheckWithServiceDependency>("Test");
});
// Act
@ -306,15 +336,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Assert
Assert.Collection(
results.Results,
results.Entries,
actual =>
{
Assert.Equal("Test", actual.Key);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
});
}
private static HealthCheckService CreateHealthChecksService(Action<IHealthChecksBuilder> configure)
private static DefaultHealthCheckService CreateHealthChecksService(Action<IHealthChecksBuilder> configure)
{
var services = new ServiceCollection();
services.AddLogging();
@ -326,7 +356,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
configure(builder);
}
return (HealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService<IHealthCheckService>();
return (DefaultHealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService<HealthCheckService>();
}
private class AnotherService { }
@ -336,12 +366,22 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public CheckWithServiceDependency(AnotherService _)
{
}
public string Name => "Test";
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default)
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
return Task.FromResult(HealthCheckResult.Healthy());
return Task.FromResult(HealthCheckResult.Passed());
}
}
private class NameCapturingCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var data = new Dictionary<string, object>()
{
{ "name", context.Registration.Name },
};
return Task.FromResult(HealthCheckResult.Passed(data: data));
}
}
}

View File

@ -0,0 +1,257 @@
// 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;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.Extensions.DependencyInjection
{
// Integration tests for extension methods on IHealthCheckBuilder
//
// We test the longest overload of each 'family' of Add...Check methods, since they chain to each other.
public class HealthChecksBuilderTest
{
[Fact]
public void AddCheck_Instance()
{
// Arrange
var instance = new DelegateHealthCheck((_) =>
{
return Task.FromResult(HealthCheckResult.Passed());
});
var services = CreateServices();
services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded,tags: new[] { "tag", }, instance: instance);
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.Same(instance, registration.Factory(serviceProvider));
}
[Fact]
public void AddCheck_T_TypeActivated()
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddCheck<TestHealthCheck>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<TestHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void AddCheck_T_Service()
{
// Arrange
var instance = new TestHealthCheck();
var services = CreateServices();
services.AddSingleton(instance);
services.AddHealthChecks().AddCheck<TestHealthCheck>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.Same(instance, registration.Factory(serviceProvider));
}
[Fact]
public void AddTypeActivatedCheck()
{
// Arrange
var services = CreateServices();
services
.AddHealthChecks()
.AddTypeActivatedCheck<TestHealthCheckWithArgs>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, args: new object[] { 5, "hi", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
var check = Assert.IsType<TestHealthCheckWithArgs>(registration.Factory(serviceProvider));
Assert.Equal(5, check.I);
Assert.Equal("hi", check.S);
}
[Fact]
public void AddDelegateCheck_NoArg()
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, check: () =>
{
return HealthCheckResult.Passed();
});
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void AddDelegateCheck_CancellationToken()
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddCheck("test", (_) =>
{
return HealthCheckResult.Passed();
}, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void AddAsyncDelegateCheck_NoArg()
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddAsyncCheck("test", () =>
{
return Task.FromResult(HealthCheckResult.Passed());
}, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void AddAsyncDelegateCheck_CancellationToken()
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddAsyncCheck("test", (_) =>
{
return Task.FromResult(HealthCheckResult.Passed());
}, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks()
{
var services = new ServiceCollection();
services
.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed()));
services
.AddHealthChecks()
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Passed()));
// Act
var options = services.BuildServiceProvider().GetRequiredService<IOptions<HealthCheckServiceOptions>>();
// Assert
Assert.Collection(
options.Value.Registrations,
actual => Assert.Equal("Foo", actual.Name),
actual => Assert.Equal("Bar", actual.Name));
}
private IServiceCollection CreateServices()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddOptions();
return services;
}
private class TestHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
}
private class TestHealthCheckWithArgs : IHealthCheck
{
public TestHealthCheckWithArgs(int i, string s)
{
I = i;
S = s;
}
public int I { get; set; }
public string S { get; set; }
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
}
}
}

View File

@ -1,12 +1,12 @@
// 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 Microsoft.Extensions.Diagnostics.HealthChecks;
using Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
namespace Microsoft.Extensions.DependencyInjection
{
public class ServiceCollectionExtensionsTests
public class ServiceCollectionExtensionsTest
{
[Fact]
public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently()
@ -23,8 +23,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
actual =>
{
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
Assert.Equal(typeof(IHealthCheckService), actual.ServiceType);
Assert.Equal(typeof(HealthCheckService), actual.ImplementationType);
Assert.Equal(typeof(HealthCheckService), actual.ServiceType);
Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType);
Assert.Null(actual.ImplementationInstance);
Assert.Null(actual.ImplementationFactory);
});

View File

@ -1,33 +0,0 @@
// 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.Linq;
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 checks = services.BuildServiceProvider().GetRequiredService<IEnumerable<IHealthCheck>>();
// Assert
Assert.Collection(
checks,
actual => Assert.Equal("Foo", actual.Name),
actual => Assert.Equal("Bar", actual.Name));
}
}
}

View File

@ -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
{
public class HealthReportTest
{
[Theory]
[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>()
{
{"Foo", new HealthReportEntry(HealthStatus.Healthy, null, null, null) },
{"Bar", new HealthReportEntry(HealthStatus.Healthy, null, null, null) },
{"Baz", new HealthReportEntry(status, exception: null, description: null, data: null) },
{"Quick", new HealthReportEntry(HealthStatus.Healthy, null, null, null) },
{"Quack", new HealthReportEntry(HealthStatus.Healthy, null, null, null) },
{"Quock", new HealthReportEntry(HealthStatus.Healthy, null, null, null) },
});
Assert.Equal(status, result.Status);
}
}
}

View File

@ -8,7 +8,6 @@
<FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
<ExperimentalVersionPrefix>0.6.0</ExperimentalVersionPrefix>
<ExperimentalVersionSuffix>alpha1</ExperimentalVersionSuffix>
<ExperimentalPackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(ExperimentalVersionSuffix)' == 'rtm' ">$(ExperimentalVersionPrefix)</ExperimentalPackageVersion>