From ebafbcdae33ddb2981f96c7790e1c6f422a59a8a Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 3 Aug 2018 14:35:54 -0700 Subject: [PATCH] Add filtering by port This adds UseHealthChecks overloads that configure the health checks middleware to listen on a preconfigured port. This is sugar for MapWhen, but it's important because a significant set of users will want to use Health Checks in this way. --- .../ManagementPortStartup.cs | 47 +++++ samples/HealthChecksSample/Program.cs | 1 + .../Properties/launchSettings.json | 15 ++ ...HealthCheckApplicationBuilderExtensions.cs | 174 +++++++++++++++++- .../HealthCheckMiddlewareTests.cs | 60 ++++++ 5 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 samples/HealthChecksSample/ManagementPortStartup.cs create mode 100644 samples/HealthChecksSample/Properties/launchSettings.json diff --git a/samples/HealthChecksSample/ManagementPortStartup.cs b/samples/HealthChecksSample/ManagementPortStartup.cs new file mode 100644 index 0000000000..a6ccce97ab --- /dev/null +++ b/samples/HealthChecksSample/ManagementPortStartup.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace HealthChecksSample +{ + // Pass in `--scenario port` at the command line to run this sample. + public class ManagementPortStartup + { + public ManagementPortStartup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + 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 but only on the specified port. + // + // 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 + // + // Use UseHealthChecks with a port will only process health checks requests on connection + // to the specified port. This is typically used in a container environment where you can expose + // a port for monitoring services to have access to the service. + // - In this case the management is configured in the launchSettings.json and passed through + // and environment variable + // - Additionally, the server is also configured to listen to requests on the management port. + app.UseHealthChecks("/health", port: Configuration["ManagementPort"]); + + app.Run(async (context) => + { + await context.Response.WriteAsync($"Go to http://localhost:{Configuration["ManagementPort"]}/health to see the health status"); + }); + } + } +} diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index b4e8874f22..a5c804e8f0 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -18,6 +18,7 @@ namespace HealthChecksSample { "basic", typeof(BasicStartup) }, { "writer", typeof(CustomWriterStartup) }, { "liveness", typeof(LivenessProbeStartup) }, + { "port", typeof(ManagementPortStartup) }, }; } diff --git a/samples/HealthChecksSample/Properties/launchSettings.json b/samples/HealthChecksSample/Properties/launchSettings.json new file mode 100644 index 0000000000..6afb19b2b3 --- /dev/null +++ b/samples/HealthChecksSample/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "HealthChecksSample": { + "commandName": "Project", + "commandLineArgs": "", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:5000/;http://localhost:5001/", + "ASPNETCORE_MANAGEMENTPORT": "5001" + }, + "applicationUrl": "http://localhost:5000/" + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs index e6239a3488..6c41fd5b79 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs @@ -40,7 +40,8 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentException("A URL path must be provided", nameof(path)); } - return app.Map(path, b => b.UseMiddleware()); + UseHealthChecksCore(app, path, port: null, Array.Empty()); + return app; } /// @@ -73,7 +74,176 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(options)); } - return app.Map(path, b => b.UseMiddleware(Options.Create(options))); + UseHealthChecksCore(app, path, port: null, new[] { Options.Create(options), }); + return app; + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// The port to listen on. Must be a local port on which the server is listening. + /// A reference to the after the operation has completed. + /// + /// + /// This method will use to + /// listen to health checks requests on the specified URL path and port. + /// + /// + /// The health check middleware will use default settings from . + /// + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, int port) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + UseHealthChecksCore(app, path, port, Array.Empty()); + return app; + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// The port to listen on. Must be a local port on which the server is listening. + /// A reference to the after the operation has completed. + /// + /// + /// This method will use to + /// listen to health checks requests on the specified URL path and port. + /// + /// + /// The health check middleware will use default settings from . + /// + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, string port) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + if (port == null) + { + throw new ArgumentNullException(nameof(port)); + } + + if (!int.TryParse(port, out var portAsInt)) + { + throw new ArgumentException("The port must be a valid integer.", nameof(port)); + } + + UseHealthChecksCore(app, path, portAsInt, Array.Empty()); + return app; + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// The port to listen on. Must be a local port on which the server is listening. + /// A used to configure the middleware. + /// A reference to the after the operation has completed. + /// + /// + /// This method will use to + /// listen to health checks requests on the specified URL path. + /// + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, int port, 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)); + } + + UseHealthChecksCore(app, path, port, new[] { Options.Create(options), }); + return app; + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// The port to listen on. Must be a local port on which the server is listening. + /// A used to configure the middleware. + /// A reference to the after the operation has completed. + /// + /// + /// This method will use to + /// listen to health checks requests on the specified URL path. + /// + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, string port, 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 (path == null) + { + throw new ArgumentNullException(nameof(port)); + } + + if (!int.TryParse(port, out var portAsInt)) + { + throw new ArgumentException("The port must be a valid integer.", nameof(port)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + UseHealthChecksCore(app, path, portAsInt, new[] { Options.Create(options), }); + return app; + } + + private static void UseHealthChecksCore(IApplicationBuilder app, PathString path, int? port, object[] args) + { + if (port == null) + { + app.Map(path, b => b.UseMiddleware(args)); + } + else + { + app.MapWhen( + c => c.Connection.LocalPort == port && c.Request.Path.StartsWithSegments(path), + b => b.UseMiddleware(args)); + } } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 0555ab6f7a..692e8b5bfd 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -355,5 +355,65 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks "The following health checks were not found: 'Bazzzzzz'. Registered health checks: 'Foo, Bar, Baz'.", ex.Message); } + + [Fact] + public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use(next => async (context) => + { + // Need to fake setting the connection info. TestServer doesn't + // do that, because it doesn't have a connection. + context.Connection.LocalPort = context.Request.Host.Port.Value; + await next(context); + }); + + app.UseHealthChecks("/health", port: 5001); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5001/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 CanListenOnPort_RejectsRequest_OnOtherPort() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use(next => async (context) => + { + // Need to fake setting the connection info. TestServer doesn't + // do that, because it doesn't have a connection. + context.Connection.LocalPort = context.Request.Host.Port.Value; + await next(context); + }); + + app.UseHealthChecks("/health", port: 5001); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5000/health"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } } }