This repository has been archived by the owner on Dec 8, 2018. It is now read-only.
Allow health checks to use any DI lifetime #466
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filter? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eh, we considered it and thought this was less overloaded. If we use filter (or another term) other places in the framework then I want to change this. |
||
|
||
/// <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 }, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,127 +6,139 @@ | |
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); | ||
} | ||
var healthChecks = scope.ServiceProvider.GetRequiredService<IEnumerable<IHealthCheck>>(); | ||
EnsureNoDuplicates(healthChecks); | ||
} | ||
} | ||
|
||
public Task<CompositeHealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default) => | ||
CheckHealthAsync(predicate: null, cancellationToken); | ||
|
||
if (duplicates.Count > 0) | ||
public async Task<CompositeHealthCheckResult> CheckHealthAsync( | ||
Func<IHealthCheck, bool> predicate, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
using (var scope = _scopeFactory.CreateScope()) | ||
{ | ||
throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicates)}", nameof(healthChecks)); | ||
var healthChecks = scope.ServiceProvider.GetRequiredService<IEnumerable<IHealthCheck>>(); | ||
|
||
var results = new Dictionary<string, HealthCheckResult>(StringComparer.OrdinalIgnoreCase); | ||
foreach (var healthCheck in healthChecks) | ||
{ | ||
if (predicate != null && !predicate(healthCheck)) | ||
{ | ||
continue; | ||
} | ||
|
||
cancellationToken.ThrowIfCancellationRequested(); | ||
|
||
// If the health check does things like make Database queries using EF or backend HTTP calls, | ||
// it may be valuable to know that logs it generates are part of a health check. So we start a scope. | ||
using (_logger.BeginScope(new HealthCheckLogScope(healthCheck.Name))) | ||
{ | ||
HealthCheckResult result; | ||
try | ||
{ | ||
Log.HealthCheckBegin(_logger, healthCheck); | ||
var stopwatch = ValueStopwatch.StartNew(); | ||
result = await healthCheck.CheckHealthAsync(cancellationToken); | ||
Log.HealthCheckEnd(_logger, healthCheck, result, stopwatch.GetElapsedTime()); | ||
} | ||
catch (Exception ex) | ||
{ | ||
Log.HealthCheckError(_logger, healthCheck, ex); | ||
result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null); | ||
} | ||
|
||
// This can only happen if the result is default(HealthCheckResult) | ||
if (result.Status == HealthCheckStatus.Unknown) | ||
{ | ||
// This is different from the case above. We throw here because a health check is doing something specifically incorrect. | ||
throw new InvalidOperationException($"Health check '{healthCheck.Name}' returned a result with a status of Unknown"); | ||
} | ||
|
||
results[healthCheck.Name] = result; | ||
} | ||
} | ||
|
||
return new CompositeHealthCheckResult(results); | ||
} | ||
} | ||
|
||
Checks = healthChecks.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase); | ||
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 (_logger.IsEnabled(LogLevel.Debug)) | ||
if (duplicateNames.Count > 0) | ||
{ | ||
foreach (var check in Checks) | ||
{ | ||
_logger.LogDebug("Health check '{healthCheckName}' has been registered", check.Key); | ||
} | ||
throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(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) | ||
private static class Log | ||
{ | ||
var results = new Dictionary<string, HealthCheckResult>(Checks.Count, StringComparer.OrdinalIgnoreCase); | ||
foreach (var check in checks) | ||
public static class EventIds | ||
{ | ||
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))) | ||
{ | ||
HealthCheckResult result; | ||
try | ||
{ | ||
_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); | ||
} | ||
public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice |
||
public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd"); | ||
public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError"); | ||
} | ||
|
||
// 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; | ||
} | ||
private static readonly Action<ILogger, string, Exception> _healthCheckBegin = LoggerMessage.Define<string>( | ||
LogLevel.Debug, | ||
EventIds.HealthCheckBegin, | ||
"Running health check {HealthCheckName}"); | ||
|
||
results[check.Name] = result; | ||
} | ||
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing this to an arbitrary predicate rather than an explicit 'filter by name' seems like a good usability improvement. Since the set of checks has to be resolved per-request a lot of motivations for providing 'filter by name' seemed to melt away.