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:
commit
0bb4e948f7
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"]));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue