diff --git a/build/dependencies.props b/build/dependencies.props index 0ae59c4402..34ae14f945 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -31,6 +31,7 @@ 3.0.0-alpha1-10352 3.0.0-alpha1-10352 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 2.0.9 2.1.2 2.2.0-preview1-26618-02 diff --git a/samples/HealthChecksSample/DBHealthStartup.cs b/samples/HealthChecksSample/DBHealthStartup.cs index f82f4c54ef..58b6c8d157 100644 --- a/samples/HealthChecksSample/DBHealthStartup.cs +++ b/samples/HealthChecksSample/DBHealthStartup.cs @@ -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"])); } diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs index f5eabc6696..aa644d231f 100644 --- a/samples/HealthChecksSample/LivenessProbeStartup.cs +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -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) => diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index d24e0b38c1..425b0b3f1a 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -15,7 +15,7 @@ namespace HealthChecksSample { _scenarios = new Dictionary(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() diff --git a/samples/HealthChecksSample/appsettings.json b/samples/HealthChecksSample/appsettings.json index 77b5f890a4..21b2dcdbfd 100644 --- a/samples/HealthChecksSample/appsettings.json +++ b/samples/HealthChecksSample/appsettings.json @@ -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" + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 3a220f2595..20ec5e356e 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -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); } /// @@ -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)) diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index b57374f2ae..7930d25577 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -15,15 +15,19 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks public class HealthCheckOptions { /// - /// 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. /// /// - /// If is empty, the will run all + /// If is null, the 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. /// - public ISet HealthCheckNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); + public Func Predicate { get; set; } + /// + /// Gets a dictionary mapping the to an HTTP status code applied to the response. + /// This property can be used to configure the status codes returned for each status. + /// public IDictionary ResultStatusCodes { get; } = new Dictionary() { { HealthCheckStatus.Healthy, StatusCodes.Status200OK }, diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs index 35cc0a1c82..111113ea77 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs @@ -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 { - /// - /// Default implementation of . - /// - public class HealthCheckService : IHealthCheckService + internal class HealthCheckService : IHealthCheckService { + private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; - /// - /// A containing all the health checks registered in the application. - /// - /// - /// The key maps to the property of the health check, and the value is the - /// instance itself. - /// - public IReadOnlyDictionary Checks { get; } - - /// - /// Constructs a from the provided collection of instances. - /// - /// The instances that have been registered in the application. - public HealthCheckService(IEnumerable healthChecks) : this(healthChecks, NullLogger.Instance) { } - - /// - /// Constructs a from the provided collection of instances, and the provided logger. - /// - /// The instances that have been registered in the application. - /// A that can be used to log events that occur during health check operations. - public HealthCheckService(IEnumerable healthChecks, ILogger logger) + public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger 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(StringComparer.OrdinalIgnoreCase); - var duplicates = new List(); - 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>(); + EnsureNoDuplicates(healthChecks); } } - /// - /// Runs all the health checks in the application and returns the aggregated status. - /// - /// A which can be used to cancel the health checks. - /// - /// A which will complete when all the health checks have been run, - /// yielding a containing the results. - /// public Task CheckHealthAsync(CancellationToken cancellationToken = default) => - CheckHealthAsync(Checks.Values, cancellationToken); - - /// - /// Runs the provided health checks and returns the aggregated status - /// - /// The instances to be run. - /// A which can be used to cancel the health checks. - /// - /// A which will complete when all the health checks have been run, - /// yielding a containing the results. - /// - public async Task CheckHealthAsync(IEnumerable checks, CancellationToken cancellationToken = default) + CheckHealthAsync(predicate: null, cancellationToken); + + public async Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default) { - var results = new Dictionary(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>(); + + var results = new Dictionary(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 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 _healthCheckBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckBegin, + "Running health check {HealthCheckName}"); + + private static readonly Action _healthCheckEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckEnd, + "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}"); + + private static readonly Action _healthCheckError = LoggerMessage.Define( + 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); } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs index b2f931fcd3..d3af5b17c2 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs @@ -68,7 +68,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Adds a new health check with the implementation. + /// Adds a new health check with the provided implementation. /// /// The to add the check to. /// An implementation. @@ -88,5 +88,27 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.AddSingleton(check); return builder; } + + /// + /// Adds a new health check as a transient dependency injected service with the provided type. + /// + /// The health check implementation type. + /// The . + /// The . + /// + /// This method will register a transient service of type with the + /// provided implementation type . Using this method to register a health + /// check allows you to register a health check that depends on transient and scoped services. + /// + public static IHealthChecksBuilder AddCheck(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; + } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs index 9c1bfbafc4..f931475a4d 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs @@ -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 { /// - /// A service which can be used to check the status of instances registered in the application. + /// A service which can be used to check the status of instances + /// registered in the application. /// + /// + /// + /// The default implementation of is registered in the dependency + /// injection container as a singleton service by calling + /// . + /// + /// + /// The returned by + /// + /// provides a convenience API for registering health checks. + /// + /// + /// The default implementation of will use all services + /// of type registered in the dependency injection container. + /// 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. + /// + /// public interface IHealthCheckService { - /// - /// A containing all the health checks registered in the application. - /// - /// - /// The key maps to the property of the health check, and the value is the - /// instance itself. - /// - IReadOnlyDictionary Checks { get; } - /// /// Runs all the health checks in the application and returns the aggregated status. /// @@ -34,13 +46,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// /// Runs the provided health checks and returns the aggregated status /// - /// The instances to be run. + /// + /// A predicate that can be used to include health checks based on user-defined criteria. + /// /// A which can be used to cancel the health checks. /// /// A which will complete when all the health checks have been run, /// yielding a containing the results. /// - Task CheckHealthAsync(IEnumerable checks, + Task CheckHealthAsync(Func predicate, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index 56f70f7a10..2b3d0e6261 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -14,6 +14,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder + diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..13e969bfad --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 692e8b5bfd..1af49d8422 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -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(() => 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() { diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs index 2a7fa8395f..6e7c28e9ef 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs @@ -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(); + var logger = services.GetRequiredService>(); // Act - var exception = Assert.Throws(() => new HealthCheckService(checks)); + var exception = Assert.Throws(() => 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(faultedException)), - new HealthCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())), + b.AddCheck("Throws", ct => throw thrownException); + b.AddCheck("Faults", ct => Task.FromException(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()); + var service = CreateHealthChecksService(b => + { + // Override the logger factory for testing + b.Services.AddSingleton(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(); + + b.AddCheck(); + }); + + // 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(); + + b.AddCheck(); + }); + + // 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(); + + b.AddCheck(); + }); + + // 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 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(); + } + + private class AnotherService { } + + private class CheckWithServiceDependency : IHealthCheck + { + public CheckWithServiceDependency(AnotherService _) + { + } + + public string Name => "Test"; + + public Task CheckHealthAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(HealthCheckResult.Healthy()); + } + } } } diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs index a24a4a5b4e..f1bcbbf00b 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs @@ -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(); + var checks = services.BuildServiceProvider().GetRequiredService>(); // 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)); } } }