From 47f427d5ac6c3db6e09fd301a8646387319fcadc Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 31 Jul 2018 16:34:55 -0700 Subject: [PATCH 1/2] Updating Health Checks for 2.2 A bunch of small changes and updates for 2.2 focused at making our main scenarios more streamlined and focused. Also adds samples for extensibility we support so far. A list of changes: Clearing baselines for these projects. We didn't ship anything in 2.1 so there should be nothing in the baselines. -- The middleware now uses Map for path matching. This makes the actual `HealthCheckMiddleware` more standalone. This will make it easy to use with Dispatcher/Endpoint Routing in the future. This also manifests by removing Path from HealthCheckOptions - the path is an explicit argument to the UseHealthChecks middelware - this streamlines the design for 3.0. -- Added extensibility for customizing the status codes (aspnet/Home#2584) -- Added extensibility for writing the textual output (aspnet/Home#2583) -- Changed the default output to be `text/plain`. The most common use cases for health checks don't include a detailed status. The existing output format is still available as an option. --- build/dependencies.props | 1 + samples/HealthChecksSample/BasicStartup.cs | 35 ++ .../HealthChecksSample/CustomWriterStartup.cs | 67 ++++ .../DetailedStatusStartup.cs | 68 ++++ .../HealthChecksSample/GCInfoHealthCheck.cs | 41 ++ .../HealthChecksSample.csproj | 7 +- samples/HealthChecksSample/Program.cs | 37 +- samples/HealthChecksSample/Startup.cs | 28 -- ...HealthCheckApplicationBuilderExtensions.cs | 67 ++++ .../HealthCheckAppBuilderExtensions.cs | 32 -- .../HealthCheckMiddleware.cs | 84 ++-- .../HealthCheckOptions.cs | 22 +- .../HealthCheckResponseWriters.cs | 56 +++ .../baseline.netcore.json | 110 ------ .../baseline.netcore.json | 372 ------------------ .../HealthCheck.cs | 16 +- .../HealthChecksBuilderAddCheckExtensions.cs | 61 ++- .../baseline.netcore.json | 329 ---------------- .../HealthCheckMiddlewareSampleTest.cs | 62 +++ .../HealthCheckMiddlewareTests.cs | 233 ++++++++--- ...Core.Diagnostics.HealthChecks.Tests.csproj | 1 + 21 files changed, 733 insertions(+), 996 deletions(-) create mode 100644 samples/HealthChecksSample/BasicStartup.cs create mode 100644 samples/HealthChecksSample/CustomWriterStartup.cs create mode 100644 samples/HealthChecksSample/DetailedStatusStartup.cs create mode 100644 samples/HealthChecksSample/GCInfoHealthCheck.cs delete mode 100644 samples/HealthChecksSample/Startup.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckAppBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs create mode 100644 test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs diff --git a/build/dependencies.props b/build/dependencies.props index 7ef83bd4d79..c201e62915e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -16,6 +16,7 @@ 2.2.0-preview1-34823 2.2.0-preview1-34823 2.2.0-preview1-34823 + 2.2.0-preview1-34823 2.2.0-preview1-34823 2.2.0-preview1-34823 2.2.0-preview1-34823 diff --git a/samples/HealthChecksSample/BasicStartup.cs b/samples/HealthChecksSample/BasicStartup.cs new file mode 100644 index 00000000000..c89d78c8e5d --- /dev/null +++ b/samples/HealthChecksSample/BasicStartup.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace HealthChecksSample +{ + // Pass in `--scenario basic` at the command line to run this sample. + public class BasicStartup + { + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services.AddHealthChecks(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health. + // + // By default health checks will return a 200 with 'Healthy'. + // - No health checks are registered by default, the app is healthy if it is reachable + // - The default response writer writes the HealthCheckStatus as text/plain content + // + // This is the simplest way to use health checks, it is suitable for systems + // that want to check for 'liveness' of an application. + app.UseHealthChecks("/health"); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health to see the health status"); + }); + } + } +} diff --git a/samples/HealthChecksSample/CustomWriterStartup.cs b/samples/HealthChecksSample/CustomWriterStartup.cs new file mode 100644 index 00000000000..87e58b7b7d5 --- /dev/null +++ b/samples/HealthChecksSample/CustomWriterStartup.cs @@ -0,0 +1,67 @@ +using System.Linq; +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 +{ + // Pass in `--scenario writer` at the command line to run this sample. + public class CustomWriterStartup + { + 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(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health + // + // This example overrides the HealthCheckResponseWriter to write the health + // check result in a totally custom way. + app.UseHealthChecks("/health", new HealthCheckOptions() + { + // This custom writer formats the detailed status as an HTML table. + ResponseWriter = WriteResponse, + }); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health to see the health status"); + }); + } + + private static Task WriteResponse(HttpContext httpContext, CompositeHealthCheckResult result) + { + httpContext.Response.ContentType = "text/html"; + return httpContext.Response.WriteAsync($@" + + +

+ Everything is {result.Status} +

+ + + + + + {string.Join("", result.Results.Select(kvp => $""))} + +
NameStatus
{kvp.Key}{kvp.Value.Status}
+ +"); + } + } +} diff --git a/samples/HealthChecksSample/DetailedStatusStartup.cs b/samples/HealthChecksSample/DetailedStatusStartup.cs new file mode 100644 index 00000000000..76475c5db50 --- /dev/null +++ b/samples/HealthChecksSample/DetailedStatusStartup.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +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 +{ + // Pass in `--scenario detailed` at the command line to run this sample. + public class DetailedStatusStartup + { + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services + .AddHealthChecks() + + // Registers a custom health check, in this case it will execute an + // inline delegate. + .AddCheck("GC Info", () => + { + // This example will report degraded status if the application is using + // more than 1gb of memory. + // + // Additionally we include some GC info in the reported diagnostics. + var allocated = GC.GetTotalMemory(forceFullCollection: false); + var data = new Dictionary() + { + { "Allocated", allocated }, + { "Gen0Collections", GC.CollectionCount(0) }, + { "Gen1Collections", GC.CollectionCount(1) }, + { "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; + + return Task.FromResult(new HealthCheckResult( + status, + exception: null, + description: "reports degraded status if allocated bytes >= 1gb", + data: data)); + }); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health + // + // This example overrides the ResponseWriter to include a detailed + // status as JSON. Use this response writer (or create your own) to include + // detailed diagnostic information for use by a monitoring system. + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health to see the health status"); + }); + } + } +} diff --git a/samples/HealthChecksSample/GCInfoHealthCheck.cs b/samples/HealthChecksSample/GCInfoHealthCheck.cs new file mode 100644 index 00000000000..644fdb0a325 --- /dev/null +++ b/samples/HealthChecksSample/GCInfoHealthCheck.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecksSample +{ + // This is an example of a custom health check that implements IHealthCheck. + // This is the same core logic as the DetailedStatusStartup example. + // See CustomWriterStartup to see how this is registered. + public class GCInfoHealthCheck : IHealthCheck + { + public string Name { get; } = "GCInfo"; + + public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + // This example will report degraded status if the application is using + // more than 1gb of memory. + // + // Additionally we include some GC info in the reported diagnostics. + var allocated = GC.GetTotalMemory(forceFullCollection: false); + var data = new Dictionary() + { + { "Allocated", allocated }, + { "Gen0Collections", GC.CollectionCount(0) }, + { "Gen1Collections", GC.CollectionCount(1) }, + { "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; + + return Task.FromResult(new HealthCheckResult( + status, + exception: null, + description: "reports degraded status if allocated bytes >= 1gb", + data: data)); + } + } +} diff --git a/samples/HealthChecksSample/HealthChecksSample.csproj b/samples/HealthChecksSample/HealthChecksSample.csproj index 19ef79337e1..2f3c0e6aaa0 100644 --- a/samples/HealthChecksSample/HealthChecksSample.csproj +++ b/samples/HealthChecksSample/HealthChecksSample.csproj @@ -1,10 +1,13 @@ - + - netcoreapp2.0 + + netcoreapp2.0;net461 + netcoreapp2.0 + diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index be3f8bcdf10..bd72511db6b 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -1,23 +1,54 @@ +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace HealthChecksSample { public class Program { + private static readonly Dictionary _scenarios; + + static Program() + { + _scenarios = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "", typeof(BasicStartup) }, + { "basic", typeof(BasicStartup) }, + { "detailed", typeof(DetailedStatusStartup) }, + { "writer", typeof(CustomWriterStartup) }, + }; + } + public static void Main(string[] args) { BuildWebHost(args).Run(); } - public static IWebHost BuildWebHost(string[] args) => - new WebHostBuilder() + public static IWebHost BuildWebHost(string[] args) + { + var config = new ConfigurationBuilder() + .AddEnvironmentVariables(prefix: "ASPNETCORE_") + .AddCommandLine(args) + .Build(); + + var scenario = config["scenario"] ?? string.Empty; + if (!_scenarios.TryGetValue(scenario, out var startupType)) + { + startupType = typeof(BasicStartup); + } + + return new WebHostBuilder() + .UseConfiguration(config) .ConfigureLogging(builder => { builder.AddConsole(); }) .UseKestrel() - .UseStartup() + .UseStartup(startupType) .Build(); + } + } } diff --git a/samples/HealthChecksSample/Startup.cs b/samples/HealthChecksSample/Startup.cs deleted file mode 100644 index e3bb04150c5..00000000000 --- a/samples/HealthChecksSample/Startup.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; - -namespace HealthChecksSample -{ - public class Startup - { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - services.AddHealthChecks(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - app.UseHealthChecks("/health"); - - app.Run(async (context) => - { - await context.Response.WriteAsync("Hello World!"); - }); - } - } -} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs new file mode 100644 index 00000000000..f1f47b3ba54 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs @@ -0,0 +1,67 @@ +// 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.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// extension methods for the . + /// + public static class HealthCheckApplicationBuilderExtensions + { + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// A reference to the after the operation has completed. + /// + /// The health check middleware will use default settings other than the provided . + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + return app.Map(path, b => b.UseMiddleware()); + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// A used to configure the middleware. + /// A reference to the after the operation has completed. + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, HealthCheckOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.Map(path, b => b.UseMiddleware(Options.Create(options))); + } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckAppBuilderExtensions.cs deleted file mode 100644 index 916ca46df41..00000000000 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckAppBuilderExtensions.cs +++ /dev/null @@ -1,32 +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 Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Builder -{ - /// - /// extension methods for the . - /// - public static class HealthCheckAppBuilderExtensions - { - /// - /// Adds a middleware that provides a REST API for requesting health check status. - /// - /// The . - /// The path on which to provide the API. - /// A reference to the after the operation has completed. - public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path) - { - app = app ?? throw new ArgumentNullException(nameof(app)); - - return app.UseMiddleware(Options.Create(new HealthCheckOptions() - { - Path = path - })); - } - } -} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 18c10da2871..5f3ca6df471 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -2,14 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { @@ -19,60 +15,62 @@ public class HealthCheckMiddleware private readonly HealthCheckOptions _healthCheckOptions; private readonly IHealthCheckService _healthCheckService; - public HealthCheckMiddleware(RequestDelegate next, IOptions healthCheckOptions, IHealthCheckService healthCheckService) + public HealthCheckMiddleware( + RequestDelegate next, + IOptions healthCheckOptions, + IHealthCheckService healthCheckService) { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (healthCheckOptions == null) + { + throw new ArgumentNullException(nameof(healthCheckOptions)); + } + + if (healthCheckService == null) + { + throw new ArgumentNullException(nameof(healthCheckService)); + } + _next = next; _healthCheckOptions = healthCheckOptions.Value; _healthCheckService = healthCheckService; } /// - /// Process an individual request. + /// Processes a request. /// - /// + /// /// - public async Task InvokeAsync(HttpContext context) + public async Task InvokeAsync(HttpContext httpContext) { - if (context.Request.Path == _healthCheckOptions.Path) + if (httpContext == null) { - // Get results - var result = await _healthCheckService.CheckHealthAsync(context.RequestAborted); + throw new ArgumentNullException(nameof(httpContext)); + } - // Map status to response code - switch (result.Status) - { - case HealthCheckStatus.Failed: - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - break; - case HealthCheckStatus.Unhealthy: - context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - break; - case HealthCheckStatus.Degraded: - // Degraded doesn't mean unhealthy so we return 200, but the content will contain more details - context.Response.StatusCode = StatusCodes.Status200OK; - break; - case HealthCheckStatus.Healthy: - context.Response.StatusCode = StatusCodes.Status200OK; - break; - default: - // This will only happen when we change HealthCheckStatus and we don't update this. - Debug.Fail($"Unrecognized HealthCheckStatus value: {result.Status}"); - throw new InvalidOperationException($"Unrecognized HealthCheckStatus value: {result.Status}"); - } + // Get results + var result = await _healthCheckService.CheckHealthAsync(httpContext.RequestAborted); - // Render results to JSON - var json = new JObject( - new JProperty("status", result.Status.ToString()), - new JProperty("results", new JObject(result.Results.Select(pair => - new JProperty(pair.Key, new JObject( - new JProperty("status", pair.Value.Status.ToString()), - new JProperty("description", pair.Value.Description), - new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value)))))))))); - await context.Response.WriteAsync(json.ToString(Formatting.None)); + // Map status to response code - this is customizable via options. + if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) + { + var message = + $"No status code mapping found for {nameof(HealthCheckStatus)} value: {result.Status}." + + $"{nameof(HealthCheckOptions)}.{nameof(HealthCheckOptions.ResultStatusCodes)} must contain" + + $"an entry for {result.Status}."; + + throw new InvalidOperationException(message); } - else + + httpContext.Response.StatusCode = statusCode; + + if (_healthCheckOptions.ResponseWriter != null) { - await _next(context); + await _healthCheckOptions.ResponseWriter(httpContext, result); } } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index 8416e6843dd..6426fcbc3b6 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -1,7 +1,11 @@ // 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.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { @@ -10,9 +14,23 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// public class HealthCheckOptions { + public IDictionary ResultStatusCodes { get; } = new Dictionary() + { + { HealthCheckStatus.Healthy, StatusCodes.Status200OK }, + { HealthCheckStatus.Degraded, StatusCodes.Status200OK }, + { HealthCheckStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, + + // This means that a health check failed, so 500 is appropriate. This is an error. + { HealthCheckStatus.Failed, StatusCodes.Status500InternalServerError }, + }; + /// - /// Gets or sets the path at which the Health Check results will be available. + /// Gets or sets a delegate used to write the response. /// - public PathString Path { get; set; } + /// + /// The default value is a delegate that will write a minimal text/plain response with the value + /// of as a string. + /// + 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 new file mode 100644 index 00000000000..95ddf00ac8e --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs @@ -0,0 +1,56 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Diagnostics.HealthChecks +{ + public static class HealthCheckResponseWriters + { + public static Task WriteMinimalPlaintext(HttpContext httpContext, CompositeHealthCheckResult result) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + httpContext.Response.ContentType = "text/plain"; + return httpContext.Response.WriteAsync(result.Status.ToString()); + } + + public static Task WriteDetailedJson(HttpContext httpContext, CompositeHealthCheckResult result) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(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(pair.Key, new JObject( + new JProperty("status", pair.Value.Status.ToString()), + new JProperty("description", pair.Value.Description), + new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value)))))))))); + return httpContext.Response.WriteAsync(json.ToString(Formatting.Indented)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json index c0e8deddd1f..d089d2470dc 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json @@ -1,115 +1,5 @@ { "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ - { - "Name": "Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckMiddleware", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "InvokeAsync", - "Parameters": [ - { - "Name": "context", - "Type": "Microsoft.AspNetCore.Http.HttpContext" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "next", - "Type": "Microsoft.AspNetCore.Http.RequestDelegate" - }, - { - "Name": "healthCheckOptions", - "Type": "Microsoft.Extensions.Options.IOptions" - }, - { - "Name": "healthCheckService", - "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Path", - "Parameters": [], - "ReturnType": "Microsoft.AspNetCore.Http.PathString", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "set_Path", - "Parameters": [ - { - "Name": "value", - "Type": "Microsoft.AspNetCore.Http.PathString" - } - ], - "ReturnType": "System.Void", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.AspNetCore.Builder.HealthCheckAppBuilderExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "UseHealthChecks", - "Parameters": [ - { - "Name": "app", - "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" - }, - { - "Name": "path", - "Type": "Microsoft.AspNetCore.Http.PathString" - } - ], - "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } ] } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json index 7792748d55e..871db4c089e 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json @@ -1,377 +1,5 @@ { "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Visibility": "Public", - "Kind": "Struct", - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Status", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Exception", - "Parameters": [], - "ReturnType": "System.Exception", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Description", - "Parameters": [], - "ReturnType": "System.String", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Data", - "Parameters": [], - "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "exception", - "Type": "System.Exception" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "exception", - "Type": "System.Exception" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "exception", - "Type": "System.Exception" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Healthy", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Healthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Healthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "exception", - "Type": "System.Exception" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "exception", - "Type": "System.Exception" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "exception", - "Type": "System.Exception" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "status", - "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus" - }, - { - "Name": "exception", - "Type": "System.Exception" - }, - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus", - "Visibility": "Public", - "Kind": "Enumeration", - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Field", - "Name": "Unknown", - "Parameters": [], - "GenericParameter": [], - "Literal": "0" - }, - { - "Kind": "Field", - "Name": "Failed", - "Parameters": [], - "GenericParameter": [], - "Literal": "1" - }, - { - "Kind": "Field", - "Name": "Unhealthy", - "Parameters": [], - "GenericParameter": [], - "Literal": "2" - }, - { - "Kind": "Field", - "Name": "Degraded", - "Parameters": [], - "GenericParameter": [], - "Literal": "3" - }, - { - "Kind": "Field", - "Name": "Healthy", - "Parameters": [], - "GenericParameter": [], - "Literal": "4" - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Name", - "Parameters": [], - "ReturnType": "System.String", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } ] } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs index e72910df335..da6a92d0623 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs @@ -11,15 +11,10 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// A simple implementation of which uses a provided delegate to /// implement the check. /// - public class HealthCheck : IHealthCheck + public sealed class HealthCheck : IHealthCheck { private readonly Func> _check; - /// - /// Gets the name of the health check, which should indicate the component being checked. - /// - public string Name { get; } - /// /// Create an instance of from the specified and . /// @@ -27,10 +22,15 @@ public class HealthCheck : IHealthCheck /// A delegate which provides the code to execute when the health check is run. public HealthCheck(string name, Func> check) { - Name = name; - _check = 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. /// diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs index 3b3ee1eb54a..b2f931fcd39 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs @@ -22,8 +22,22 @@ public static class HealthChecksBuilderAddCheckExtensions /// The . public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) { - builder.Services.AddSingleton(services => new HealthCheck(name, check)); - return builder; + 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)); } /// @@ -33,7 +47,46 @@ public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, s /// 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) => - builder.AddCheck(name, _ => check()); + 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 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; + } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json b/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json index 4938a12605c..cb2fe053f13 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json @@ -1,334 +1,5 @@ { "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ - { - "Name": "Microsoft.Extensions.DependencyInjection.HealthChecksBuilderAddCheckExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "AddCheck", - "Parameters": [ - { - "Name": "builder", - "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder" - }, - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "check", - "Type": "System.Func>" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "AddCheck", - "Parameters": [ - { - "Name": "builder", - "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder" - }, - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "check", - "Type": "System.Func>" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.DependencyInjection.HealthCheckServiceCollectionExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "AddHealthChecks", - "Parameters": [ - { - "Name": "services", - "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.CompositeHealthCheckResult", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Results", - "Parameters": [], - "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Status", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "results", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheck", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck" - ], - "Members": [ - { - "Kind": "Method", - "Name": "get_Name", - "Parameters": [], - "ReturnType": "System.String", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "check", - "Type": "System.Func>" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService" - ], - "Members": [ - { - "Kind": "Method", - "Name": "get_Checks", - "Parameters": [], - "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "checks", - "Type": "System.Collections.Generic.IEnumerable" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "healthChecks", - "Type": "System.Collections.Generic.IEnumerable" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "healthChecks", - "Type": "System.Collections.Generic.IEnumerable" - }, - { - "Name": "logger", - "Type": "Microsoft.Extensions.Logging.ILogger" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Services", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Checks", - "Parameters": [], - "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "checks", - "Type": "System.Collections.Generic.IEnumerable" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } ] } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs new file mode 100644 index 00000000000..1007017e11d --- /dev/null +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs @@ -0,0 +1,62 @@ +// 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.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace Microsoft.AspNetCore.Diagnostics.HealthChecks +{ + public class HealthCheckMiddlewareSampleTest + { + [Fact] + public async Task BasicStartup() + { + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CustomWriterStartup() + { + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/html", response.Content.Headers.ContentType.ToString()); + + // Ignoring the body since it contains a bunch of statistics + } + + [Fact] + public async Task DetailedStatusStartup() + { + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + + // Ignoring the body since it contains a bunch of statistics + } + } +} diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index c6aaee52ff2..b81747581c7 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -17,10 +17,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { public class HealthCheckMiddlewareTests { - [Theory] - [InlineData("/frob")] - [InlineData("/health/")] // Match is exact, for now at least - public async Task IgnoresRequestThatDoesNotMatchPath(string requestPath) + [Fact] // Matches based on '.Map' + public async Task IgnoresRequestThatDoesNotMatchPath() { var builder = new WebHostBuilder() .Configure(app => @@ -34,22 +32,13 @@ public async Task IgnoresRequestThatDoesNotMatchPath(string requestPath) var server = new TestServer(builder); var client = server.CreateClient(); - var response = await client.GetAsync(requestPath); + var response = await client.GetAsync("/frob"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Theory] - [InlineData("/health")] - [InlineData("/Health")] - [InlineData("/HEALTH")] - public async Task ReturnsEmptyHealthyRequestIfNoHealthChecksRegistered(string requestPath) + [Fact] // Matches based on '.Map' + public async Task MatchIsCaseInsensitive() { - var expectedJson = JsonConvert.SerializeObject(new - { - status = "Healthy", - results = new { } - }, Formatting.None); - var builder = new WebHostBuilder() .Configure(app => { @@ -62,47 +51,13 @@ public async Task ReturnsEmptyHealthyRequestIfNoHealthChecksRegistered(string re var server = new TestServer(builder); var client = server.CreateClient(); - var response = await client.GetAsync(requestPath); - - var result = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedJson, result); + var response = await client.GetAsync("/HEALTH"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] - public async Task ReturnsResultsFromHealthChecks() + public async Task ReturnsPlainTextStatus() { - var expectedJson = JsonConvert.SerializeObject(new - { - status = "Unhealthy", - results = new - { - Foo = new - { - status = "Healthy", - description = "Good to go!", - data = new { } - }, - Bar = new - { - status = "Degraded", - description = "Feeling a bit off.", - data = new { someUsefulAttribute = 42 } - }, - Baz = new - { - status = "Unhealthy", - description = "Not feeling good at all", - data = new { } - }, - Boz = new - { - status = "Unhealthy", - description = string.Empty, - data = new { } - }, - }, - }, Formatting.None); - var builder = new WebHostBuilder() .Configure(app => { @@ -110,22 +65,16 @@ public async Task ReturnsResultsFromHealthChecks() }) .ConfigureServices(services => { - services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("Good to go!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Feeling a bit off.", new Dictionary() - { - { "someUsefulAttribute", 42 } - }))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Unhealthy("Not feeling good at all", new Exception("Bad times")))) - .AddCheck("Boz", () => Task.FromResult(HealthCheckResult.Unhealthy(new Exception("Very bad times")))); + services.AddHealthChecks(); }); var server = new TestServer(builder); var client = server.CreateClient(); var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedJson, result); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } [Fact] @@ -146,8 +95,11 @@ public async Task StatusCodeIs200IfNoChecks() var response = await client.GetAsync("/health"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } + [Fact] public async Task StatusCodeIs200IfAllChecksHealthy() { @@ -169,6 +121,8 @@ public async Task StatusCodeIs200IfAllChecksHealthy() var response = await client.GetAsync("/health"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } [Fact] @@ -192,6 +146,8 @@ public async Task StatusCodeIs200IfCheckIsDegraded() var response = await client.GetAsync("/health"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Degraded", await response.Content.ReadAsStringAsync()); } [Fact] @@ -215,6 +171,8 @@ public async Task StatusCodeIs503IfCheckIsUnhealthy() var response = await client.GetAsync("/health"); Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync()); } [Fact] @@ -238,6 +196,155 @@ public async Task StatusCodeIs500IfCheckIsFailed() var response = await client.GetAsync("/health"); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Failed", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task DetailedJsonReturnsEmptyHealthyResponseIfNoHealthChecksRegistered() + { + var expectedJson = JsonConvert.SerializeObject(new + { + status = "Healthy", + results = new { } + }, Formatting.Indented); + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedJson, result); + } + + [Fact] + public async Task DetailedJsonReturnsResultsFromHealthChecks() + { + var expectedJson = JsonConvert.SerializeObject(new + { + status = "Unhealthy", + results = new + { + Foo = new + { + status = "Healthy", + description = "Good to go!", + data = new { } + }, + Bar = new + { + status = "Degraded", + description = "Feeling a bit off.", + data = new { someUsefulAttribute = 42 } + }, + Baz = new + { + status = "Unhealthy", + description = "Not feeling good at all", + data = new { } + }, + Boz = new + { + status = "Unhealthy", + description = string.Empty, + data = new { } + }, + }, + }, Formatting.Indented); + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); + }) + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("Good to go!"))) + .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Feeling a bit off.", new Dictionary() + { + { "someUsefulAttribute", 42 } + }))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Unhealthy("Not feeling good at all", new Exception("Bad times")))) + .AddCheck("Boz", () => Task.FromResult(HealthCheckResult.Unhealthy(new Exception("Very bad times")))); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedJson, result); + } + + [Fact] + public async Task NoResponseWriterReturnsEmptyBody() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = null, + }); + }) + .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!"))); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CanSetCustomStatusCodes() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResultStatusCodes = + { + [HealthCheckStatus.Healthy] = 201, + } + }); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj index 001b0c0990c..8ac9320ae84 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj @@ -14,6 +14,7 @@ + From 64124e9c8591dccef583a0e004414a8dfe6df5d6 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 31 Jul 2018 18:00:51 -0700 Subject: [PATCH 2/2] Add filtering to Health Checks middleware This allows each middleware to be configured with a specific set of checks (by name). See the comments in the sample for how this is frequently used. This is also addresses aspnet/Home#2575 - or at least the part that we plan to do. We think that any sort of built-in system of metadata or tags is vast overkill, and doesn't really align with the primary usage of health checks. We're providing building blocks, and these can be put together to build more complicated things like what's described in aspnet/Home#2575. --- .../LivenessProbeStartup.cs | 80 +++++++++++++++++++ samples/HealthChecksSample/Program.cs | 1 + .../SlowDependencyHealthCheck.cs | 32 ++++++++ .../HealthCheckMiddleware.cs | 43 +++++++++- .../HealthCheckOptions.cs | 10 +++ .../HealthCheckMiddlewareSampleTest.cs | 65 +++++++++++++++ .../HealthCheckMiddlewareTests.cs | 62 ++++++++++++++ 7 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 samples/HealthChecksSample/LivenessProbeStartup.cs create mode 100644 samples/HealthChecksSample/SlowDependencyHealthCheck.cs diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs new file mode 100644 index 00000000000..d7645b5f0e8 --- /dev/null +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -0,0 +1,80 @@ +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 +{ + // Pass in `--scenario liveness` at the command line to run this sample. + public class LivenessProbeStartup + { + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services + .AddHealthChecks() + .AddCheck("identity", () => Task.FromResult(HealthCheckResult.Healthy())) + .AddCheck(new SlowDependencyHealthCheck()); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware twice: + // - at /health/ready for 'readiness' + // - at /health/live for 'liveness' + // + // Using a separate liveness and readiness check is useful in an environment like Kubernetes + // when an application needs to do significant work before accepting requests. Using separate + // checks allows the orchestrator to distinguish whether the application is functioning but + // not yet ready or if the application has failed to start. + // + // For instance the liveness check will do a quick set of checks to determine if the process + // is functioning correctly. + // + // The readiness check might do a set of more expensive or time-consuming checks to determine + // if all other resources are responding. + // + // See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/ for + // more details about readiness and liveness probes in Kubernetes. + // + // In this example, the liveness check will us an 'identity' check that always returns healthy. + // + // In this example, the readiness check will run all registered checks, include a check with an + // long initialization time (15 seconds). + + + // The readiness check uses all of the registered health checks (default) + app.UseHealthChecks("/health/ready", new HealthCheckOptions() + { + // This sample is using detailed status to make more apparent which checks are being run - any + // output format will work with liveness and readiness checks. + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); + + // The liveness check uses an 'identity' health check that always returns healty + app.UseHealthChecks("/health/live", new HealthCheckOptions() + { + // Filters the set of health checks run by this middleware + HealthCheckNames = + { + "identity", + }, + + // This sample is using detailed status to make more apparent which checks are being run - any + // output format will work with liveness and readiness checks. + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health/ready to see the readiness status"); + await context.Response.WriteAsync(Environment.NewLine); + await context.Response.WriteAsync("Go to /health/live to see the liveness status"); + }); + } + } +} diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index bd72511db6b..4d9455ed2d9 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -18,6 +18,7 @@ static Program() { "basic", typeof(BasicStartup) }, { "detailed", typeof(DetailedStatusStartup) }, { "writer", typeof(CustomWriterStartup) }, + { "liveness", typeof(LivenessProbeStartup) }, }; } diff --git a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs new file mode 100644 index 00000000000..1ca03f88ae7 --- /dev/null +++ b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecksSample +{ + // Simulates a health check for an application dependency that takes a while to initialize. + // This is part of the readiness/liveness probe sample. + public class SlowDependencyHealthCheck : IHealthCheck + { + public static readonly string HealthCheckName = "slow_dependency"; + + private readonly Task _task; + + public SlowDependencyHealthCheck() + { + _task = Task.Delay(15 * 1000); + } + + public string Name => HealthCheckName; + + public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + if (_task.IsCompleted) + { + return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready")); + } + + return Task.FromResult(HealthCheckResult.Unhealthy("Dependency is still initializing")); + } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 5f3ca6df471..3a220f25950 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -2,6 +2,8 @@ // 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.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -14,6 +16,7 @@ public class HealthCheckMiddleware private readonly RequestDelegate _next; private readonly HealthCheckOptions _healthCheckOptions; private readonly IHealthCheckService _healthCheckService; + private readonly IHealthCheck[] _checks; public HealthCheckMiddleware( RequestDelegate next, @@ -38,6 +41,8 @@ public HealthCheckMiddleware( _next = next; _healthCheckOptions = healthCheckOptions.Value; _healthCheckService = healthCheckService; + + _checks = FilterHealthChecks(_healthCheckService.Checks, healthCheckOptions.Value.HealthCheckNames); } /// @@ -53,7 +58,7 @@ public async Task InvokeAsync(HttpContext httpContext) } // Get results - var result = await _healthCheckService.CheckHealthAsync(httpContext.RequestAborted); + var result = await _healthCheckService.CheckHealthAsync(_checks, httpContext.RequestAborted); // Map status to response code - this is customizable via options. if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) @@ -73,5 +78,41 @@ public async Task InvokeAsync(HttpContext httpContext) await _healthCheckOptions.ResponseWriter(httpContext, result); } } + + private static IHealthCheck[] FilterHealthChecks( + IReadOnlyDictionary checks, + ISet names) + { + // If there are no filters then include all checks. + if (names.Count == 0) + { + return checks.Values.ToArray(); + } + + // Keep track of what we don't find so we can report errors. + var notFound = new HashSet(names, StringComparer.OrdinalIgnoreCase); + var matches = new List(); + + foreach (var kvp in checks) + { + if (!notFound.Remove(kvp.Key)) + { + // This check was excluded + continue; + } + + matches.Add(kvp.Value); + } + + if (notFound.Count > 0) + { + var message = + $"The following health checks were not found: '{string.Join(", ", notFound)}'. " + + $"Registered health checks: '{string.Join(", ", checks.Keys)}'."; + throw new InvalidOperationException(message); + } + + return matches.ToArray(); + } } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index 6426fcbc3b6..b57374f2aee 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -14,6 +14,16 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// public class HealthCheckOptions { + /// + /// Gets a set of health check names used to filter the set of health checks run. + /// + /// + /// If is empty, the will run all + /// registered health checks - this is the default behavior. To run a subset of health checks, + /// add the names of the desired health checks. + /// + public ISet HealthCheckNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); + public IDictionary ResultStatusCodes { get; } = new Dictionary() { { HealthCheckStatus.Healthy, StatusCodes.Status200OK }, diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs index 1007017e11d..4e716ed3b9b 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json; using Xunit; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks @@ -58,5 +59,69 @@ public async Task DetailedStatusStartup() // Ignoring the body since it contains a bunch of statistics } + + [Fact] + public async Task LivenessProbeStartup_Liveness() + { + var expectedJson = JsonConvert.SerializeObject(new + { + status = "Healthy", + results = new + { + identity = new + { + status = "Healthy", + description = "", + data = new { } + }, + }, + }, Formatting.Indented); + + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health/live"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + Assert.Equal(expectedJson, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task LivenessProbeStartup_Readiness() + { + var expectedJson = JsonConvert.SerializeObject(new + { + status = "Unhealthy", + results = new + { + identity = new + { + status = "Healthy", + description = "", + data = new { } + }, + slow_dependency = new + { + status = "Unhealthy", + description = "Dependency is still initializing", + data = new { } + }, + }, + }, Formatting.Indented); + + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health/ready"); + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + Assert.Equal(expectedJson, await response.Content.ReadAsStringAsync()); + } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index b81747581c7..74d23c81bfb 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -346,5 +346,67 @@ public async Task CanSetCustomStatusCodes() Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } + + [Fact] + public async Task CanFilterChecks() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions() + { + HealthCheckNames = + { + "Baz", + "FOO", + }, + }); + }) + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + // Will get filtered out + .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public void CanFilterChecks_ThrowsForMissingCheck() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions() + { + HealthCheckNames = + { + "Bazzzzzz", + "FOO", + }, + }); + }) + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + }); + + var ex = Assert.Throws(() => new TestServer(builder)); + Assert.Equal( + "The following health checks were not found: 'Bazzzzzz'. Registered health checks: 'Foo, Bar, Baz'.", + ex.Message); + } } }