diff --git a/build/dependencies.props b/build/dependencies.props index 7ef83bd4d7..c201e62915 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 0000000000..c89d78c8e5 --- /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 0000000000..87e58b7b7d --- /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 0000000000..76475c5db5 --- /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 0000000000..644fdb0a32 --- /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 19ef79337e..2f3c0e6aaa 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 be3f8bcdf1..bd72511db6 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 e3bb04150c..0000000000 --- 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 0000000000..f1f47b3ba5 --- /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 916ca46df4..0000000000 --- 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 18c10da287..5f3ca6df47 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 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks 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); - - // 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}"); - } - - // 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)); + throw new ArgumentNullException(nameof(httpContext)); } - else + + // Get results + var result = await _healthCheckService.CheckHealthAsync(httpContext.RequestAborted); + + // Map status to response code - this is customizable via options. + if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) { - await _next(context); + 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); + } + + httpContext.Response.StatusCode = statusCode; + + if (_healthCheckOptions.ResponseWriter != null) + { + await _healthCheckOptions.ResponseWriter(httpContext, result); } } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index 8416e6843d..6426fcbc3b 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 0000000000..95ddf00ac8 --- /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 c0e8deddd1..d089d2470d 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 7792748d55..871db4c089 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 e72910df33..da6a92d062 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 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// 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 3b3ee1eb54..b2f931fcd3 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs @@ -22,8 +22,22 @@ namespace Microsoft.Extensions.DependencyInjection /// 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 @@ namespace Microsoft.Extensions.DependencyInjection /// 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 4938a12605..cb2fe053f1 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 0000000000..1007017e11 --- /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 c6aaee52ff..b81747581c 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,26 +32,190 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks 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 builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/HEALTH"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task ReturnsPlainTextStatus() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + 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 StatusCodeIs200IfNoChecks() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + 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 StatusCodeIs200IfAllChecksHealthy() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .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!"))); + }); + 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 StatusCodeIs200IfCheckIsDegraded() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .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!"))); + }); + 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("Degraded", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task StatusCodeIs503IfCheckIsUnhealthy() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .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("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task StatusCodeIs500IfCheckIsFailed() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .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!"))); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + 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.None); + }, Formatting.Indented); var builder = new WebHostBuilder() .Configure(app => { - app.UseHealthChecks("/health"); + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); }) .ConfigureServices(services => { @@ -62,14 +224,14 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks var server = new TestServer(builder); var client = server.CreateClient(); - var response = await client.GetAsync(requestPath); + var response = await client.GetAsync("/health"); var result = await response.Content.ReadAsStringAsync(); Assert.Equal(expectedJson, result); } [Fact] - public async Task ReturnsResultsFromHealthChecks() + public async Task DetailedJsonReturnsResultsFromHealthChecks() { var expectedJson = JsonConvert.SerializeObject(new { @@ -101,12 +263,15 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks data = new { } }, }, - }, Formatting.None); + }, Formatting.Indented); var builder = new WebHostBuilder() .Configure(app => { - app.UseHealthChecks("/health"); + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); }) .ConfigureServices(services => { @@ -129,78 +294,15 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks } [Fact] - public async Task StatusCodeIs200IfNoChecks() + public async Task NoResponseWriterReturnsEmptyBody() { var builder = new WebHostBuilder() .Configure(app => { - app.UseHealthChecks("/health"); - }) - .ConfigureServices(services => - { - services.AddHealthChecks(); - }); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var response = await client.GetAsync("/health"); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task StatusCodeIs200IfAllChecksHealthy() - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseHealthChecks("/health"); - }) - .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!"))); - }); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var response = await client.GetAsync("/health"); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task StatusCodeIs200IfCheckIsDegraded() - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseHealthChecks("/health"); - }) - .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!"))); - }); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var response = await client.GetAsync("/health"); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task StatusCodeIs503IfCheckIsUnhealthy() - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseHealthChecks("/health"); + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = null, + }); }) .ConfigureServices(services => { @@ -215,29 +317,34 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks var response = await client.GetAsync("/health"); Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); } [Fact] - public async Task StatusCodeIs500IfCheckIsFailed() + public async Task CanSetCustomStatusCodes() { var builder = new WebHostBuilder() .Configure(app => { - app.UseHealthChecks("/health"); + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResultStatusCodes = + { + [HealthCheckStatus.Healthy] = 201, + } + }); }) .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!"))); + services.AddHealthChecks(); }); var server = new TestServer(builder); var client = server.CreateClient(); var response = await client.GetAsync("/health"); - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + 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 001b0c0990..8ac9320ae8 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 @@ +