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) public void ConfigureServices(IServiceCollection services)
{ {
// Registers required services for health checks // Registers required services for health checks
services.AddHealthChecks(); services.AddHealthChecks()
// This is an example of registering a custom health check as a service. // Registers a custom health check implementation
// All IHealthCheck services will be available to the health check service and .AddGCInfoCheck("GCInfo");
// middleware.
//
// We recommend registering all health checks as Singleton services.
services.AddSingleton<IHealthCheck, GCInfoHealthCheck>();
} }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) 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"; httpContext.Response.ContentType = "application/json";
var json = new JObject( var json = new JObject(
new JProperty("status", result.Status.ToString()), 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(pair.Key, new JObject(
new JProperty("status", pair.Value.Status.ToString()), new JProperty("status", pair.Value.Status.ToString()),
new JProperty("description", pair.Value.Description), new JProperty("description", pair.Value.Description),

View File

@ -22,7 +22,7 @@ namespace HealthChecksSample
// Registers required services for health checks // Registers required services for health checks
services.AddHealthChecks() services.AddHealthChecks()
// Add a health check for a SQL database // 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) public void Configure(IApplicationBuilder app, IHostingEnvironment env)

View File

@ -11,20 +11,17 @@ namespace HealthChecksSample
{ {
public abstract class DbConnectionHealthCheck : IHealthCheck public abstract class DbConnectionHealthCheck : IHealthCheck
{ {
protected DbConnectionHealthCheck(string name, string connectionString) protected DbConnectionHealthCheck(string connectionString)
: this(name, connectionString, testQuery: null) : 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)); ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
TestQuery = testQuery; TestQuery = testQuery;
} }
public string Name { get; }
protected string ConnectionString { get; } protected string ConnectionString { get; }
// This sample supports specifying a query to run as a boolean test of whether the database // 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); 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)) using (var connection = CreateConnection(ConnectionString))
{ {
@ -54,11 +51,11 @@ namespace HealthChecksSample
} }
catch (DbException ex) 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.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
namespace HealthChecksSample namespace HealthChecksSample
{ {
// This is an example of a custom health check that implements IHealthCheck. // 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 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 // 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. // Additionally we include some GC info in the reported diagnostics.
var allocated = GC.GetTotalMemory(forceFullCollection: false); var allocated = GC.GetTotalMemory(forceFullCollection: false);
@ -28,14 +65,20 @@ namespace HealthChecksSample
{ "Gen2Collections", GC.CollectionCount(2) }, { "Gen2Collections", GC.CollectionCount(2) },
}; };
// Report degraded status if the allocated memory is >= 1gb (in bytes) // Report failure if the allocated memory is >= the threshold
var status = allocated >= 1024 * 1024 * 1024 ? HealthCheckStatus.Degraded : HealthCheckStatus.Healthy; var result = allocated >= options.Threshold;
return Task.FromResult(new HealthCheckResult( return Task.FromResult(new HealthCheckResult(
status, result,
exception: null,
description: "reports degraded status if allocated bytes >= 1gb", description: "reports degraded status if allocated bytes >= 1gb",
exception: null,
data: data)); 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace HealthChecksSample namespace HealthChecksSample
{ {
@ -17,7 +15,7 @@ namespace HealthChecksSample
// Registers required services for health checks // Registers required services for health checks
services services
.AddHealthChecks() .AddHealthChecks()
.AddCheck(new SlowDependencyHealthCheck()); .AddCheck<SlowDependencyHealthCheck>("Slow", failureStatus: null, tags: new[] { "ready", });
} }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) public void Configure(IApplicationBuilder app, IHostingEnvironment env)
@ -46,10 +44,13 @@ namespace HealthChecksSample
// long initialization time (15 seconds). // long initialization time (15 seconds).
// The readiness check uses all of the registered health checks (default) // The readiness check uses all registered checks with the 'ready' tag.
app.UseHealthChecks("/health/ready"); 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() app.UseHealthChecks("/health/live", new HealthCheckOptions()
{ {
// Exclude all checks, just return a 200. // Exclude all checks, just return a 200.

View File

@ -17,16 +17,14 @@ namespace HealthChecksSample
_task = Task.Delay(15 * 1000); _task = Task.Delay(15 * 1000);
} }
public string Name => HealthCheckName; public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
{ {
if (_task.IsCompleted) 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.Common;
using System.Data.SqlClient; using System.Data.SqlClient;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace HealthChecksSample namespace HealthChecksSample
{ {
@ -8,13 +7,13 @@ namespace HealthChecksSample
{ {
private static readonly string DefaultTestQuery = "Select 1"; private static readonly string DefaultTestQuery = "Select 1";
public SqlConnectionHealthCheck(string name, string connectionString) public SqlConnectionHealthCheck(string connectionString)
: this(name, connectionString, testQuery: DefaultTestQuery) : this(connectionString, testQuery: DefaultTestQuery)
{ {
} }
public SqlConnectionHealthCheck(string name, string connectionString, string testQuery) public SqlConnectionHealthCheck(string connectionString, string testQuery)
: base(name, connectionString, testQuery ?? DefaultTestQuery) : base(connectionString, testQuery ?? DefaultTestQuery)
{ {
} }

View File

@ -15,12 +15,12 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly HealthCheckOptions _healthCheckOptions; private readonly HealthCheckOptions _healthCheckOptions;
private readonly IHealthCheckService _healthCheckService; private readonly HealthCheckService _healthCheckService;
public HealthCheckMiddleware( public HealthCheckMiddleware(
RequestDelegate next, RequestDelegate next,
IOptions<HealthCheckOptions> healthCheckOptions, IOptions<HealthCheckOptions> healthCheckOptions,
IHealthCheckService healthCheckService) HealthCheckService healthCheckService)
{ {
if (next == null) if (next == null)
{ {
@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode))
{ {
var message = 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" + $"{nameof(HealthCheckOptions)}.{nameof(HealthCheckOptions.ResultStatusCodes)} must contain" +
$"an entry for {result.Status}."; $"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, /// registered health checks - this is the default behavior. To run a subset of health checks,
/// provide a function that filters the set of checks. /// provide a function that filters the set of checks.
/// </remarks> /// </remarks>
public Func<IHealthCheck, bool> Predicate { get; set; } public Func<HealthCheckRegistration, bool> Predicate { get; set; }
/// <summary> /// <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. /// This property can be used to configure the status codes returned for each status.
/// </summary> /// </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 }, { HealthStatus.Healthy, StatusCodes.Status200OK },
{ HealthCheckStatus.Degraded, StatusCodes.Status200OK }, { HealthStatus.Degraded, StatusCodes.Status200OK },
{ HealthCheckStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, { HealthStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable },
// This means that a health check failed, so 500 is appropriate. This is an error. // This means that a health check failed, so 500 is appropriate. This is an error.
{ HealthCheckStatus.Failed, StatusCodes.Status500InternalServerError }, { HealthStatus.Failed, StatusCodes.Status500InternalServerError },
}; };
/// <summary> /// <summary>
@ -43,8 +43,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The default value is a delegate that will write a minimal <c>text/plain</c> response with the value /// 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> /// </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 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"; httpContext.Response.ContentType = "text/plain";
return httpContext.Response.WriteAsync(result.Status.ToString()); 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 static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
private string _description;
private IReadOnlyDictionary<string, object> _data;
/// <summary> /// <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> /// </summary>
public HealthCheckStatus Status { get; } /// <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>
/// <summary> /// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
/// Gets an <see cref="Exception"/> representing the exception that was thrown when checking for status (if any). /// <param name="data">Additional key-value pairs describing the health of the component.</param>
/// </summary> public HealthCheckResult(bool result, string description, Exception exception, IReadOnlyDictionary<string, object> data)
/// <remarks> {
/// This value is expected to be 'null' if <see cref="Status"/> is <see cref="HealthCheckStatus.Healthy"/>. Result = result;
/// </remarks> Description = description;
public Exception Exception { get; } Exception = exception;
Data = data ?? _emptyReadOnlyDictionary;
/// <summary> }
/// Gets a human-readable description of the status of the component that was checked.
/// </summary>
public string Description => _description ?? string.Empty;
/// <summary> /// <summary>
/// Gets additional key-value pairs describing the health of the component. /// Gets additional key-value pairs describing the health of the component.
/// </summary> /// </summary>
public IReadOnlyDictionary<string, object> Data => _data ?? _emptyReadOnlyDictionary; public IReadOnlyDictionary<string, object> Data { get; }
/// <summary> /// <summary>
/// Creates a new <see cref="HealthCheckResult"/> with the specified <paramref name="status"/>, <paramref name="exception"/>, /// Gets a human-readable description of the status of the component that was checked.
/// <paramref name="description"/>, and <paramref name="data"/>.
/// </summary> /// </summary>
/// <param name="status">A <see cref="HealthCheckStatus"/> value indicating the status of the component that was checked.</param> public string Description { get; }
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
public HealthCheckResult(HealthCheckStatus status, Exception exception, string description, IReadOnlyDictionary<string, object> data)
{
if (status == HealthCheckStatus.Unknown)
{
throw new ArgumentException($"'{nameof(HealthCheckStatus.Unknown)}' is not a valid value for the 'status' parameter.", nameof(status));
}
Status = status; /// <summary>
Exception = exception; /// Gets an <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).
_description = description; /// </summary>
_data = data; 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> /// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component. /// Creates a <see cref="HealthCheckResult"/> representing an failing component.
/// </summary> /// </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. Optional.</param>
public static HealthCheckResult Unhealthy() /// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status. Optional.</param>
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: string.Empty, data: null); /// <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>
/// <summary> public static HealthCheckResult Failed(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component. {
/// </summary> return new HealthCheckResult(result: false, description, exception, data);
/// <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);
} }
} }

View File

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

View File

@ -12,16 +12,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
/// </summary> /// </summary>
public interface IHealthCheck public interface IHealthCheck
{ {
/// <summary>
/// Gets the name of the health check, which should indicate the component being checked.
/// </summary>
string Name { get; }
/// <summary> /// <summary>
/// Runs the health check, returning the status of the component being checked. /// Runs the health check, returning the status of the component being checked.
/// </summary> /// </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> /// <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> /// <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 /// A simple implementation of <see cref="IHealthCheck"/> which uses a provided delegate to
/// implement the check. /// implement the check.
/// </summary> /// </summary>
public sealed class HealthCheck : IHealthCheck internal sealed class DelegateHealthCheck : IHealthCheck
{ {
private readonly Func<CancellationToken, Task<HealthCheckResult>> _check; private readonly Func<CancellationToken, Task<HealthCheckResult>> _check;
/// <summary> /// <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> /// </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> /// <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)); _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> /// <summary>
/// Runs the health check, returning the status of the component being checked. /// Runs the health check, returning the status of the component being checked.
/// </summary> /// </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> /// <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> /// <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 namespace Microsoft.Extensions.DependencyInjection
{ {
/// <summary> /// <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> /// </summary>
public static class HealthCheckServiceCollectionExtensions public static class HealthCheckServiceCollectionExtensions
{ {
/// <summary> /// <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. /// health checks.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This operation is idempotent - multiple invocations will still only result in a single /// 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. /// multiple times in order to get access to the <see cref="IHealthChecksBuilder"/> in multiple places.
/// </remarks> /// </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> /// <returns>An instance of <see cref="IHealthChecksBuilder"/> from which health checks can be registered.</returns>
public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services) public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
{ {
services.TryAdd(ServiceDescriptor.Singleton<IHealthCheckService, HealthCheckService>()); services.TryAdd(ServiceDescriptor.Singleton<HealthCheckService, DefaultHealthCheckService>());
return new HealthChecksBuilder(services); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.Diagnostics.HealthChecks 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; /// <summary>
private readonly ILogger<HealthCheckService> _logger; /// Runs all the health checks in the application and returns the aggregated status.
/// </summary>
public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger<HealthCheckService> logger) /// <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)); return CheckHealthAsync(predicate: null, cancellationToken);
_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);
}
} }
public Task<CompositeHealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default) => /// <summary>
CheckHealthAsync(predicate: null, cancellationToken); /// Runs the provided health checks and returns the aggregated status
/// </summary>
public async Task<CompositeHealthCheckResult> CheckHealthAsync( /// <param name="predicate">
Func<IHealthCheck, bool> predicate, /// A predicate that can be used to include health checks based on user-defined criteria.
CancellationToken cancellationToken = default) /// </param>
{ /// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
using (var scope = _scopeFactory.CreateScope()) /// <returns>
{ /// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
var healthChecks = scope.ServiceProvider.GetRequiredService<IEnumerable<IHealthCheck>>(); /// yielding a <see cref="HealthReport"/> containing the results.
/// </returns>
var results = new Dictionary<string, HealthCheckResult>(StringComparer.OrdinalIgnoreCase); public abstract Task<HealthReport> CheckHealthAsync(
foreach (var healthCheck in healthChecks) Func<HealthCheckRegistration, bool> predicate,
{ CancellationToken cancellationToken = default);
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);
}
}
} }
} }

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

View File

@ -1,8 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -100,7 +98,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
} }
[Fact] [Fact]
public async Task StatusCodeIs200IfAllChecksHealthy() public async Task StatusCodeIs200IfAllChecksHealthy()
{ {
@ -112,9 +109,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!"))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) .AddCheck("Bar", () => HealthCheckResult.Passed("A-ok!"))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!"));
}); });
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient(); var client = server.CreateClient();
@ -137,9 +134,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!"))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Not so great."))) .AddCheck("Bar", () => HealthCheckResult.Failed("Not so great."), failureStatus: HealthStatus.Degraded)
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!"));
}); });
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient(); var client = server.CreateClient();
@ -162,9 +159,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad.")))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
}); });
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient(); var client = server.CreateClient();
@ -187,9 +184,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddCheck("Bar", () => Task.FromResult(new HealthCheckResult(HealthCheckStatus.Failed, null, null, null))) .AddAsyncCheck("Bar", () => throw null)
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
}); });
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient(); var client = server.CreateClient();
@ -225,9 +222,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad.")))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
}); });
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient(); var client = server.CreateClient();
@ -254,9 +251,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad.")))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
}); });
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient(); var client = server.CreateClient();
@ -277,7 +274,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{ {
ResultStatusCodes = ResultStatusCodes =
{ {
[HealthCheckStatus.Healthy] = 201, [HealthStatus.Healthy] = 201,
} }
}); });
}) })
@ -308,10 +305,10 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")))
// Will get filtered out // Will get filtered out
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("A-ok!")))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!")));
}); });
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient(); 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.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Xunit; using Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ {
public class HealthCheckServiceTests public class DefaultHealthCheckServiceTest
{ {
[Fact] [Fact]
public void Constructor_ThrowsUsefulExceptionForDuplicateNames() public void Constructor_ThrowsUsefulExceptionForDuplicateNames()
@ -25,22 +26,23 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
serviceCollection.AddLogging(); serviceCollection.AddLogging();
serviceCollection.AddOptions(); serviceCollection.AddOptions();
serviceCollection.AddHealthChecks() serviceCollection.AddHealthChecks()
.AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())))
.AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())))
.AddCheck(new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())))
.AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())))
.AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))); .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed())));
var services = serviceCollection.BuildServiceProvider(); var services = serviceCollection.BuildServiceProvider();
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>(); var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
var logger = services.GetRequiredService<ILogger<HealthCheckService>>(); var options = services.GetRequiredService<IOptions<HealthCheckServiceOptions>>();
var logger = services.GetRequiredService<ILogger<DefaultHealthCheckService>>();
// Act // Act
var exception = Assert.Throws<ArgumentException>(() => new HealthCheckService(scopeFactory, logger)); var exception = Assert.Throws<ArgumentException>(() => new DefaultHealthCheckService(scopeFactory, options, logger));
// Assert // 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] [Fact]
@ -61,21 +63,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var service = CreateHealthChecksService(b => var service = CreateHealthChecksService(b =>
{ {
b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data)));
b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded);
b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception)));
}); });
// Act // Act
var results = await service.CheckHealthAsync(); var results = await service.CheckHealthAsync();
// Assert // Assert
Assert.Collection(results.Results, Assert.Collection(results.Entries,
actual => actual =>
{ {
Assert.Equal("HealthyCheck", actual.Key); Assert.Equal("HealthyCheck", actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description); 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.Null(actual.Value.Exception);
Assert.Collection(actual.Value.Data, item => Assert.Collection(actual.Value.Data, item =>
{ {
@ -87,7 +89,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ {
Assert.Equal("DegradedCheck", actual.Key); Assert.Equal("DegradedCheck", actual.Key);
Assert.Equal(DegradedMessage, actual.Value.Description); 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.Null(actual.Value.Exception);
Assert.Empty(actual.Value.Data); Assert.Empty(actual.Value.Data);
}, },
@ -95,7 +97,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ {
Assert.Equal("UnhealthyCheck", actual.Key); Assert.Equal("UnhealthyCheck", actual.Key);
Assert.Equal(UnhealthyMessage, actual.Value.Description); 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.Same(exception, actual.Value.Exception);
Assert.Empty(actual.Value.Data); Assert.Empty(actual.Value.Data);
}); });
@ -119,21 +121,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var service = CreateHealthChecksService(b => var service = CreateHealthChecksService(b =>
{ {
b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data)));
b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded);
b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception)));
}); });
// Act // Act
var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck"); var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck");
// Assert // Assert
Assert.Collection(results.Results, Assert.Collection(results.Entries,
actual => actual =>
{ {
Assert.Equal("HealthyCheck", actual.Key); Assert.Equal("HealthyCheck", actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description); 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.Null(actual.Value.Exception);
Assert.Collection(actual.Value.Data, item => Assert.Collection(actual.Value.Data, item =>
{ {
@ -144,7 +146,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
} }
[Fact] [Fact]
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckerToFailedResultAsync() public async Task CheckHealthAsync_SetsRegistrationForEachCheck()
{ {
// Arrange // Arrange
var thrownException = new InvalidOperationException("Whoops!"); var thrownException = new InvalidOperationException("Whoops!");
@ -152,35 +154,79 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var service = CreateHealthChecksService(b => var service = CreateHealthChecksService(b =>
{ {
b.AddCheck("Throws", ct => throw thrownException); b.AddCheck<NameCapturingCheck>("A");
b.AddCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException)); b.AddCheck<NameCapturingCheck>("B");
b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())); b.AddCheck<NameCapturingCheck>("C");
}); });
// Act // Act
var results = await service.CheckHealthAsync(); var results = await service.CheckHealthAsync();
// Assert // 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 => actual =>
{ {
Assert.Equal("Throws", actual.Key); Assert.Equal("Throws", actual.Key);
Assert.Equal(thrownException.Message, actual.Value.Description); 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); Assert.Same(thrownException, actual.Value.Exception);
}, },
actual => actual =>
{ {
Assert.Equal("Faults", actual.Key); Assert.Equal("Faults", actual.Key);
Assert.Equal(faultedException.Message, actual.Value.Description); 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); Assert.Same(faultedException, actual.Value.Exception);
}, },
actual => actual =>
{ {
Assert.Equal("Succeeds", actual.Key); Assert.Equal("Succeeds", actual.Key);
Assert.Empty(actual.Value.Description); Assert.Null(actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception); Assert.Null(actual.Value.Exception);
}); });
} }
@ -190,12 +236,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ {
// Arrange // Arrange
var sink = new TestSink(); var sink = new TestSink();
var check = new HealthCheck("TestScope", cancellationToken => var check = new DelegateHealthCheck(cancellationToken =>
{ {
Assert.Collection(sink.Scopes, Assert.Collection(sink.Scopes,
actual => actual =>
{ {
Assert.Equal(actual.LoggerName, typeof(HealthCheckService).FullName); Assert.Equal(actual.LoggerName, typeof(DefaultHealthCheckService).FullName);
Assert.Collection((IEnumerable<KeyValuePair<string, object>>)actual.Scope, Assert.Collection((IEnumerable<KeyValuePair<string, object>>)actual.Scope,
item => item =>
{ {
@ -203,7 +249,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
Assert.Equal("TestScope", item.Value); Assert.Equal("TestScope", item.Value);
}); });
}); });
return Task.FromResult(HealthCheckResult.Healthy()); return Task.FromResult(HealthCheckResult.Passed());
}); });
var loggerFactory = new TestLoggerFactory(sink, enabled: true); var loggerFactory = new TestLoggerFactory(sink, enabled: true);
@ -212,36 +258,20 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Override the logger factory for testing // Override the logger factory for testing
b.Services.AddSingleton<ILoggerFactory>(loggerFactory); b.Services.AddSingleton<ILoggerFactory>(loggerFactory);
b.AddCheck(check); b.AddCheck("TestScope", check);
}); });
// Act // Act
var results = await service.CheckHealthAsync(); var results = await service.CheckHealthAsync();
// Assert // Assert
Assert.Collection(results.Results, actual => Assert.Collection(results.Entries, actual =>
{ {
Assert.Equal("TestScope", actual.Key); 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] [Fact]
public async Task CheckHealthAsync_CheckCanDependOnTransientService() public async Task CheckHealthAsync_CheckCanDependOnTransientService()
{ {
@ -250,7 +280,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ {
b.Services.AddTransient<AnotherService>(); b.Services.AddTransient<AnotherService>();
b.AddCheck<CheckWithServiceDependency>(); b.AddCheck<CheckWithServiceDependency>("Test");
}); });
// Act // Act
@ -258,11 +288,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Assert // Assert
Assert.Collection( Assert.Collection(
results.Results, results.Entries,
actual => actual =>
{ {
Assert.Equal("Test", actual.Key); 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.Services.AddScoped<AnotherService>();
b.AddCheck<CheckWithServiceDependency>(); b.AddCheck<CheckWithServiceDependency>("Test");
}); });
// Act // Act
@ -282,11 +312,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Assert // Assert
Assert.Collection( Assert.Collection(
results.Results, results.Entries,
actual => actual =>
{ {
Assert.Equal("Test", actual.Key); 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.Services.AddSingleton<AnotherService>();
b.AddCheck<CheckWithServiceDependency>(); b.AddCheck<CheckWithServiceDependency>("Test");
}); });
// Act // Act
@ -306,15 +336,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Assert // Assert
Assert.Collection( Assert.Collection(
results.Results, results.Entries,
actual => actual =>
{ {
Assert.Equal("Test", actual.Key); 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(); var services = new ServiceCollection();
services.AddLogging(); services.AddLogging();
@ -326,7 +356,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
configure(builder); configure(builder);
} }
return (HealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService<IHealthCheckService>(); return (DefaultHealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService<HealthCheckService>();
} }
private class AnotherService { } private class AnotherService { }
@ -336,12 +366,22 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public CheckWithServiceDependency(AnotherService _) public CheckWithServiceDependency(AnotherService _)
{ {
} }
public string Name => "Test"; public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
public Task<HealthCheckResult> CheckHealthAsync(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. // 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. // 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; using Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks namespace Microsoft.Extensions.DependencyInjection
{ {
public class ServiceCollectionExtensionsTests public class ServiceCollectionExtensionsTest
{ {
[Fact] [Fact]
public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently() public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently()
@ -23,8 +23,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
actual => actual =>
{ {
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
Assert.Equal(typeof(IHealthCheckService), actual.ServiceType); Assert.Equal(typeof(HealthCheckService), actual.ServiceType);
Assert.Equal(typeof(HealthCheckService), actual.ImplementationType); Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType);
Assert.Null(actual.ImplementationInstance); Assert.Null(actual.ImplementationInstance);
Assert.Null(actual.ImplementationFactory); 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> <FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix> <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix> <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
<ExperimentalVersionPrefix>0.6.0</ExperimentalVersionPrefix> <ExperimentalVersionPrefix>0.6.0</ExperimentalVersionPrefix>
<ExperimentalVersionSuffix>alpha1</ExperimentalVersionSuffix> <ExperimentalVersionSuffix>alpha1</ExperimentalVersionSuffix>
<ExperimentalPackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(ExperimentalVersionSuffix)' == 'rtm' ">$(ExperimentalVersionPrefix)</ExperimentalPackageVersion> <ExperimentalPackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(ExperimentalVersionSuffix)' == 'rtm' ">$(ExperimentalVersionPrefix)</ExperimentalPackageVersion>