Merge pull request #473 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-08-30 12:47:29 -07:00 committed by GitHub
commit 0bb4e948f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 346 additions and 225 deletions

View File

@ -31,6 +31,7 @@
<MicrosoftExtensionsRazorViewsSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsRazorViewsSourcesPackageVersion> <MicrosoftExtensionsRazorViewsSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsRazorViewsSourcesPackageVersion>
<MicrosoftExtensionsStackTraceSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsStackTraceSourcesPackageVersion> <MicrosoftExtensionsStackTraceSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsStackTraceSourcesPackageVersion>
<MicrosoftExtensionsTypeNameHelperSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsTypeNameHelperSourcesPackageVersion> <MicrosoftExtensionsTypeNameHelperSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsTypeNameHelperSourcesPackageVersion>
<MicrosoftExtensionsValueStopwatchSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsValueStopwatchSourcesPackageVersion>
<MicrosoftNETCoreApp20PackageVersion>2.0.9</MicrosoftNETCoreApp20PackageVersion> <MicrosoftNETCoreApp20PackageVersion>2.0.9</MicrosoftNETCoreApp20PackageVersion>
<MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion> <MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
<MicrosoftNETCoreApp22PackageVersion>2.2.0-preview1-26618-02</MicrosoftNETCoreApp22PackageVersion> <MicrosoftNETCoreApp22PackageVersion>2.2.0-preview1-26618-02</MicrosoftNETCoreApp22PackageVersion>

View File

@ -21,7 +21,6 @@ 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(new SqlConnectionHealthCheck("MyDatabase", Configuration["ConnectionStrings:DefaultConnection"]));
} }

View File

@ -17,7 +17,6 @@ namespace HealthChecksSample
// Registers required services for health checks // Registers required services for health checks
services services
.AddHealthChecks() .AddHealthChecks()
.AddCheck("identity", () => Task.FromResult(HealthCheckResult.Healthy()))
.AddCheck(new SlowDependencyHealthCheck()); .AddCheck(new SlowDependencyHealthCheck());
} }
@ -53,11 +52,8 @@ namespace HealthChecksSample
// The liveness check uses an 'identity' health check that always returns healthy // The liveness check uses an 'identity' health check that always returns healthy
app.UseHealthChecks("/health/live", new HealthCheckOptions() app.UseHealthChecks("/health/live", new HealthCheckOptions()
{ {
// Filters the set of health checks run by this middleware // Exclude all checks, just return a 200.
HealthCheckNames = Predicate = (check) => false,
{
"identity",
},
}); });
app.Run(async (context) => app.Run(async (context) =>

View File

@ -15,7 +15,7 @@ namespace HealthChecksSample
{ {
_scenarios = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase) _scenarios = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
{ {
{ "", typeof(BasicStartup) }, { "", typeof(DBHealthStartup) },
{ "basic", typeof(BasicStartup) }, { "basic", typeof(BasicStartup) },
{ "writer", typeof(CustomWriterStartup) }, { "writer", typeof(CustomWriterStartup) },
{ "liveness", typeof(LivenessProbeStartup) }, { "liveness", typeof(LivenessProbeStartup) },
@ -48,6 +48,8 @@ namespace HealthChecksSample
.UseConfiguration(config) .UseConfiguration(config)
.ConfigureLogging(builder => .ConfigureLogging(builder =>
{ {
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddConfiguration(config);
builder.AddConsole(); builder.AddConsole();
}) })
.UseKestrel() .UseKestrel()

View File

@ -1,5 +1,13 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=HealthCheckSample;Trusted_Connection=True;MultipleActiveResultSets=true;ConnectRetryCount=0" "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=HealthCheckSample;Trusted_Connection=True;MultipleActiveResultSets=true;ConnectRetryCount=0"
},
"Logging": {
"LogLevel": {
"Default": "Debug"
},
"Console": {
"IncludeScopes": "true"
}
} }
} }

View File

@ -16,7 +16,6 @@ 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 IHealthCheckService _healthCheckService;
private readonly IHealthCheck[] _checks;
public HealthCheckMiddleware( public HealthCheckMiddleware(
RequestDelegate next, RequestDelegate next,
@ -41,8 +40,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
_next = next; _next = next;
_healthCheckOptions = healthCheckOptions.Value; _healthCheckOptions = healthCheckOptions.Value;
_healthCheckService = healthCheckService; _healthCheckService = healthCheckService;
_checks = FilterHealthChecks(_healthCheckService.Checks, healthCheckOptions.Value.HealthCheckNames);
} }
/// <summary> /// <summary>
@ -58,7 +55,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
} }
// Get results // Get results
var result = await _healthCheckService.CheckHealthAsync(_checks, httpContext.RequestAborted); var result = await _healthCheckService.CheckHealthAsync(_healthCheckOptions.Predicate, httpContext.RequestAborted);
// Map status to response code - this is customizable via options. // Map status to response code - this is customizable via options.
if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode))

View File

@ -15,15 +15,19 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
public class HealthCheckOptions public class HealthCheckOptions
{ {
/// <summary> /// <summary>
/// Gets a set of health check names used to filter the set of health checks run. /// Gets or sets a predicate that is used to filter the set of health checks executed.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If <see cref="HealthCheckNames"/> is empty, the <see cref="HealthCheckMiddleware"/> will run all /// If <see cref="Predicate"/> is <c>null</c>, the <see cref="HealthCheckMiddleware"/> will run all
/// 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,
/// add the names of the desired health checks. /// provide a function that filters the set of checks.
/// </remarks> /// </remarks>
public ISet<string> HealthCheckNames { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase); public Func<IHealthCheck, bool> Predicate { get; set; }
/// <summary>
/// Gets a dictionary mapping the <see cref="HealthCheckStatus"/> to an HTTP status code applied to the response.
/// This property can be used to configure the status codes returned for each status.
/// </summary>
public IDictionary<HealthCheckStatus, int> ResultStatusCodes { get; } = new Dictionary<HealthCheckStatus, int>() public IDictionary<HealthCheckStatus, int> ResultStatusCodes { get; } = new Dictionary<HealthCheckStatus, int>()
{ {
{ HealthCheckStatus.Healthy, StatusCodes.Status200OK }, { HealthCheckStatus.Healthy, StatusCodes.Status200OK },

View File

@ -6,127 +6,139 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Extensions.Diagnostics.HealthChecks namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ {
/// <summary> internal class HealthCheckService : IHealthCheckService
/// Default implementation of <see cref="IHealthCheckService"/>.
/// </summary>
public class HealthCheckService : IHealthCheckService
{ {
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<HealthCheckService> _logger; private readonly ILogger<HealthCheckService> _logger;
/// <summary> public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger<HealthCheckService> logger)
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing all the health checks registered in the application.
/// </summary>
/// <remarks>
/// The key maps to the <see cref="IHealthCheck.Name"/> property of the health check, and the value is the <see cref="IHealthCheck"/>
/// instance itself.
/// </remarks>
public IReadOnlyDictionary<string, IHealthCheck> Checks { get; }
/// <summary>
/// Constructs a <see cref="HealthCheckService"/> from the provided collection of <see cref="IHealthCheck"/> instances.
/// </summary>
/// <param name="healthChecks">The <see cref="IHealthCheck"/> instances that have been registered in the application.</param>
public HealthCheckService(IEnumerable<IHealthCheck> healthChecks) : this(healthChecks, NullLogger<HealthCheckService>.Instance) { }
/// <summary>
/// Constructs a <see cref="HealthCheckService"/> from the provided collection of <see cref="IHealthCheck"/> instances, and the provided logger.
/// </summary>
/// <param name="healthChecks">The <see cref="IHealthCheck"/> instances that have been registered in the application.</param>
/// <param name="logger">A <see cref="ILogger{T}"/> that can be used to log events that occur during health check operations.</param>
public HealthCheckService(IEnumerable<IHealthCheck> healthChecks, ILogger<HealthCheckService> logger)
{ {
healthChecks = healthChecks ?? throw new ArgumentNullException(nameof(healthChecks)); _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
// Scan the list for duplicate names to provide a better error if there are duplicates. // We're specifically going out of our way to do this at startup time. We want to make sure you
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase); // get any kind of health-check related error as early as possible. Waiting until someone
var duplicates = new List<string>(); // actually tries to **run** health checks would be real baaaaad.
foreach (var check in healthChecks) using (var scope = _scopeFactory.CreateScope())
{ {
if (!names.Add(check.Name)) var healthChecks = scope.ServiceProvider.GetRequiredService<IEnumerable<IHealthCheck>>();
{ EnsureNoDuplicates(healthChecks);
duplicates.Add(check.Name);
}
}
if (duplicates.Count > 0)
{
throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicates)}", nameof(healthChecks));
}
Checks = healthChecks.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase);
if (_logger.IsEnabled(LogLevel.Debug))
{
foreach (var check in Checks)
{
_logger.LogDebug("Health check '{healthCheckName}' has been registered", check.Key);
}
} }
} }
/// <summary>
/// Runs all the health checks in the application and returns the aggregated status.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
/// <returns>
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
/// </returns>
public Task<CompositeHealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default) => public Task<CompositeHealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default) =>
CheckHealthAsync(Checks.Values, cancellationToken); CheckHealthAsync(predicate: null, cancellationToken);
/// <summary> public async Task<CompositeHealthCheckResult> CheckHealthAsync(
/// Runs the provided health checks and returns the aggregated status Func<IHealthCheck, bool> predicate,
/// </summary> CancellationToken cancellationToken = default)
/// <param name="checks">The <see cref="IHealthCheck"/> instances to be run.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
/// <returns>
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
/// </returns>
public async Task<CompositeHealthCheckResult> CheckHealthAsync(IEnumerable<IHealthCheck> checks, CancellationToken cancellationToken = default)
{ {
var results = new Dictionary<string, HealthCheckResult>(Checks.Count, StringComparer.OrdinalIgnoreCase); using (var scope = _scopeFactory.CreateScope())
foreach (var check in checks)
{ {
cancellationToken.ThrowIfCancellationRequested(); var healthChecks = scope.ServiceProvider.GetRequiredService<IEnumerable<IHealthCheck>>();
// 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. var results = new Dictionary<string, HealthCheckResult>(StringComparer.OrdinalIgnoreCase);
using (_logger.BeginScope(new HealthCheckLogScope(check.Name))) foreach (var healthCheck in healthChecks)
{ {
HealthCheckResult result; if (predicate != null && !predicate(healthCheck))
try
{ {
_logger.LogTrace("Running health check: {healthCheckName}", check.Name); continue;
result = await check.CheckHealthAsync(cancellationToken);
_logger.LogTrace("Health check '{healthCheckName}' completed with status '{healthCheckStatus}'", check.Name, result.Status);
}
catch (Exception ex)
{
// We don't log this as an error because a health check failing shouldn't bring down the active task.
_logger.LogError(ex, "Health check '{healthCheckName}' threw an unexpected exception", check.Name);
result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null);
} }
// This can only happen if the result is default(HealthCheckResult) cancellationToken.ThrowIfCancellationRequested();
if (result.Status == HealthCheckStatus.Unknown)
{
// This is different from the case above. We throw here because a health check is doing something specifically incorrect.
var exception = new InvalidOperationException($"Health check '{check.Name}' returned a result with a status of Unknown");
_logger.LogError(exception, "Health check '{healthCheckName}' returned a result with a status of Unknown", check.Name);
throw exception;
}
results[check.Name] = result; // 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);
} }
return new CompositeHealthCheckResult(results);
} }
} }
} }

View File

@ -68,7 +68,7 @@ namespace Microsoft.Extensions.DependencyInjection
} }
/// <summary> /// <summary>
/// Adds a new health check with the implementation. /// Adds a new health check with the provided implementation.
/// </summary> /// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add the check to.</param> /// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add the check to.</param>
/// <param name="check">An <see cref="IHealthCheck"/> implementation.</param> /// <param name="check">An <see cref="IHealthCheck"/> implementation.</param>
@ -88,5 +88,27 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.AddSingleton<IHealthCheck>(check); builder.Services.AddSingleton<IHealthCheck>(check);
return builder; 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

@ -1,26 +1,38 @@
// 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.Collections.Generic; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.Extensions.Diagnostics.HealthChecks namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ {
/// <summary> /// <summary>
/// A service which can be used to check the status of <see cref="IHealthCheck"/> instances registered in the application. /// A service which can be used to check the status of <see cref="IHealthCheck"/> instances
/// registered in the application.
/// </summary> /// </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 public interface IHealthCheckService
{ {
/// <summary>
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing all the health checks registered in the application.
/// </summary>
/// <remarks>
/// The key maps to the <see cref="IHealthCheck.Name"/> property of the health check, and the value is the <see cref="IHealthCheck"/>
/// instance itself.
/// </remarks>
IReadOnlyDictionary<string, IHealthCheck> Checks { get; }
/// <summary> /// <summary>
/// Runs all the health checks in the application and returns the aggregated status. /// Runs all the health checks in the application and returns the aggregated status.
/// </summary> /// </summary>
@ -34,13 +46,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
/// <summary> /// <summary>
/// Runs the provided health checks and returns the aggregated status /// Runs the provided health checks and returns the aggregated status
/// </summary> /// </summary>
/// <param name="checks">The <see cref="IHealthCheck"/> instances to be run.</param> /// <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> /// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
/// <returns> /// <returns>
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run, /// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results. /// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
/// </returns> /// </returns>
Task<CompositeHealthCheckResult> CheckHealthAsync(IEnumerable<IHealthCheck> checks, Task<CompositeHealthCheckResult> CheckHealthAsync(Func<IHealthCheck, bool> predicate,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
} }
} }

View File

@ -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.ValueStopwatch.Sources" Version="$(MicrosoftExtensionsValueStopwatchSourcesPackageVersion)" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj" /> <ProjectReference Include="..\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj" />

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -302,11 +302,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{ {
app.UseHealthChecks("/health", new HealthCheckOptions() app.UseHealthChecks("/health", new HealthCheckOptions()
{ {
HealthCheckNames = Predicate = (check) => check.Name == "Foo" || check.Name == "Baz",
{
"Baz",
"FOO",
},
}); });
}) })
.ConfigureServices(services => .ConfigureServices(services =>
@ -327,35 +323,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
} }
[Fact]
public void CanFilterChecks_ThrowsForMissingCheck()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
HealthCheckNames =
{
"Bazzzzzz",
"FOO",
},
});
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!")))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var ex = Assert.Throws<InvalidOperationException>(() => new TestServer(builder));
Assert.Equal(
"The following health checks were not found: 'Bazzzzzz'. Registered health checks: 'Foo, Bar, Baz'.",
ex.Message);
}
[Fact] [Fact]
public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort() public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort()
{ {

View File

@ -3,7 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Xunit; using Xunit;
@ -12,40 +14,30 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ {
public class HealthCheckServiceTests public class HealthCheckServiceTests
{ {
[Fact]
public void Constructor_BuildsDictionaryOfChecks()
{
// Arrange
var fooCheck = new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()));
var barCheck = new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy()));
var bazCheck = new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()));
var checks = new[] { fooCheck, barCheck, bazCheck };
// Act
var service = new HealthCheckService(checks);
// Assert
Assert.Same(fooCheck, service.Checks["Foo"]);
Assert.Same(barCheck, service.Checks["Bar"]);
Assert.Same(bazCheck, service.Checks["Baz"]);
Assert.Equal(3, service.Checks.Count);
}
[Fact] [Fact]
public void Constructor_ThrowsUsefulExceptionForDuplicateNames() public void Constructor_ThrowsUsefulExceptionForDuplicateNames()
{ {
// Arrange // Arrange
var checks = new[] //
{ // Doing this the old fashioned way so we can verify that the exception comes
new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())), // from the constructor.
new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())), var serviceCollection = new ServiceCollection();
new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())), serviceCollection.AddLogging();
new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())), serviceCollection.AddOptions();
new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())), serviceCollection.AddHealthChecks()
}; .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())));
var services = serviceCollection.BuildServiceProvider();
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
var logger = services.GetRequiredService<ILogger<HealthCheckService>>();
// Act // Act
var exception = Assert.Throws<ArgumentException>(() => new HealthCheckService(checks)); var exception = Assert.Throws<ArgumentException>(() => new HealthCheckService(scopeFactory, logger));
// Assert // Assert
Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message); Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message);
@ -67,15 +59,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ DataKey, DataValue } { DataKey, DataValue }
}; };
var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); var service = CreateHealthChecksService(b =>
var degradedCheck = new HealthCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
var unhealthyCheck = new HealthCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
var service = new HealthCheckService(new[]
{ {
healthyCheck, b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
degradedCheck, b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
unhealthyCheck, b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
}); });
// Act // Act
@ -85,7 +73,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
Assert.Collection(results.Results, Assert.Collection(results.Results,
actual => actual =>
{ {
Assert.Equal(healthyCheck.Name, 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(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception); Assert.Null(actual.Value.Exception);
@ -97,7 +85,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
}, },
actual => actual =>
{ {
Assert.Equal(degradedCheck.Name, 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(HealthCheckStatus.Degraded, actual.Value.Status);
Assert.Null(actual.Value.Exception); Assert.Null(actual.Value.Exception);
@ -105,7 +93,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
}, },
actual => actual =>
{ {
Assert.Equal(unhealthyCheck.Name, 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(HealthCheckStatus.Unhealthy, actual.Value.Status);
Assert.Same(exception, actual.Value.Exception); Assert.Same(exception, actual.Value.Exception);
@ -114,7 +102,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
} }
[Fact] [Fact]
public async Task CheckAsync_RunsProvidedChecksAndAggregatesResultsAsync() public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync()
{ {
const string DataKey = "Foo"; const string DataKey = "Foo";
const string DataValue = "Bar"; const string DataValue = "Bar";
@ -129,28 +117,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ DataKey, DataValue } { DataKey, DataValue }
}; };
var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); var service = CreateHealthChecksService(b =>
var degradedCheck = new HealthCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
var unhealthyCheck = new HealthCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
var service = new HealthCheckService(new[]
{ {
healthyCheck, b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
degradedCheck, b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
unhealthyCheck, b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
}); });
// Act // Act
var results = await service.CheckHealthAsync(new[] var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck");
{
service.Checks["HealthyCheck"]
});
// Assert // Assert
Assert.Collection(results.Results, Assert.Collection(results.Results,
actual => actual =>
{ {
Assert.Equal(healthyCheck.Name, 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(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception); Assert.Null(actual.Value.Exception);
@ -168,11 +149,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Arrange // Arrange
var thrownException = new InvalidOperationException("Whoops!"); var thrownException = new InvalidOperationException("Whoops!");
var faultedException = new InvalidOperationException("Ohnoes!"); var faultedException = new InvalidOperationException("Ohnoes!");
var service = new HealthCheckService(new[]
var service = CreateHealthChecksService(b =>
{ {
new HealthCheck("Throws", ct => throw thrownException), b.AddCheck("Throws", ct => throw thrownException);
new HealthCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException)), b.AddCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException));
new HealthCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())), b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy()));
}); });
// Act // Act
@ -223,8 +205,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
}); });
return Task.FromResult(HealthCheckResult.Healthy()); return Task.FromResult(HealthCheckResult.Healthy());
}); });
var loggerFactory = new TestLoggerFactory(sink, enabled: true); var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var service = new HealthCheckService(new[] { check }, loggerFactory.CreateLogger<HealthCheckService>()); var service = CreateHealthChecksService(b =>
{
// Override the logger factory for testing
b.Services.AddSingleton<ILoggerFactory>(loggerFactory);
b.AddCheck(check);
});
// Act // Act
var results = await service.CheckHealthAsync(); var results = await service.CheckHealthAsync();
@ -241,9 +230,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult() public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult()
{ {
// Arrange // Arrange
var service = new HealthCheckService(new[] var service = CreateHealthChecksService(b =>
{ {
new HealthCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))), b.AddCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult)));
}); });
// Act // Act
@ -252,5 +241,108 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Assert // Assert
Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message); Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message);
} }
[Fact]
public async Task CheckHealthAsync_CheckCanDependOnTransientService()
{
// Arrange
var service = CreateHealthChecksService(b =>
{
b.Services.AddTransient<AnotherService>();
b.AddCheck<CheckWithServiceDependency>();
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(
results.Results,
actual =>
{
Assert.Equal("Test", actual.Key);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
});
}
[Fact]
public async Task CheckHealthAsync_CheckCanDependOnScopedService()
{
// Arrange
var service = CreateHealthChecksService(b =>
{
b.Services.AddScoped<AnotherService>();
b.AddCheck<CheckWithServiceDependency>();
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(
results.Results,
actual =>
{
Assert.Equal("Test", actual.Key);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
});
}
[Fact]
public async Task CheckHealthAsync_CheckCanDependOnSingletonService()
{
// Arrange
var service = CreateHealthChecksService(b =>
{
b.Services.AddSingleton<AnotherService>();
b.AddCheck<CheckWithServiceDependency>();
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(
results.Results,
actual =>
{
Assert.Equal("Test", actual.Key);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
});
}
private static HealthCheckService CreateHealthChecksService(Action<IHealthChecksBuilder> configure)
{
var services = new ServiceCollection();
services.AddLogging();
services.AddOptions();
var builder = services.AddHealthChecks();
if (configure != null)
{
configure(builder);
}
return (HealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService<IHealthCheckService>();
}
private class AnotherService { }
private class CheckWithServiceDependency : IHealthCheck
{
public CheckWithServiceDependency(AnotherService _)
{
}
public string Name => "Test";
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(HealthCheckResult.Healthy());
}
}
} }
} }

View File

@ -1,6 +1,8 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Xunit; using Xunit;
@ -19,12 +21,13 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy())); .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy()));
// Act // Act
var healthCheckService = services.BuildServiceProvider().GetRequiredService<IHealthCheckService>(); var checks = services.BuildServiceProvider().GetRequiredService<IEnumerable<IHealthCheck>>();
// Assert // Assert
Assert.Collection(healthCheckService.Checks, Assert.Collection(
actual => Assert.Equal("Foo", actual.Key), checks,
actual => Assert.Equal("Bar", actual.Key)); actual => Assert.Equal("Foo", actual.Name),
actual => Assert.Equal("Bar", actual.Name));
} }
} }
} }