From dd07e6743c9a9cdd45d48017a4a5ee932ea3188e Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 2 Sep 2018 12:09:26 -0700 Subject: [PATCH 1/5] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index ad2a6ed050..cd53ae3566 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,35 +3,35 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-20180807.2 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 + 2.2.0-preview1-20180821.1 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 2.0.9 2.1.2 2.2.0-preview1-26618-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 3fbcc80189..ad704918df 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180807.2 -commithash:11495dbd236104434e08cb1152fcb58cf2a20923 +version:2.2.0-preview1-20180821.1 +commithash:c8d0cc52cd1abb697be24e288ffd54f8fae8bf17 From b3e163e44fa51de3388658b0f80c73e8692893c6 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Wed, 5 Sep 2018 16:34:00 -0700 Subject: [PATCH 2/5] Update branding to 2.2.0-preview3 --- version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.props b/version.props index f77be62107..761c13440a 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ 2.2.0 - preview2 + preview3 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 @@ -10,7 +10,7 @@ $(VersionSuffix)-$(BuildNumber) 0.5.0 - preview2 + preview3 $(ExperimentalVersionPrefix) $(ExperimentalVersionPrefix)-$(ExperimentalVersionSuffix)-final $(ExperimentalVersionSuffix)-$(BuildNumber) From f94ad0f2029243f116f7639e50a5c10ef63a63c5 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 9 Sep 2018 12:10:09 -0700 Subject: [PATCH 3/5] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 64 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index cd53ae3566..c3f2510f69 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,43 +3,43 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-20180821.1 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 + 2.2.0-preview1-20180907.8 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 2.0.9 - 2.1.2 - 2.2.0-preview1-26618-02 + 2.1.3 + 2.2.0-preview2-26905-02 15.6.1 4.7.49 2.0.3 11.0.2 - 4.5.1 + 4.6.0-preview2-26905-02 4.5.0 1.6.0 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index ad704918df..312f82f9a5 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180821.1 -commithash:c8d0cc52cd1abb697be24e288ffd54f8fae8bf17 +version:2.2.0-preview1-20180907.8 +commithash:078918eb5c1f176ee1da351c584fb4a4d7491aa0 From 3ac6439dcfde84a4861a5d728123e2a58d96bad4 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 16 Sep 2018 12:09:03 -0700 Subject: [PATCH 4/5] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index c3f2510f69..e80c82a59e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,35 +3,35 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-20180907.8 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 + 2.2.0-preview1-20180911.1 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 2.0.9 2.1.3 2.2.0-preview2-26905-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 312f82f9a5..7124f37441 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180907.8 -commithash:078918eb5c1f176ee1da351c584fb4a4d7491aa0 +version:2.2.0-preview1-20180911.1 +commithash:ddfecdfc6e8e4859db5a0daea578070b862aac65 From 4259b65c1619380a0fc14c1b906269fbf5ae0c82 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 19 Sep 2018 14:48:34 -0700 Subject: [PATCH 5/5] Use options for registering health checks (#479) * Use options for registering health checks This change pivots to use options for registering health checks. We get a few pretty nice things out of this, and it unblocks some of our requirements. Now all registration methods support the application developer configuring the name, failure-status, and tags for each health check. This is a requirment, that we weren't really satisfying - which is what led to this redesign. In support of this health checks now return pass/fail, and the service is responsible for assigning the status. ---- Health check authors that need configuration data (connection string as an example) now have three ways to do this depending on their requirements. 1. Create an instance and register that (easiest) 2. Use Type Activation and pass parameters (middle) 3. Use named options (richest) We expect most health checks to need/want some kind of configuration - which 1) works pretty well to solve. However many other health checks will need DI + configuration. It was also a gap that we didn't have a good way to use named options, when it's such a good fit for our scenarios. Added new registration methods for type activation that allow you to pass parameters for 2). Added a context type that allows the running health check access to its registration for 3). ---- Redesigned and renamed how status gets reported. Health checks return pass/fail result, but the overall HealthReport includes entries of a different type. This seems fine because there isn't really a way to consume a HealthCheckResult directly - the service is the only consumer. ---- Added support for tags. This was easy to add now that we have a separate registration type, and it's quite handy for building filters (see sample). * HARDER BETTER STRONGER FASTER --- .../HealthChecksSample/CustomWriterStartup.cs | 16 +- samples/HealthChecksSample/DBHealthStartup.cs | 2 +- .../DbConnectionHealthCheck.cs | 15 +- .../HealthChecksSample/GCInfoHealthCheck.cs | 59 +++- .../LivenessProbeStartup.cs | 13 +- .../SlowDependencyHealthCheck.cs | 8 +- .../SqlConnectionHealthCheck.cs | 9 +- .../HealthCheckMiddleware.cs | 6 +- .../HealthCheckOptions.cs | 18 +- .../HealthCheckResponseWriters.cs | 2 +- .../HealthCheckContext.cs | 13 + .../HealthCheckRegistration.cs | 132 +++++++++ .../HealthCheckResult.cs | 203 ++++---------- .../{HealthCheckStatus.cs => HealthStatus.cs} | 30 +- .../IHealthCheck.cs | 8 +- .../CompositeHealthCheckResult.cs | 66 ----- .../DefaultHealthCheckService.cs | 149 ++++++++++ ...{HealthCheck.cs => DelegateHealthCheck.cs} | 16 +- .../HealthCheckServiceCollectionExtensions.cs | 10 +- .../HealthChecksBuilder.cs | 33 +++ .../HealthChecksBuilderAddCheckExtensions.cs | 191 +++++++++++++ .../HealthChecksBuilderDelegateExtensions.cs | 169 ++++++++++++ .../IHealthChecksBuilder.cs | 24 ++ .../HealthCheckService.cs | 173 +++--------- .../HealthCheckServiceOptions.cs | 18 ++ .../HealthChecksBuilder.cs | 17 -- .../HealthChecksBuilderAddCheckExtensions.cs | 114 -------- .../HealthReport.cs | 60 ++++ .../HealthReportEntry.cs | 53 ++++ .../IHealthCheckService.cs | 60 ---- .../IHealthChecksBuilder.cs | 22 -- ...Extensions.Diagnostics.HealthChecks.csproj | 5 +- .../HealthCheckMiddlewareTests.cs | 47 ++-- .../CompositeHealthCheckResultTests.cs | 31 --- ...ts.cs => DefaultHealthCheckServiceTest.cs} | 178 +++++++----- .../HealthChecksBuilderTest.cs | 257 ++++++++++++++++++ .../ServiceCollectionExtensionsTest.cs} | 10 +- .../HealthChecksBuilderTests.cs | 33 --- .../HealthReportTest.cs | 31 +++ 39 files changed, 1478 insertions(+), 823 deletions(-) create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs rename src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/{HealthCheckStatus.cs => HealthStatus.cs} (53%) delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs rename src/Microsoft.Extensions.Diagnostics.HealthChecks/{HealthCheck.cs => DelegateHealthCheck.cs} (61%) rename src/Microsoft.Extensions.Diagnostics.HealthChecks/{ => DependencyInjection}/HealthCheckServiceCollectionExtensions.cs (68%) create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs delete mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs rename test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/{HealthCheckServiceTests.cs => DefaultHealthCheckServiceTest.cs} (59%) create mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs rename test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/{ServiceCollectionExtensionsTests.cs => DependencyInjection/ServiceCollectionExtensionsTest.cs} (76%) delete mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs create mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs diff --git a/samples/HealthChecksSample/CustomWriterStartup.cs b/samples/HealthChecksSample/CustomWriterStartup.cs index 0c3e542b72..6e37b1c6ff 100644 --- a/samples/HealthChecksSample/CustomWriterStartup.cs +++ b/samples/HealthChecksSample/CustomWriterStartup.cs @@ -17,14 +17,10 @@ namespace HealthChecksSample public void ConfigureServices(IServiceCollection services) { // Registers required services for health checks - services.AddHealthChecks(); - - // This is an example of registering a custom health check as a service. - // All IHealthCheck services will be available to the health check service and - // middleware. - // - // We recommend registering all health checks as Singleton services. - services.AddSingleton(); + services.AddHealthChecks() + + // Registers a custom health check implementation + .AddGCInfoCheck("GCInfo"); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) @@ -45,13 +41,13 @@ namespace HealthChecksSample }); } - private static Task WriteResponse(HttpContext httpContext, CompositeHealthCheckResult result) + private static Task WriteResponse(HttpContext httpContext, HealthReport result) { httpContext.Response.ContentType = "application/json"; var json = new JObject( new JProperty("status", result.Status.ToString()), - new JProperty("results", new JObject(result.Results.Select(pair => + new JProperty("results", new JObject(result.Entries.Select(pair => new JProperty(pair.Key, new JObject( new JProperty("status", pair.Value.Status.ToString()), new JProperty("description", pair.Value.Description), diff --git a/samples/HealthChecksSample/DBHealthStartup.cs b/samples/HealthChecksSample/DBHealthStartup.cs index 58b6c8d157..43dd6abda6 100644 --- a/samples/HealthChecksSample/DBHealthStartup.cs +++ b/samples/HealthChecksSample/DBHealthStartup.cs @@ -22,7 +22,7 @@ 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"])); + .AddCheck("MyDatabase", new SqlConnectionHealthCheck(Configuration["ConnectionStrings:DefaultConnection"])); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) diff --git a/samples/HealthChecksSample/DbConnectionHealthCheck.cs b/samples/HealthChecksSample/DbConnectionHealthCheck.cs index 00d9bd4803..5a0ddcdd55 100644 --- a/samples/HealthChecksSample/DbConnectionHealthCheck.cs +++ b/samples/HealthChecksSample/DbConnectionHealthCheck.cs @@ -11,20 +11,17 @@ namespace HealthChecksSample { public abstract class DbConnectionHealthCheck : IHealthCheck { - protected DbConnectionHealthCheck(string name, string connectionString) - : this(name, connectionString, testQuery: null) + protected DbConnectionHealthCheck(string connectionString) + : this(connectionString, testQuery: null) { } - protected DbConnectionHealthCheck(string name, string connectionString, string testQuery) + protected DbConnectionHealthCheck(string connectionString, string testQuery) { - Name = name ?? throw new System.ArgumentNullException(nameof(name)); ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); TestQuery = testQuery; } - public string Name { get; } - protected string ConnectionString { get; } // This sample supports specifying a query to run as a boolean test of whether the database @@ -36,7 +33,7 @@ namespace HealthChecksSample protected abstract DbConnection CreateConnection(string connectionString); - public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { using (var connection = CreateConnection(ConnectionString)) { @@ -54,11 +51,11 @@ namespace HealthChecksSample } catch (DbException ex) { - return HealthCheckResult.Unhealthy(ex); + return HealthCheckResult.Failed(exception: ex); } } - return HealthCheckResult.Healthy(); + return HealthCheckResult.Passed(); } } } diff --git a/samples/HealthChecksSample/GCInfoHealthCheck.cs b/samples/HealthChecksSample/GCInfoHealthCheck.cs index f37ade8980..20bcc1eb11 100644 --- a/samples/HealthChecksSample/GCInfoHealthCheck.cs +++ b/samples/HealthChecksSample/GCInfoHealthCheck.cs @@ -2,21 +2,58 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; namespace HealthChecksSample { // This is an example of a custom health check that implements IHealthCheck. // - // See CustomWriterStartup to see how this is registered. + // This example also shows a technique for authoring a health check that needs to be registered + // with additional configuration data. This technique works via named options, and is useful + // for authoring health checks that can be disctributed as libraries. + + public static class GCInfoHealthCheckBuilderExtensions + { + public static IHealthChecksBuilder AddGCInfoCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus = null, + IEnumerable tags = null, + long? thresholdInBytes = null) + { + // Register a check of type GCInfo + builder.AddCheck(name, failureStatus ?? HealthStatus.Degraded, tags); + + // Configure named options to pass the threshold into the check. + if (thresholdInBytes.HasValue) + { + builder.Services.Configure(name, options => + { + options.Threshold = thresholdInBytes.Value; + }); + } + + return builder; + } + } + public class GCInfoHealthCheck : IHealthCheck { - public string Name { get; } = "GCInfo"; + private readonly IOptionsMonitor _options; - public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + public GCInfoHealthCheck(IOptionsMonitor options) { + _options = options; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) + { + var options = _options.Get(context.Registration.Name); + // This example will report degraded status if the application is using - // more than 1gb of memory. + // more than the configured amount of memory (1gb by default). // // Additionally we include some GC info in the reported diagnostics. var allocated = GC.GetTotalMemory(forceFullCollection: false); @@ -28,14 +65,20 @@ namespace HealthChecksSample { "Gen2Collections", GC.CollectionCount(2) }, }; - // Report degraded status if the allocated memory is >= 1gb (in bytes) - var status = allocated >= 1024 * 1024 * 1024 ? HealthCheckStatus.Degraded : HealthCheckStatus.Healthy; + // Report failure if the allocated memory is >= the threshold + var result = allocated >= options.Threshold; return Task.FromResult(new HealthCheckResult( - status, - exception: null, + result, description: "reports degraded status if allocated bytes >= 1gb", + exception: null, data: data)); } } + + public class GCInfoOptions + { + // The failure threshold (in bytes) + public long Threshold { get; set; } = 1024L * 1024L * 1024L; + } } diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs index aa644d231f..82676221ce 100644 --- a/samples/HealthChecksSample/LivenessProbeStartup.cs +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -1,11 +1,9 @@ using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthChecksSample { @@ -17,7 +15,7 @@ namespace HealthChecksSample // Registers required services for health checks services .AddHealthChecks() - .AddCheck(new SlowDependencyHealthCheck()); + .AddCheck("Slow", failureStatus: null, tags: new[] { "ready", }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) @@ -46,10 +44,13 @@ namespace HealthChecksSample // long initialization time (15 seconds). - // The readiness check uses all of the registered health checks (default) - app.UseHealthChecks("/health/ready"); + // The readiness check uses all registered checks with the 'ready' tag. + app.UseHealthChecks("/health/ready", new HealthCheckOptions() + { + Predicate = (check) => check.Tags.Contains("ready"), + }); - // The liveness check uses an 'identity' health check that always returns healthy + // The liveness filters out all checks and just returns success app.UseHealthChecks("/health/live", new HealthCheckOptions() { // Exclude all checks, just return a 200. diff --git a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs index 7832724c81..e319952cf0 100644 --- a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs +++ b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs @@ -17,16 +17,14 @@ namespace HealthChecksSample _task = Task.Delay(15 * 1000); } - public string Name => HealthCheckName; - - public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { if (_task.IsCompleted) { - return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready")); + return Task.FromResult(HealthCheckResult.Passed("Dependency is ready")); } - return Task.FromResult(HealthCheckResult.Unhealthy("Dependency is still initializing")); + return Task.FromResult(HealthCheckResult.Failed("Dependency is still initializing")); } } } diff --git a/samples/HealthChecksSample/SqlConnectionHealthCheck.cs b/samples/HealthChecksSample/SqlConnectionHealthCheck.cs index 5849da2202..fcc135ad1e 100644 --- a/samples/HealthChecksSample/SqlConnectionHealthCheck.cs +++ b/samples/HealthChecksSample/SqlConnectionHealthCheck.cs @@ -1,6 +1,5 @@ using System.Data.Common; using System.Data.SqlClient; -using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthChecksSample { @@ -8,13 +7,13 @@ namespace HealthChecksSample { private static readonly string DefaultTestQuery = "Select 1"; - public SqlConnectionHealthCheck(string name, string connectionString) - : this(name, connectionString, testQuery: DefaultTestQuery) + public SqlConnectionHealthCheck(string connectionString) + : this(connectionString, testQuery: DefaultTestQuery) { } - public SqlConnectionHealthCheck(string name, string connectionString, string testQuery) - : base(name, connectionString, testQuery ?? DefaultTestQuery) + public SqlConnectionHealthCheck(string connectionString, string testQuery) + : base(connectionString, testQuery ?? DefaultTestQuery) { } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 20ec5e356e..e06ef8509b 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -15,12 +15,12 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { private readonly RequestDelegate _next; private readonly HealthCheckOptions _healthCheckOptions; - private readonly IHealthCheckService _healthCheckService; + private readonly HealthCheckService _healthCheckService; public HealthCheckMiddleware( RequestDelegate next, IOptions healthCheckOptions, - IHealthCheckService healthCheckService) + HealthCheckService healthCheckService) { if (next == null) { @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) { var message = - $"No status code mapping found for {nameof(HealthCheckStatus)} value: {result.Status}." + + $"No status code mapping found for {nameof(HealthStatus)} value: {result.Status}." + $"{nameof(HealthCheckOptions)}.{nameof(HealthCheckOptions.ResultStatusCodes)} must contain" + $"an entry for {result.Status}."; diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index 7930d25577..42996e3701 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -22,20 +22,20 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// registered health checks - this is the default behavior. To run a subset of health checks, /// provide a function that filters the set of checks. /// - public Func Predicate { get; set; } + public Func Predicate { get; set; } /// - /// Gets a dictionary mapping the to an HTTP status code applied to the response. + /// 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() + public IDictionary ResultStatusCodes { get; } = new Dictionary() { - { HealthCheckStatus.Healthy, StatusCodes.Status200OK }, - { HealthCheckStatus.Degraded, StatusCodes.Status200OK }, - { HealthCheckStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, + { HealthStatus.Healthy, StatusCodes.Status200OK }, + { HealthStatus.Degraded, StatusCodes.Status200OK }, + { HealthStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, // This means that a health check failed, so 500 is appropriate. This is an error. - { HealthCheckStatus.Failed, StatusCodes.Status500InternalServerError }, + { HealthStatus.Failed, StatusCodes.Status500InternalServerError }, }; /// @@ -43,8 +43,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// /// /// The default value is a delegate that will write a minimal text/plain response with the value - /// of as a string. + /// of as a string. /// - public Func ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; + public Func ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs index a74072f63f..50cb8c736e 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { internal static class HealthCheckResponseWriters { - public static Task WriteMinimalPlaintext(HttpContext httpContext, CompositeHealthCheckResult result) + public static Task WriteMinimalPlaintext(HttpContext httpContext, HealthReport result) { httpContext.Response.ContentType = "text/plain"; return httpContext.Response.WriteAsync(result.Status.ToString()); diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs new file mode 100644 index 0000000000..027451c0d2 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public sealed class HealthCheckContext + { + /// + /// Gets or sets the of the currently executing . + /// + public HealthCheckRegistration Registration { get; set; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs new file mode 100644 index 0000000000..9291c38846 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs @@ -0,0 +1,132 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represent the registration information associated with an implementation. + /// + /// + /// + /// The health check registration is provided as a separate object so that application developers can customize + /// how health check implementations are configured. + /// + /// + /// The registration is provided to an implementation during execution through + /// . This allows a health check implementation to access named + /// options or perform other operations based on the registered name. + /// + /// + public sealed class HealthCheckRegistration + { + private Func _factory; + private string _name; + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// The instance. + /// + /// The that should be reported upon failure of the health check. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable tags) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + Name = name; + FailureStatus = failureStatus ?? HealthStatus.Unhealthy; + Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + Factory = (_) => instance; + } + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// A delegate used to create the instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + public HealthCheckRegistration( + string name, + Func factory, + HealthStatus? failureStatus, + IEnumerable tags) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + Name = name; + FailureStatus = failureStatus ?? HealthStatus.Unhealthy; + Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + Factory = factory; + } + + /// + /// Gets or sets a delegate used to create the instance. + /// + public Func Factory + { + get => _factory; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _factory = value; + } + } + + /// + /// Gets or sets the that should be reported upon failure of the health check. + /// + public HealthStatus FailureStatus { get; set; } + + /// + /// Gets or sets the health check name. + /// + public string Name + { + get => _name; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _name = value; + } + } + + /// + /// Gets a list of tags that can be used for filtering health checks. + /// + public ISet Tags { get; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs index e672c88db5..12cd38d08e 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs @@ -13,176 +13,65 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { private static readonly IReadOnlyDictionary _emptyReadOnlyDictionary = new Dictionary(); - private string _description; - private IReadOnlyDictionary _data; - /// - /// Gets a value indicating the status of the component that was checked. + /// Creates a new with the specified values for , , + /// , and . /// - public HealthCheckStatus Status { get; } - - /// - /// Gets an representing the exception that was thrown when checking for status (if any). - /// - /// - /// This value is expected to be 'null' if is . - /// - public Exception Exception { get; } - - /// - /// Gets a human-readable description of the status of the component that was checked. - /// - public string Description => _description ?? string.Empty; + /// A value indicating the pass/fail status of the component that was checked. + /// A human-readable description of the status of the component that was checked. + /// An representing the exception that was thrown when checking for status (if any). + /// Additional key-value pairs describing the health of the component. + public HealthCheckResult(bool result, string description, Exception exception, IReadOnlyDictionary data) + { + Result = result; + Description = description; + Exception = exception; + Data = data ?? _emptyReadOnlyDictionary; + } /// /// Gets additional key-value pairs describing the health of the component. /// - public IReadOnlyDictionary Data => _data ?? _emptyReadOnlyDictionary; + public IReadOnlyDictionary Data { get; } /// - /// Creates a new with the specified , , - /// , and . + /// Gets a human-readable description of the status of the component that was checked. /// - /// A value indicating the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public HealthCheckResult(HealthCheckStatus status, Exception exception, string description, IReadOnlyDictionary data) - { - if (status == HealthCheckStatus.Unknown) - { - throw new ArgumentException($"'{nameof(HealthCheckStatus.Unknown)}' is not a valid value for the 'status' parameter.", nameof(status)); - } + public string Description { get; } - Status = status; - Exception = exception; - _description = description; - _data = data; + /// + /// Gets an representing the exception that was thrown when checking for status (if any). + /// + public Exception Exception { get; } + + /// + /// Gets a value indicating the pass/fail status of the component that was checked. If true, then the component + /// is considered to have passed health validation. A false value will be mapped to the configured + /// by the health check system. + /// + public bool Result { get; } + + /// + /// Creates a representing a passing component. + /// + /// A representing a passing component. + /// A human-readable description of the status of the component that was checked. Optional. + /// Additional key-value pairs describing the health of the component. Optional. + public static HealthCheckResult Passed(string description = null, IReadOnlyDictionary data = null) + { + return new HealthCheckResult(result: true, description, exception: null, data); } /// - /// Creates a representing an unhealthy component. + /// Creates a representing an failing component. /// - /// A representing an unhealthy component. - public static HealthCheckResult Unhealthy() - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - public static HealthCheckResult Unhealthy(string description) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Unhealthy(string description, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: data); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// An representing the exception that was thrown when checking for status (if any). - public static HealthCheckResult Unhealthy(Exception exception) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description: string.Empty, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - public static HealthCheckResult Unhealthy(string description, Exception exception) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Unhealthy(string description, Exception exception, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data); - - /// - /// Creates a representing a healthy component. - /// - /// A representing a healthy component. - public static HealthCheckResult Healthy() - => new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing a healthy component. - /// - /// A representing a healthy component. - /// A human-readable description of the status of the component that was checked. - public static HealthCheckResult Healthy(string description) - => new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: null); - - /// - /// Creates a representing a healthy component. - /// - /// A representing a healthy component. - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Healthy(string description, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: data); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - public static HealthCheckResult Degraded() - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - public static HealthCheckResult Degraded(string description) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Degraded(string description, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: data); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - public static HealthCheckResult Degraded(Exception exception) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - public static HealthCheckResult Degraded(string description, Exception exception) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Degraded(string description, Exception exception, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data); + /// A human-readable description of the status of the component that was checked. Optional. + /// An representing the exception that was thrown when checking for status. Optional. + /// Additional key-value pairs describing the health of the component. Optional. + /// A representing an failing component. + public static HealthCheckResult Failed(string description = null, Exception exception = null, IReadOnlyDictionary data = null) + { + return new HealthCheckResult(result: false, description, exception, data); + } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckStatus.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs similarity index 53% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckStatus.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs index c16739eadc..d4293cb7b4 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckStatus.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs @@ -4,38 +4,38 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { /// - /// Represents the status of a health check result. + /// Represents the reported status of a health check result. /// /// - /// The values of this enum or ordered from least healthy to most healthy. So is - /// greater than but less than . + /// + /// A health status is derived the pass/fail result of an () + /// and the corresponding value of . + /// + /// + /// The values of this enum or ordered from least healthy to most healthy. So is + /// greater than but less than . + /// /// - public enum HealthCheckStatus + public enum HealthStatus { /// - /// This value should not be returned by a health check. It is used to represent an uninitialized value. + /// Indicates that an unexpected exception was thrown when running the health check. /// - Unknown = 0, - - /// - /// This value should not be returned by a health check. It is used to indicate that an unexpected exception was - /// thrown when running the health check. - /// - Failed = 1, + Failed = 0, /// /// Indicates that the health check determined that the component was unhealthy. /// - Unhealthy = 2, + Unhealthy = 1, /// /// Indicates that the health check determined that the component was in a degraded state. /// - Degraded = 3, + Degraded = 2, /// /// Indicates that the health check determined that the component was healthy. /// - Healthy = 4, + Healthy = 3, } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs index 2bf751180d..1b69953b67 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs @@ -12,16 +12,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// public interface IHealthCheck { - /// - /// Gets the name of the health check, which should indicate the component being checked. - /// - string Name { get; } - /// /// Runs the health check, returning the status of the component being checked. /// + /// A context object associated with the current execution. /// A that can be used to cancel the health check. /// A that completes when the health check has finished, yielding the status of the component being checked. - Task CheckHealthAsync(CancellationToken cancellationToken = default); + Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs deleted file mode 100644 index 900b15bd71..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs +++ /dev/null @@ -1,66 +0,0 @@ -// 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; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - /// - /// Represents the results of multiple health checks. - /// - public class CompositeHealthCheckResult - { - /// - /// A containing the results from each health check. - /// - /// - /// The keys in this dictionary map to the name of the health check, the values are the - /// returned when was called for that health check. - /// - public IReadOnlyDictionary Results { get; } - - /// - /// Gets a representing the aggregate status of all the health checks. - /// - /// - /// This value is determined by taking the "worst" result of all the results. So if any result is , - /// this value is . If no result is but any result is - /// , this value is , etc. - /// - public HealthCheckStatus Status { get; } - - /// - /// Create a new from the specified results. - /// - /// A containing the results from each health check. - public CompositeHealthCheckResult(IReadOnlyDictionary results) - { - Results = results; - Status = CalculateAggregateStatus(results.Values); - } - - private HealthCheckStatus CalculateAggregateStatus(IEnumerable results) - { - // This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list - var currentValue = HealthCheckStatus.Healthy; - foreach (var result in results) - { - if (currentValue > result.Status) - { - currentValue = result.Status; - } - - if (currentValue == HealthCheckStatus.Failed) - { - // Game over, man! Game over! - // (We hit the worst possible status, so there's no need to keep iterating) - return currentValue; - } - } - - return currentValue; - } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs new file mode 100644 index 0000000000..b7a34ff35a --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs @@ -0,0 +1,149 @@ +// 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; +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.Options; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + internal class DefaultHealthCheckService : HealthCheckService + { + private readonly IServiceScopeFactory _scopeFactory; + private readonly IOptions _options; + private readonly ILogger _logger; + + public DefaultHealthCheckService( + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger) + { + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + // 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. + ValidateRegistrations(_options.Value.Registrations); + } + public override async Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default) + { + var registrations = _options.Value.Registrations; + + using (var scope = _scopeFactory.CreateScope()) + { + var context = new HealthCheckContext(); + var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var registration in registrations) + { + if (predicate != null && !predicate(registration)) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var healthCheck = registration.Factory(scope.ServiceProvider); + + // 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(registration.Name))) + { + var stopwatch = ValueStopwatch.StartNew(); + context.Registration = registration; + + Log.HealthCheckBegin(_logger, registration); + + HealthReportEntry entry; + try + { + var result = await healthCheck.CheckHealthAsync(context, cancellationToken); + + entry = new HealthReportEntry( + result.Result ? HealthStatus.Healthy : registration.FailureStatus, + result.Description, + result.Exception, + result.Data); + + Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime()); + } + catch (Exception ex) + { + entry = new HealthReportEntry(HealthStatus.Failed, ex.Message, ex, data: null); + Log.HealthCheckError(_logger, registration, ex, stopwatch.GetElapsedTime()); + } + + entries[registration.Name] = entry; + } + } + + return new HealthReport(entries); + } + } + + private static void ValidateRegistrations(IEnumerable registrations) + { + // Scan the list for duplicate names to provide a better error if there are duplicates. + var duplicateNames = registrations + .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(registrations)); + } + } + + 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 after {ElapsedMilliseconds}ms"); + + public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration) + { + _healthCheckBegin(logger, registration.Name, null); + } + + public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration) + { + _healthCheckEnd(logger, registration.Name, duration.TotalMilliseconds, entry.Status, null); + } + + public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration) + { + _healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception); + } + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DelegateHealthCheck.cs similarity index 61% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks/DelegateHealthCheck.cs index da6a92d062..94069fd7d1 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DelegateHealthCheck.cs @@ -11,31 +11,25 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// A simple implementation of which uses a provided delegate to /// implement the check. /// - public sealed class HealthCheck : IHealthCheck + internal sealed class DelegateHealthCheck : IHealthCheck { private readonly Func> _check; /// - /// Create an instance of from the specified and . + /// Create an instance of from the specified delegate. /// - /// The name of the health check, which should indicate the component being checked. /// A delegate which provides the code to execute when the health check is run. - public HealthCheck(string name, Func> check) + public DelegateHealthCheck(Func> check) { - Name = name ?? throw new ArgumentNullException(nameof(name)); _check = check ?? throw new ArgumentNullException(nameof(check)); } - /// - /// Gets the name of the health check, which should indicate the component being checked. - /// - public string Name { get; } - /// /// Runs the health check, returning the status of the component being checked. /// + /// A context object associated with the current execution. /// A that can be used to cancel the health check. /// A that completes when the health check has finished, yielding the status of the component being checked. - public Task CheckHealthAsync(CancellationToken cancellationToken = default) => _check(cancellationToken); + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) => _check(cancellationToken); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceCollectionExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs similarity index 68% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceCollectionExtensions.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs index f0caa8db1f..76b0dc45c1 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceCollectionExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs @@ -7,24 +7,24 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Microsoft.Extensions.DependencyInjection { /// - /// Provides extension methods for registering in an . + /// Provides extension methods for registering in an . /// public static class HealthCheckServiceCollectionExtensions { /// - /// Adds the to the container, using the provided delegate to register + /// Adds the to the container, using the provided delegate to register /// health checks. /// /// /// This operation is idempotent - multiple invocations will still only result in a single - /// instance in the . It can be invoked + /// instance in the . It can be invoked /// multiple times in order to get access to the in multiple places. /// - /// The to add the to. + /// The to add the to. /// An instance of from which health checks can be registered. public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); return new HealthChecksBuilder(services); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs new file mode 100644 index 0000000000..231dd51717 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs @@ -0,0 +1,33 @@ +// 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; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + internal class HealthChecksBuilder : IHealthChecksBuilder + { + public HealthChecksBuilder(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + + public IHealthChecksBuilder Add(HealthCheckRegistration registration) + { + if (registration == null) + { + throw new ArgumentNullException(nameof(registration)); + } + + Services.Configure(options => + { + options.Registrations.Add(registration); + }); + + return this; + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs new file mode 100644 index 0000000000..9508889054 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs @@ -0,0 +1,191 @@ +// 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; +using System.Collections.Generic; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Provides basic extension methods for registering instances in an . + /// + public static class HealthChecksBuilderAddCheckExtensions + { + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// An instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + IHealthCheck instance, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. If a service of type is registred in the dependency injection container + /// with any liftime it will be used. Otherwise an instance of type will be constructed with + /// access to services from the dependency injection container. + /// + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus = null, + IEnumerable tags = null) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.GetServiceOrCreateInstance(s), failureStatus, tags)); + } + + // NOTE: AddTypeActivatedCheck has overloads rather than default parameters values, because default parameter values don't + // play super well with params. + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// Additional arguments to provide to the constructor. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck(this IHealthChecksBuilder builder, string name, params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return AddTypeActivatedCheck(builder, name, failureStatus: null, tags: null); + } + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// Additional arguments to provide to the constructor. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return AddTypeActivatedCheck(builder, name, failureStatus, tags: null); + } + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// Additional arguments to provide to the constructor. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + IEnumerable tags, + params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.CreateInstance(s, args), failureStatus, tags)); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs new file mode 100644 index 0000000000..70edd0649d --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs @@ -0,0 +1,169 @@ +// 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; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Provides extension methods for registering delegates with the . + /// + public static class HealthChecksBuilderDelegateExtensions + { + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => Task.FromResult(check())); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct))); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => check()); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => check(ct)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs new file mode 100644 index 0000000000..eb78293f87 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs @@ -0,0 +1,24 @@ +// 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 Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// A builder used to register health checks. + /// + public interface IHealthChecksBuilder + { + /// + /// Adds a for a health check. + /// + /// The . + IHealthChecksBuilder Add(HealthCheckRegistration registration); + + /// + /// Gets the into which instances should be registered. + /// + IServiceCollection Services { get; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs index 111113ea77..e4a128148d 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs @@ -2,143 +2,60 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -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; namespace Microsoft.Extensions.Diagnostics.HealthChecks { - internal class HealthCheckService : IHealthCheckService + /// + /// 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. + /// + /// + /// implementations can be registered through extension methods provided by + /// . + /// + /// + public abstract class HealthCheckService { - private readonly IServiceScopeFactory _scopeFactory; - private readonly ILogger _logger; - - public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger logger) + /// + /// 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) { - _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - // 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()) - { - var healthChecks = scope.ServiceProvider.GetRequiredService>(); - EnsureNoDuplicates(healthChecks); - } + return CheckHealthAsync(predicate: null, cancellationToken); } - public Task CheckHealthAsync(CancellationToken cancellationToken = default) => - CheckHealthAsync(predicate: null, cancellationToken); - - public async Task CheckHealthAsync( - Func predicate, - CancellationToken cancellationToken = default) - { - using (var scope = _scopeFactory.CreateScope()) - { - var healthChecks = scope.ServiceProvider.GetRequiredService>(); - - var results = new Dictionary(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); - } - } - - 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); - } - } + /// + /// Runs the provided health checks and returns the aggregated status + /// + /// + /// 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. + /// + public abstract Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs new file mode 100644 index 0000000000..b8dfdb9b40 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Options for the default implementation of + /// + public sealed class HealthCheckServiceOptions + { + /// + /// Gets the health check registrations. + /// + public ICollection Registrations { get; } = new List(); + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs deleted file mode 100644 index 4e1d851eff..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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 Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - internal class HealthChecksBuilder : IHealthChecksBuilder - { - public IServiceCollection Services { get; } - - public HealthChecksBuilder(IServiceCollection services) - { - Services = services; - } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs deleted file mode 100644 index d3af5b17c2..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -// 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; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// Provides extension methods for registering instances in an . - /// - public static class HealthChecksBuilderAddCheckExtensions - { - /// - /// Adds a new health check with the specified name and implementation. - /// - /// The to add the check to. - /// The name of the health check, which should indicate the component being checked. - /// A delegate which provides the code to execute when the health check is run. - /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (check == null) - { - throw new ArgumentNullException(nameof(check)); - } - - return builder.AddCheck(new HealthCheck(name, check)); - } - - /// - /// Adds a new health check with the specified name and implementation. - /// - /// The to add the check to. - /// The name of the health check, which should indicate the component being checked. - /// A delegate which provides the code to execute when the health check is run. - /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (check == null) - { - throw new ArgumentNullException(nameof(check)); - } - - return builder.AddCheck(name, _ => check()); - } - - /// - /// Adds a new health check with the provided implementation. - /// - /// The to add the check to. - /// An implementation. - /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, IHealthCheck check) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (check == null) - { - throw new ArgumentNullException(nameof(check)); - } - - 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/HealthReport.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs new file mode 100644 index 0000000000..29359a547f --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs @@ -0,0 +1,60 @@ +// 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; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represents the result of executing a group of instances. + /// + public sealed class HealthReport + { + /// + /// Create a new from the specified results. + /// + /// A containing the results from each health check. + public HealthReport(IReadOnlyDictionary entries) + { + Entries = entries; + Status = CalculateAggregateStatus(entries.Values); + } + + /// + /// A containing the results from each health check. + /// + /// + /// The keys in this dictionary map the name of each executed health check to a for the + /// result data retruned from the corresponding health check. + /// + public IReadOnlyDictionary Entries { get; } + + /// + /// Gets a representing the aggregate status of all the health checks. The value of + /// will be the most servere status reported by a health check. If no checks were executed, the value is always . + /// + public HealthStatus Status { get; } + + private HealthStatus CalculateAggregateStatus(IEnumerable entries) + { + // This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list + var currentValue = HealthStatus.Healthy; + foreach (var entry in entries) + { + if (currentValue > entry.Status) + { + currentValue = entry.Status; + } + + if (currentValue == HealthStatus.Failed) + { + // Game over, man! Game over! + // (We hit the worst possible status, so there's no need to keep iterating) + return currentValue; + } + } + + return currentValue; + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs new file mode 100644 index 0000000000..17ed5ae288 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs @@ -0,0 +1,53 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represents an entry in a . Corresponds to the result of a single . + /// + public struct HealthReportEntry + { + private static readonly IReadOnlyDictionary _emptyReadOnlyDictionary = new Dictionary(); + + /// + /// Creates a new with the specified values for , , + /// , and . + /// + /// A value indicating the health status of the component that was checked. + /// A human-readable description of the status of the component that was checked. + /// An representing the exception that was thrown when checking for status (if any). + /// Additional key-value pairs describing the health of the component. + public HealthReportEntry(HealthStatus status, string description, Exception exception, IReadOnlyDictionary data) + { + Status = status; + Description = description; + Exception = exception; + Data = data ?? _emptyReadOnlyDictionary; + } + + /// + /// Gets additional key-value pairs describing the health of the component. + /// + public IReadOnlyDictionary Data { get; } + + /// + /// Gets a human-readable description of the status of the component that was checked. + /// + public string Description { get; } + + /// + /// Gets an representing the exception that was thrown when checking for status (if any). + /// + public Exception Exception { get; } + + /// + /// Gets the health status of the component that was checked. The is based on the pass/fail value of + /// and the configured value of . + /// + public HealthStatus Status { get; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs deleted file mode 100644 index f931475a4d..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs +++ /dev/null @@ -1,60 +0,0 @@ -// 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; -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. - /// - /// - /// - /// 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 - { - /// - /// 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. - /// - Task CheckHealthAsync(CancellationToken cancellationToken = default); - - /// - /// Runs the provided health checks and returns the aggregated status - /// - /// - /// 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(Func predicate, - CancellationToken cancellationToken = default); - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs deleted file mode 100644 index 22cd691aa2..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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 Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - /// - /// A builder used to collect instances of and register them on an . - /// - /// - /// This type wraps an and provides a place for health check components to attach extension - /// methods for registering themselves in the . - /// - public interface IHealthChecksBuilder - { - /// - /// Gets the into which instances should be registered. - /// - IServiceCollection Services { get; } - } -} 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 2b3d0e6261..725226023e 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -3,8 +3,8 @@ Components for performing health checks in .NET applications Commonly Used Types: -Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService -Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder +Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService +Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder netstandard2.0 $(NoWarn);CS1591 @@ -14,6 +14,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder + diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 16fe68ac2a..7a2435948c 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -1,8 +1,6 @@ // 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; -using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -100,7 +98,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } - [Fact] public async Task StatusCodeIs200IfAllChecksHealthy() { @@ -112,9 +109,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!")) + .AddCheck("Bar", () => HealthCheckResult.Passed("A-ok!")) + .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!")); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -137,9 +134,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Not so great."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!")) + .AddCheck("Bar", () => HealthCheckResult.Failed("Not so great."), failureStatus: HealthStatus.Degraded) + .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!")); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -162,9 +159,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -187,9 +184,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(new HealthCheckResult(HealthCheckStatus.Failed, null, null, null))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => throw null) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -225,9 +222,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -254,9 +251,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -277,7 +274,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { ResultStatusCodes = { - [HealthCheckStatus.Healthy] = 201, + [HealthStatus.Healthy] = 201, } }); }) @@ -308,10 +305,10 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) // Will get filtered out - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("A-ok!"))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs deleted file mode 100644 index 3852969cda..0000000000 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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 Xunit; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Tests -{ - public class CompositeHealthCheckResultTests - { - [Theory] - [InlineData(HealthCheckStatus.Healthy)] - [InlineData(HealthCheckStatus.Degraded)] - [InlineData(HealthCheckStatus.Unhealthy)] - [InlineData(HealthCheckStatus.Failed)] - public void Status_MatchesWorstStatusInResults(HealthCheckStatus statusValue) - { - var result = new CompositeHealthCheckResult(new Dictionary() - { - {"Foo", HealthCheckResult.Healthy() }, - {"Bar", HealthCheckResult.Healthy() }, - {"Baz", new HealthCheckResult(statusValue, exception: null, description: null, data: null) }, - {"Quick", HealthCheckResult.Healthy() }, - {"Quack", HealthCheckResult.Healthy() }, - {"Quock", HealthCheckResult.Healthy() }, - }); - - Assert.Equal(statusValue, result.Status); - } - } -} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs similarity index 59% rename from test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs rename to test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs index 6e7c28e9ef..024309a024 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs @@ -8,11 +8,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.Extensions.Diagnostics.HealthChecks { - public class HealthCheckServiceTests + public class DefaultHealthCheckServiceTest { [Fact] public void Constructor_ThrowsUsefulExceptionForDuplicateNames() @@ -25,22 +26,23 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks 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()))); + .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))); var services = serviceCollection.BuildServiceProvider(); - + var scopeFactory = services.GetRequiredService(); - var logger = services.GetRequiredService>(); + var options = services.GetRequiredService>(); + var logger = services.GetRequiredService>(); // Act - var exception = Assert.Throws(() => new HealthCheckService(scopeFactory, logger)); + var exception = Assert.Throws(() => new DefaultHealthCheckService(scopeFactory, options, logger)); // Assert - Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message); + Assert.StartsWith($"Duplicate health checks were registered with the name(s): Foo, Baz", exception.Message); } [Fact] @@ -61,21 +63,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - 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))); + b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data))); + b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded); + b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Results, + Assert.Collection(results.Entries, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Collection(actual.Value.Data, item => { @@ -87,7 +89,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { Assert.Equal("DegradedCheck", actual.Key); Assert.Equal(DegradedMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status); + Assert.Equal(HealthStatus.Degraded, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Empty(actual.Value.Data); }, @@ -95,7 +97,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { Assert.Equal("UnhealthyCheck", actual.Key); Assert.Equal(UnhealthyMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Unhealthy, actual.Value.Status); + Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); Assert.Same(exception, actual.Value.Exception); Assert.Empty(actual.Value.Data); }); @@ -119,21 +121,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - 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))); + b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data))); + b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded); + b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck"); // Assert - Assert.Collection(results.Results, + Assert.Collection(results.Entries, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Collection(actual.Value.Data, item => { @@ -144,7 +146,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks } [Fact] - public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckerToFailedResultAsync() + public async Task CheckHealthAsync_SetsRegistrationForEachCheck() { // Arrange var thrownException = new InvalidOperationException("Whoops!"); @@ -152,35 +154,79 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - b.AddCheck("Throws", ct => throw thrownException); - b.AddCheck("Faults", ct => Task.FromException(faultedException)); - b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())); + b.AddCheck("A"); + b.AddCheck("B"); + b.AddCheck("C"); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Results, + Assert.Collection( + results.Entries, + actual => + { + Assert.Equal("A", actual.Key); + Assert.Collection( + actual.Value.Data, + kvp => Assert.Equal(kvp, new KeyValuePair("name", "A"))); + }, + actual => + { + Assert.Equal("B", actual.Key); + Assert.Collection( + actual.Value.Data, + kvp => Assert.Equal(kvp, new KeyValuePair("name", "B"))); + }, + actual => + { + Assert.Equal("C", actual.Key); + Assert.Collection( + actual.Value.Data, + kvp => Assert.Equal(kvp, new KeyValuePair("name", "C"))); + }); + } + + [Fact] + public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToFailedResultAsync() + { + // Arrange + var thrownException = new InvalidOperationException("Whoops!"); + var faultedException = new InvalidOperationException("Ohnoes!"); + + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("Throws", ct => throw thrownException); + b.AddAsyncCheck("Faults", ct => Task.FromException(faultedException)); + b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Passed())); + }); + + // Act + var results = await service.CheckHealthAsync(); + + // Assert + Assert.Collection( + results.Entries, actual => { Assert.Equal("Throws", actual.Key); Assert.Equal(thrownException.Message, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status); + Assert.Equal(HealthStatus.Failed, actual.Value.Status); Assert.Same(thrownException, actual.Value.Exception); }, actual => { Assert.Equal("Faults", actual.Key); Assert.Equal(faultedException.Message, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status); + Assert.Equal(HealthStatus.Failed, actual.Value.Status); Assert.Same(faultedException, actual.Value.Exception); }, actual => { Assert.Equal("Succeeds", actual.Key); - Assert.Empty(actual.Value.Description); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Null(actual.Value.Description); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); }); } @@ -190,12 +236,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { // Arrange var sink = new TestSink(); - var check = new HealthCheck("TestScope", cancellationToken => + var check = new DelegateHealthCheck(cancellationToken => { Assert.Collection(sink.Scopes, actual => { - Assert.Equal(actual.LoggerName, typeof(HealthCheckService).FullName); + Assert.Equal(actual.LoggerName, typeof(DefaultHealthCheckService).FullName); Assert.Collection((IEnumerable>)actual.Scope, item => { @@ -203,7 +249,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks Assert.Equal("TestScope", item.Value); }); }); - return Task.FromResult(HealthCheckResult.Healthy()); + return Task.FromResult(HealthCheckResult.Passed()); }); var loggerFactory = new TestLoggerFactory(sink, enabled: true); @@ -212,36 +258,20 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Override the logger factory for testing b.Services.AddSingleton(loggerFactory); - b.AddCheck(check); + b.AddCheck("TestScope", check); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Results, actual => + Assert.Collection(results.Entries, actual => { Assert.Equal("TestScope", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } - [Fact] - public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult() - { - // Arrange - var service = CreateHealthChecksService(b => - { - b.AddCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))); - }); - - // Act - var ex = await Assert.ThrowsAsync(() => service.CheckHealthAsync()); - - // Assert - Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message); - } - [Fact] public async Task CheckHealthAsync_CheckCanDependOnTransientService() { @@ -250,7 +280,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.Services.AddTransient(); - b.AddCheck(); + b.AddCheck("Test"); }); // Act @@ -258,11 +288,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Collection( - results.Results, + results.Entries, actual => { Assert.Equal("Test", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } @@ -274,7 +304,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.Services.AddScoped(); - b.AddCheck(); + b.AddCheck("Test"); }); // Act @@ -282,11 +312,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Collection( - results.Results, + results.Entries, actual => { Assert.Equal("Test", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } @@ -298,7 +328,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.Services.AddSingleton(); - b.AddCheck(); + b.AddCheck("Test"); }); // Act @@ -306,15 +336,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Collection( - results.Results, + results.Entries, actual => { Assert.Equal("Test", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } - private static HealthCheckService CreateHealthChecksService(Action configure) + private static DefaultHealthCheckService CreateHealthChecksService(Action configure) { var services = new ServiceCollection(); services.AddLogging(); @@ -326,7 +356,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks configure(builder); } - return (HealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService(); + return (DefaultHealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService(); } private class AnotherService { } @@ -336,12 +366,22 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public CheckWithServiceDependency(AnotherService _) { } - - public string Name => "Test"; - - public Task CheckHealthAsync(CancellationToken cancellationToken = default) + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - return Task.FromResult(HealthCheckResult.Healthy()); + return Task.FromResult(HealthCheckResult.Passed()); + } + } + + private class NameCapturingCheck : IHealthCheck + { + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var data = new Dictionary() + { + { "name", context.Registration.Name }, + }; + return Task.FromResult(HealthCheckResult.Passed(data: data)); } } } diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs new file mode 100644 index 0000000000..c4974013c7 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs @@ -0,0 +1,257 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection +{ + // Integration tests for extension methods on IHealthCheckBuilder + // + // We test the longest overload of each 'family' of Add...Check methods, since they chain to each other. + public class HealthChecksBuilderTest + { + [Fact] + public void AddCheck_Instance() + { + // Arrange + var instance = new DelegateHealthCheck((_) => + { + return Task.FromResult(HealthCheckResult.Passed()); + }); + + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded,tags: new[] { "tag", }, instance: instance); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.Same(instance, registration.Factory(serviceProvider)); + } + + [Fact] + public void AddCheck_T_TypeActivated() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddCheck_T_Service() + { + // Arrange + var instance = new TestHealthCheck(); + + var services = CreateServices(); + services.AddSingleton(instance); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.Same(instance, registration.Factory(serviceProvider)); + } + + [Fact] + public void AddTypeActivatedCheck() + { + // Arrange + var services = CreateServices(); + services + .AddHealthChecks() + .AddTypeActivatedCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, args: new object[] { 5, "hi", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + + var check = Assert.IsType(registration.Factory(serviceProvider)); + Assert.Equal(5, check.I); + Assert.Equal("hi", check.S); + } + + [Fact] + public void AddDelegateCheck_NoArg() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, check: () => + { + return HealthCheckResult.Passed(); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddDelegateCheck_CancellationToken() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", (_) => + { + return HealthCheckResult.Passed(); + }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddAsyncDelegateCheck_NoArg() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddAsyncCheck("test", () => + { + return Task.FromResult(HealthCheckResult.Passed()); + }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddAsyncDelegateCheck_CancellationToken() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddAsyncCheck("test", (_) => + { + return Task.FromResult(HealthCheckResult.Passed()); + }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks() + { + var services = new ServiceCollection(); + services + .AddHealthChecks() + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed())); + services + .AddHealthChecks() + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Passed())); + + // Act + var options = services.BuildServiceProvider().GetRequiredService>(); + + // Assert + Assert.Collection( + options.Value.Registrations, + actual => Assert.Equal("Foo", actual.Name), + actual => Assert.Equal("Bar", actual.Name)); + } + + private IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + return services; + } + + private class TestHealthCheck : IHealthCheck + { + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + } + + private class TestHealthCheckWithArgs : IHealthCheck + { + public TestHealthCheckWithArgs(int i, string s) + { + I = i; + S = s; + } + + public int I { get; set; } + + public string S { get; set; } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/ServiceCollectionExtensionsTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs similarity index 76% rename from test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/ServiceCollectionExtensionsTests.cs rename to test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs index b33bd36eb6..34b92b7b5e 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/ServiceCollectionExtensionsTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs @@ -1,12 +1,12 @@ // 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 Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Xunit; -namespace Microsoft.Extensions.Diagnostics.HealthChecks +namespace Microsoft.Extensions.DependencyInjection { - public class ServiceCollectionExtensionsTests + public class ServiceCollectionExtensionsTest { [Fact] public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently() @@ -23,8 +23,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks actual => { Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); - Assert.Equal(typeof(IHealthCheckService), actual.ServiceType); - Assert.Equal(typeof(HealthCheckService), actual.ImplementationType); + Assert.Equal(typeof(HealthCheckService), actual.ServiceType); + Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType); Assert.Null(actual.ImplementationInstance); Assert.Null(actual.ImplementationFactory); }); diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs deleted file mode 100644 index f1bcbbf00b..0000000000 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - public class HealthChecksBuilderTests - { - [Fact] - public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks() - { - var services = new ServiceCollection(); - services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy())); - services.AddHealthChecks() - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy())); - - // Act - var checks = services.BuildServiceProvider().GetRequiredService>(); - - // Assert - Assert.Collection( - checks, - actual => Assert.Equal("Foo", actual.Name), - actual => Assert.Equal("Bar", actual.Name)); - } - } -} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs new file mode 100644 index 0000000000..9ce0a4b620 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs @@ -0,0 +1,31 @@ +// 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 Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public class HealthReportTest + { + [Theory] + [InlineData(HealthStatus.Healthy)] + [InlineData(HealthStatus.Degraded)] + [InlineData(HealthStatus.Unhealthy)] + [InlineData(HealthStatus.Failed)] + public void Status_MatchesWorstStatusInResults(HealthStatus status) + { + var result = new HealthReport(new Dictionary() + { + {"Foo", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Bar", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Baz", new HealthReportEntry(status, exception: null, description: null, data: null) }, + {"Quick", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Quack", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Quock", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + }); + + Assert.Equal(status, result.Status); + } + } +}