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>
<MicrosoftExtensionsStackTraceSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsStackTraceSourcesPackageVersion>
<MicrosoftExtensionsTypeNameHelperSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsTypeNameHelperSourcesPackageVersion>
<MicrosoftExtensionsValueStopwatchSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsValueStopwatchSourcesPackageVersion>
<MicrosoftNETCoreApp20PackageVersion>2.0.9</MicrosoftNETCoreApp20PackageVersion>
<MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
<MicrosoftNETCoreApp22PackageVersion>2.2.0-preview1-26618-02</MicrosoftNETCoreApp22PackageVersion>

View File

@ -21,7 +21,6 @@ namespace HealthChecksSample
{
// Registers required services for health checks
services.AddHealthChecks()
// Add a health check for a SQL database
.AddCheck(new SqlConnectionHealthCheck("MyDatabase", Configuration["ConnectionStrings:DefaultConnection"]));
}

View File

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

View File

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

View File

@ -1,5 +1,13 @@
{
"ConnectionStrings": {
"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 HealthCheckOptions _healthCheckOptions;
private readonly IHealthCheckService _healthCheckService;
private readonly IHealthCheck[] _checks;
public HealthCheckMiddleware(
RequestDelegate next,
@ -41,8 +40,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
_next = next;
_healthCheckOptions = healthCheckOptions.Value;
_healthCheckService = healthCheckService;
_checks = FilterHealthChecks(_healthCheckService.Checks, healthCheckOptions.Value.HealthCheckNames);
}
/// <summary>
@ -58,7 +55,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
}
// 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.
if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode))

View File

@ -15,15 +15,19 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
public class HealthCheckOptions
{
/// <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>
/// <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,
/// add the names of the desired health checks.
/// provide a function that filters the set of checks.
/// </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>()
{
{ HealthCheckStatus.Healthy, StatusCodes.Status200OK },

View File

@ -6,127 +6,139 @@ 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.Logging.Abstractions;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Default implementation of <see cref="IHealthCheckService"/>.
/// </summary>
public class HealthCheckService : IHealthCheckService
internal class HealthCheckService : IHealthCheckService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<HealthCheckService> _logger;
/// <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>
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)
public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger<HealthCheckService> logger)
{
healthChecks = healthChecks ?? throw new ArgumentNullException(nameof(healthChecks));
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// Scan the list for duplicate names to provide a better error if there are duplicates.
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var duplicates = new List<string>();
foreach (var check in healthChecks)
// 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())
{
if (!names.Add(check.Name))
{
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);
}
var healthChecks = scope.ServiceProvider.GetRequiredService<IEnumerable<IHealthCheck>>();
EnsureNoDuplicates(healthChecks);
}
}
/// <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) =>
CheckHealthAsync(Checks.Values, cancellationToken);
/// <summary>
/// Runs the provided health checks and returns the aggregated status
/// </summary>
/// <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)
CheckHealthAsync(predicate: null, cancellationToken);
public async Task<CompositeHealthCheckResult> CheckHealthAsync(
Func<IHealthCheck, bool> predicate,
CancellationToken cancellationToken = default)
{
var results = new Dictionary<string, HealthCheckResult>(Checks.Count, StringComparer.OrdinalIgnoreCase);
foreach (var check in checks)
using (var scope = _scopeFactory.CreateScope())
{
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(check.Name)))
var healthChecks = scope.ServiceProvider.GetRequiredService<IEnumerable<IHealthCheck>>();
var results = new Dictionary<string, HealthCheckResult>(StringComparer.OrdinalIgnoreCase);
foreach (var healthCheck in healthChecks)
{
HealthCheckResult result;
try
if (predicate != null && !predicate(healthCheck))
{
_logger.LogTrace("Running health check: {healthCheckName}", check.Name);
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);
continue;
}
// 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.
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;
}
cancellationToken.ThrowIfCancellationRequested();
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>
/// Adds a new health check with the implementation.
/// 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>
@ -88,5 +88,27 @@ namespace Microsoft.Extensions.DependencyInjection
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

@ -1,26 +1,38 @@
// 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;
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.
/// 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>
/// 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>
/// Runs all the health checks in the application and returns the aggregated status.
/// </summary>
@ -34,13 +46,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
/// <summary>
/// Runs the provided health checks and returns the aggregated status
/// </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>
/// <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(IEnumerable<IHealthCheck> checks,
Task<CompositeHealthCheckResult> CheckHealthAsync(Func<IHealthCheck, bool> predicate,
CancellationToken cancellationToken = default);
}
}

View File

@ -14,6 +14,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.ValueStopwatch.Sources" Version="$(MicrosoftExtensionsValueStopwatchSourcesPackageVersion)" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<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()
{
HealthCheckNames =
{
"Baz",
"FOO",
},
Predicate = (check) => check.Name == "Foo" || check.Name == "Baz",
});
})
.ConfigureServices(services =>
@ -327,35 +323,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
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]
public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort()
{

View File

@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
@ -12,40 +14,30 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
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]
public void Constructor_ThrowsUsefulExceptionForDuplicateNames()
{
// Arrange
var checks = new[]
{
new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())),
new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())),
new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())),
new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())),
new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())),
};
//
// Doing this the old fashioned way so we can verify that the exception comes
// from the constructor.
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging();
serviceCollection.AddOptions();
serviceCollection.AddHealthChecks()
.AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())))
.AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())));
var services = serviceCollection.BuildServiceProvider();
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
var logger = services.GetRequiredService<ILogger<HealthCheckService>>();
// Act
var exception = Assert.Throws<ArgumentException>(() => new HealthCheckService(checks));
var exception = Assert.Throws<ArgumentException>(() => new HealthCheckService(scopeFactory, logger));
// Assert
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 }
};
var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
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[]
var service = CreateHealthChecksService(b =>
{
healthyCheck,
degradedCheck,
unhealthyCheck,
b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
});
// Act
@ -85,7 +73,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
Assert.Collection(results.Results,
actual =>
{
Assert.Equal(healthyCheck.Name, actual.Key);
Assert.Equal("HealthyCheck", actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
@ -97,7 +85,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
},
actual =>
{
Assert.Equal(degradedCheck.Name, actual.Key);
Assert.Equal("DegradedCheck", actual.Key);
Assert.Equal(DegradedMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status);
Assert.Null(actual.Value.Exception);
@ -105,7 +93,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
},
actual =>
{
Assert.Equal(unhealthyCheck.Name, actual.Key);
Assert.Equal("UnhealthyCheck", actual.Key);
Assert.Equal(UnhealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Unhealthy, actual.Value.Status);
Assert.Same(exception, actual.Value.Exception);
@ -114,7 +102,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
}
[Fact]
public async Task CheckAsync_RunsProvidedChecksAndAggregatesResultsAsync()
public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync()
{
const string DataKey = "Foo";
const string DataValue = "Bar";
@ -129,28 +117,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ DataKey, DataValue }
};
var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
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[]
var service = CreateHealthChecksService(b =>
{
healthyCheck,
degradedCheck,
unhealthyCheck,
b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
});
// Act
var results = await service.CheckHealthAsync(new[]
{
service.Checks["HealthyCheck"]
});
var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck");
// Assert
Assert.Collection(results.Results,
actual =>
{
Assert.Equal(healthyCheck.Name, actual.Key);
Assert.Equal("HealthyCheck", actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
@ -168,11 +149,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Arrange
var thrownException = new InvalidOperationException("Whoops!");
var faultedException = new InvalidOperationException("Ohnoes!");
var service = new HealthCheckService(new[]
var service = CreateHealthChecksService(b =>
{
new HealthCheck("Throws", ct => throw thrownException),
new HealthCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException)),
new HealthCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())),
b.AddCheck("Throws", ct => throw thrownException);
b.AddCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException));
b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy()));
});
// Act
@ -223,8 +205,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
});
return Task.FromResult(HealthCheckResult.Healthy());
});
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
var results = await service.CheckHealthAsync();
@ -241,9 +230,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult()
{
// 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
@ -252,5 +241,108 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Assert
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.
// 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;
@ -19,12 +21,13 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy()));
// Act
var healthCheckService = services.BuildServiceProvider().GetRequiredService<IHealthCheckService>();
var checks = services.BuildServiceProvider().GetRequiredService<IEnumerable<IHealthCheck>>();
// Assert
Assert.Collection(healthCheckService.Checks,
actual => Assert.Equal("Foo", actual.Key),
actual => Assert.Equal("Bar", actual.Key));
Assert.Collection(
checks,
actual => Assert.Equal("Foo", actual.Name),
actual => Assert.Equal("Bar", actual.Name));
}
}
}