diff --git a/samples/HealthChecksSample/CustomWriterStartup.cs b/samples/HealthChecksSample/CustomWriterStartup.cs index 0c3e542b72..6e37b1c6ff 100644 --- a/samples/HealthChecksSample/CustomWriterStartup.cs +++ b/samples/HealthChecksSample/CustomWriterStartup.cs @@ -17,14 +17,10 @@ namespace HealthChecksSample public void ConfigureServices(IServiceCollection services) { // Registers required services for health checks - services.AddHealthChecks(); - - // This is an example of registering a custom health check as a service. - // All IHealthCheck services will be available to the health check service and - // middleware. - // - // We recommend registering all health checks as Singleton services. - services.AddSingleton(); + services.AddHealthChecks() + + // Registers a custom health check implementation + .AddGCInfoCheck("GCInfo"); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) @@ -45,13 +41,13 @@ namespace HealthChecksSample }); } - private static Task WriteResponse(HttpContext httpContext, CompositeHealthCheckResult result) + private static Task WriteResponse(HttpContext httpContext, HealthReport result) { httpContext.Response.ContentType = "application/json"; var json = new JObject( new JProperty("status", result.Status.ToString()), - new JProperty("results", new JObject(result.Results.Select(pair => + new JProperty("results", new JObject(result.Entries.Select(pair => new JProperty(pair.Key, new JObject( new JProperty("status", pair.Value.Status.ToString()), new JProperty("description", pair.Value.Description), diff --git a/samples/HealthChecksSample/DBHealthStartup.cs b/samples/HealthChecksSample/DBHealthStartup.cs index 58b6c8d157..43dd6abda6 100644 --- a/samples/HealthChecksSample/DBHealthStartup.cs +++ b/samples/HealthChecksSample/DBHealthStartup.cs @@ -22,7 +22,7 @@ namespace HealthChecksSample // Registers required services for health checks services.AddHealthChecks() // Add a health check for a SQL database - .AddCheck(new SqlConnectionHealthCheck("MyDatabase", Configuration["ConnectionStrings:DefaultConnection"])); + .AddCheck("MyDatabase", new SqlConnectionHealthCheck(Configuration["ConnectionStrings:DefaultConnection"])); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) diff --git a/samples/HealthChecksSample/DbConnectionHealthCheck.cs b/samples/HealthChecksSample/DbConnectionHealthCheck.cs index 00d9bd4803..5a0ddcdd55 100644 --- a/samples/HealthChecksSample/DbConnectionHealthCheck.cs +++ b/samples/HealthChecksSample/DbConnectionHealthCheck.cs @@ -11,20 +11,17 @@ namespace HealthChecksSample { public abstract class DbConnectionHealthCheck : IHealthCheck { - protected DbConnectionHealthCheck(string name, string connectionString) - : this(name, connectionString, testQuery: null) + protected DbConnectionHealthCheck(string connectionString) + : this(connectionString, testQuery: null) { } - protected DbConnectionHealthCheck(string name, string connectionString, string testQuery) + protected DbConnectionHealthCheck(string connectionString, string testQuery) { - Name = name ?? throw new System.ArgumentNullException(nameof(name)); ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); TestQuery = testQuery; } - public string Name { get; } - protected string ConnectionString { get; } // This sample supports specifying a query to run as a boolean test of whether the database @@ -36,7 +33,7 @@ namespace HealthChecksSample protected abstract DbConnection CreateConnection(string connectionString); - public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { using (var connection = CreateConnection(ConnectionString)) { @@ -54,11 +51,11 @@ namespace HealthChecksSample } catch (DbException ex) { - return HealthCheckResult.Unhealthy(ex); + return HealthCheckResult.Failed(exception: ex); } } - return HealthCheckResult.Healthy(); + return HealthCheckResult.Passed(); } } } diff --git a/samples/HealthChecksSample/GCInfoHealthCheck.cs b/samples/HealthChecksSample/GCInfoHealthCheck.cs index f37ade8980..20bcc1eb11 100644 --- a/samples/HealthChecksSample/GCInfoHealthCheck.cs +++ b/samples/HealthChecksSample/GCInfoHealthCheck.cs @@ -2,21 +2,58 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; namespace HealthChecksSample { // This is an example of a custom health check that implements IHealthCheck. // - // See CustomWriterStartup to see how this is registered. + // This example also shows a technique for authoring a health check that needs to be registered + // with additional configuration data. This technique works via named options, and is useful + // for authoring health checks that can be disctributed as libraries. + + public static class GCInfoHealthCheckBuilderExtensions + { + public static IHealthChecksBuilder AddGCInfoCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus = null, + IEnumerable tags = null, + long? thresholdInBytes = null) + { + // Register a check of type GCInfo + builder.AddCheck(name, failureStatus ?? HealthStatus.Degraded, tags); + + // Configure named options to pass the threshold into the check. + if (thresholdInBytes.HasValue) + { + builder.Services.Configure(name, options => + { + options.Threshold = thresholdInBytes.Value; + }); + } + + return builder; + } + } + public class GCInfoHealthCheck : IHealthCheck { - public string Name { get; } = "GCInfo"; + private readonly IOptionsMonitor _options; - public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + public GCInfoHealthCheck(IOptionsMonitor options) { + _options = options; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) + { + var options = _options.Get(context.Registration.Name); + // This example will report degraded status if the application is using - // more than 1gb of memory. + // more than the configured amount of memory (1gb by default). // // Additionally we include some GC info in the reported diagnostics. var allocated = GC.GetTotalMemory(forceFullCollection: false); @@ -28,14 +65,20 @@ namespace HealthChecksSample { "Gen2Collections", GC.CollectionCount(2) }, }; - // Report degraded status if the allocated memory is >= 1gb (in bytes) - var status = allocated >= 1024 * 1024 * 1024 ? HealthCheckStatus.Degraded : HealthCheckStatus.Healthy; + // Report failure if the allocated memory is >= the threshold + var result = allocated >= options.Threshold; return Task.FromResult(new HealthCheckResult( - status, - exception: null, + result, description: "reports degraded status if allocated bytes >= 1gb", + exception: null, data: data)); } } + + public class GCInfoOptions + { + // The failure threshold (in bytes) + public long Threshold { get; set; } = 1024L * 1024L * 1024L; + } } diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs index aa644d231f..82676221ce 100644 --- a/samples/HealthChecksSample/LivenessProbeStartup.cs +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -1,11 +1,9 @@ using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthChecksSample { @@ -17,7 +15,7 @@ namespace HealthChecksSample // Registers required services for health checks services .AddHealthChecks() - .AddCheck(new SlowDependencyHealthCheck()); + .AddCheck("Slow", failureStatus: null, tags: new[] { "ready", }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) @@ -46,10 +44,13 @@ namespace HealthChecksSample // long initialization time (15 seconds). - // The readiness check uses all of the registered health checks (default) - app.UseHealthChecks("/health/ready"); + // The readiness check uses all registered checks with the 'ready' tag. + app.UseHealthChecks("/health/ready", new HealthCheckOptions() + { + Predicate = (check) => check.Tags.Contains("ready"), + }); - // The liveness check uses an 'identity' health check that always returns healthy + // The liveness filters out all checks and just returns success app.UseHealthChecks("/health/live", new HealthCheckOptions() { // Exclude all checks, just return a 200. diff --git a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs index 7832724c81..e319952cf0 100644 --- a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs +++ b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs @@ -17,16 +17,14 @@ namespace HealthChecksSample _task = Task.Delay(15 * 1000); } - public string Name => HealthCheckName; - - public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { if (_task.IsCompleted) { - return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready")); + return Task.FromResult(HealthCheckResult.Passed("Dependency is ready")); } - return Task.FromResult(HealthCheckResult.Unhealthy("Dependency is still initializing")); + return Task.FromResult(HealthCheckResult.Failed("Dependency is still initializing")); } } } diff --git a/samples/HealthChecksSample/SqlConnectionHealthCheck.cs b/samples/HealthChecksSample/SqlConnectionHealthCheck.cs index 5849da2202..fcc135ad1e 100644 --- a/samples/HealthChecksSample/SqlConnectionHealthCheck.cs +++ b/samples/HealthChecksSample/SqlConnectionHealthCheck.cs @@ -1,6 +1,5 @@ using System.Data.Common; using System.Data.SqlClient; -using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthChecksSample { @@ -8,13 +7,13 @@ namespace HealthChecksSample { private static readonly string DefaultTestQuery = "Select 1"; - public SqlConnectionHealthCheck(string name, string connectionString) - : this(name, connectionString, testQuery: DefaultTestQuery) + public SqlConnectionHealthCheck(string connectionString) + : this(connectionString, testQuery: DefaultTestQuery) { } - public SqlConnectionHealthCheck(string name, string connectionString, string testQuery) - : base(name, connectionString, testQuery ?? DefaultTestQuery) + public SqlConnectionHealthCheck(string connectionString, string testQuery) + : base(connectionString, testQuery ?? DefaultTestQuery) { } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 20ec5e356e..e06ef8509b 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -15,12 +15,12 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { private readonly RequestDelegate _next; private readonly HealthCheckOptions _healthCheckOptions; - private readonly IHealthCheckService _healthCheckService; + private readonly HealthCheckService _healthCheckService; public HealthCheckMiddleware( RequestDelegate next, IOptions healthCheckOptions, - IHealthCheckService healthCheckService) + HealthCheckService healthCheckService) { if (next == null) { @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) { var message = - $"No status code mapping found for {nameof(HealthCheckStatus)} value: {result.Status}." + + $"No status code mapping found for {nameof(HealthStatus)} value: {result.Status}." + $"{nameof(HealthCheckOptions)}.{nameof(HealthCheckOptions.ResultStatusCodes)} must contain" + $"an entry for {result.Status}."; diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index 7930d25577..42996e3701 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -22,20 +22,20 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// registered health checks - this is the default behavior. To run a subset of health checks, /// provide a function that filters the set of checks. /// - public Func Predicate { get; set; } + public Func Predicate { get; set; } /// - /// Gets a dictionary mapping the to an HTTP status code applied to the response. + /// Gets a dictionary mapping the to an HTTP status code applied to the response. /// This property can be used to configure the status codes returned for each status. /// - public IDictionary ResultStatusCodes { get; } = new Dictionary() + public IDictionary ResultStatusCodes { get; } = new Dictionary() { - { HealthCheckStatus.Healthy, StatusCodes.Status200OK }, - { HealthCheckStatus.Degraded, StatusCodes.Status200OK }, - { HealthCheckStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, + { HealthStatus.Healthy, StatusCodes.Status200OK }, + { HealthStatus.Degraded, StatusCodes.Status200OK }, + { HealthStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, // This means that a health check failed, so 500 is appropriate. This is an error. - { HealthCheckStatus.Failed, StatusCodes.Status500InternalServerError }, + { HealthStatus.Failed, StatusCodes.Status500InternalServerError }, }; /// @@ -43,8 +43,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// /// /// The default value is a delegate that will write a minimal text/plain response with the value - /// of as a string. + /// of as a string. /// - public Func ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; + public Func ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs index a74072f63f..50cb8c736e 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { internal static class HealthCheckResponseWriters { - public static Task WriteMinimalPlaintext(HttpContext httpContext, CompositeHealthCheckResult result) + public static Task WriteMinimalPlaintext(HttpContext httpContext, HealthReport result) { httpContext.Response.ContentType = "text/plain"; return httpContext.Response.WriteAsync(result.Status.ToString()); diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs new file mode 100644 index 0000000000..027451c0d2 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public sealed class HealthCheckContext + { + /// + /// Gets or sets the of the currently executing . + /// + public HealthCheckRegistration Registration { get; set; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs new file mode 100644 index 0000000000..9291c38846 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represent the registration information associated with an implementation. + /// + /// + /// + /// The health check registration is provided as a separate object so that application developers can customize + /// how health check implementations are configured. + /// + /// + /// The registration is provided to an implementation during execution through + /// . This allows a health check implementation to access named + /// options or perform other operations based on the registered name. + /// + /// + public sealed class HealthCheckRegistration + { + private Func _factory; + private string _name; + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// The instance. + /// + /// The that should be reported upon failure of the health check. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable tags) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + Name = name; + FailureStatus = failureStatus ?? HealthStatus.Unhealthy; + Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + Factory = (_) => instance; + } + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// A delegate used to create the instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + public HealthCheckRegistration( + string name, + Func factory, + HealthStatus? failureStatus, + IEnumerable tags) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + Name = name; + FailureStatus = failureStatus ?? HealthStatus.Unhealthy; + Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + Factory = factory; + } + + /// + /// Gets or sets a delegate used to create the instance. + /// + public Func Factory + { + get => _factory; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _factory = value; + } + } + + /// + /// Gets or sets the that should be reported upon failure of the health check. + /// + public HealthStatus FailureStatus { get; set; } + + /// + /// Gets or sets the health check name. + /// + public string Name + { + get => _name; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _name = value; + } + } + + /// + /// Gets a list of tags that can be used for filtering health checks. + /// + public ISet Tags { get; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs index e672c88db5..12cd38d08e 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs @@ -13,176 +13,65 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { private static readonly IReadOnlyDictionary _emptyReadOnlyDictionary = new Dictionary(); - private string _description; - private IReadOnlyDictionary _data; - /// - /// Gets a value indicating the status of the component that was checked. + /// Creates a new with the specified values for , , + /// , and . /// - public HealthCheckStatus Status { get; } - - /// - /// Gets an representing the exception that was thrown when checking for status (if any). - /// - /// - /// This value is expected to be 'null' if is . - /// - public Exception Exception { get; } - - /// - /// Gets a human-readable description of the status of the component that was checked. - /// - public string Description => _description ?? string.Empty; + /// A value indicating the pass/fail status of the component that was checked. + /// A human-readable description of the status of the component that was checked. + /// An representing the exception that was thrown when checking for status (if any). + /// Additional key-value pairs describing the health of the component. + public HealthCheckResult(bool result, string description, Exception exception, IReadOnlyDictionary data) + { + Result = result; + Description = description; + Exception = exception; + Data = data ?? _emptyReadOnlyDictionary; + } /// /// Gets additional key-value pairs describing the health of the component. /// - public IReadOnlyDictionary Data => _data ?? _emptyReadOnlyDictionary; + public IReadOnlyDictionary Data { get; } /// - /// Creates a new with the specified , , - /// , and . + /// Gets a human-readable description of the status of the component that was checked. /// - /// A value indicating the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public HealthCheckResult(HealthCheckStatus status, Exception exception, string description, IReadOnlyDictionary data) - { - if (status == HealthCheckStatus.Unknown) - { - throw new ArgumentException($"'{nameof(HealthCheckStatus.Unknown)}' is not a valid value for the 'status' parameter.", nameof(status)); - } + public string Description { get; } - Status = status; - Exception = exception; - _description = description; - _data = data; + /// + /// Gets an representing the exception that was thrown when checking for status (if any). + /// + public Exception Exception { get; } + + /// + /// Gets a value indicating the pass/fail status of the component that was checked. If true, then the component + /// is considered to have passed health validation. A false value will be mapped to the configured + /// by the health check system. + /// + public bool Result { get; } + + /// + /// Creates a representing a passing component. + /// + /// A representing a passing component. + /// A human-readable description of the status of the component that was checked. Optional. + /// Additional key-value pairs describing the health of the component. Optional. + public static HealthCheckResult Passed(string description = null, IReadOnlyDictionary data = null) + { + return new HealthCheckResult(result: true, description, exception: null, data); } /// - /// Creates a representing an unhealthy component. + /// Creates a representing an failing component. /// - /// A representing an unhealthy component. - public static HealthCheckResult Unhealthy() - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - public static HealthCheckResult Unhealthy(string description) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Unhealthy(string description, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: data); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// An representing the exception that was thrown when checking for status (if any). - public static HealthCheckResult Unhealthy(Exception exception) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description: string.Empty, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - public static HealthCheckResult Unhealthy(string description, Exception exception) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Unhealthy(string description, Exception exception, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data); - - /// - /// Creates a representing a healthy component. - /// - /// A representing a healthy component. - public static HealthCheckResult Healthy() - => new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing a healthy component. - /// - /// A representing a healthy component. - /// A human-readable description of the status of the component that was checked. - public static HealthCheckResult Healthy(string description) - => new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: null); - - /// - /// Creates a representing a healthy component. - /// - /// A representing a healthy component. - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Healthy(string description, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: data); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - public static HealthCheckResult Degraded() - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - public static HealthCheckResult Degraded(string description) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Degraded(string description, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: data); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - public static HealthCheckResult Degraded(Exception exception) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - public static HealthCheckResult Degraded(string description, Exception exception) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Degraded(string description, Exception exception, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data); + /// A human-readable description of the status of the component that was checked. Optional. + /// An representing the exception that was thrown when checking for status. Optional. + /// Additional key-value pairs describing the health of the component. Optional. + /// A representing an failing component. + public static HealthCheckResult Failed(string description = null, Exception exception = null, IReadOnlyDictionary data = null) + { + return new HealthCheckResult(result: false, description, exception, data); + } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckStatus.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs similarity index 53% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckStatus.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs index c16739eadc..d4293cb7b4 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckStatus.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs @@ -4,38 +4,38 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { /// - /// Represents the status of a health check result. + /// Represents the reported status of a health check result. /// /// - /// The values of this enum or ordered from least healthy to most healthy. So is - /// greater than but less than . + /// + /// A health status is derived the pass/fail result of an () + /// and the corresponding value of . + /// + /// + /// The values of this enum or ordered from least healthy to most healthy. So is + /// greater than but less than . + /// /// - public enum HealthCheckStatus + public enum HealthStatus { /// - /// This value should not be returned by a health check. It is used to represent an uninitialized value. + /// Indicates that an unexpected exception was thrown when running the health check. /// - Unknown = 0, - - /// - /// This value should not be returned by a health check. It is used to indicate that an unexpected exception was - /// thrown when running the health check. - /// - Failed = 1, + Failed = 0, /// /// Indicates that the health check determined that the component was unhealthy. /// - Unhealthy = 2, + Unhealthy = 1, /// /// Indicates that the health check determined that the component was in a degraded state. /// - Degraded = 3, + Degraded = 2, /// /// Indicates that the health check determined that the component was healthy. /// - Healthy = 4, + Healthy = 3, } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs index 2bf751180d..1b69953b67 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs @@ -12,16 +12,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// public interface IHealthCheck { - /// - /// Gets the name of the health check, which should indicate the component being checked. - /// - string Name { get; } - /// /// Runs the health check, returning the status of the component being checked. /// + /// A context object associated with the current execution. /// A that can be used to cancel the health check. /// A that completes when the health check has finished, yielding the status of the component being checked. - Task CheckHealthAsync(CancellationToken cancellationToken = default); + Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs deleted file mode 100644 index 900b15bd71..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - /// - /// Represents the results of multiple health checks. - /// - public class CompositeHealthCheckResult - { - /// - /// A containing the results from each health check. - /// - /// - /// The keys in this dictionary map to the name of the health check, the values are the - /// returned when was called for that health check. - /// - public IReadOnlyDictionary Results { get; } - - /// - /// Gets a representing the aggregate status of all the health checks. - /// - /// - /// This value is determined by taking the "worst" result of all the results. So if any result is , - /// this value is . If no result is but any result is - /// , this value is , etc. - /// - public HealthCheckStatus Status { get; } - - /// - /// Create a new from the specified results. - /// - /// A containing the results from each health check. - public CompositeHealthCheckResult(IReadOnlyDictionary results) - { - Results = results; - Status = CalculateAggregateStatus(results.Values); - } - - private HealthCheckStatus CalculateAggregateStatus(IEnumerable results) - { - // This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list - var currentValue = HealthCheckStatus.Healthy; - foreach (var result in results) - { - if (currentValue > result.Status) - { - currentValue = result.Status; - } - - if (currentValue == HealthCheckStatus.Failed) - { - // Game over, man! Game over! - // (We hit the worst possible status, so there's no need to keep iterating) - return currentValue; - } - } - - return currentValue; - } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs new file mode 100644 index 0000000000..b7a34ff35a --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs @@ -0,0 +1,149 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + internal class DefaultHealthCheckService : HealthCheckService + { + private readonly IServiceScopeFactory _scopeFactory; + private readonly IOptions _options; + private readonly ILogger _logger; + + public DefaultHealthCheckService( + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger) + { + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + // We're specifically going out of our way to do this at startup time. We want to make sure you + // get any kind of health-check related error as early as possible. Waiting until someone + // actually tries to **run** health checks would be real baaaaad. + ValidateRegistrations(_options.Value.Registrations); + } + public override async Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default) + { + var registrations = _options.Value.Registrations; + + using (var scope = _scopeFactory.CreateScope()) + { + var context = new HealthCheckContext(); + var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var registration in registrations) + { + if (predicate != null && !predicate(registration)) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var healthCheck = registration.Factory(scope.ServiceProvider); + + // If the health check does things like make Database queries using EF or backend HTTP calls, + // it may be valuable to know that logs it generates are part of a health check. So we start a scope. + using (_logger.BeginScope(new HealthCheckLogScope(registration.Name))) + { + var stopwatch = ValueStopwatch.StartNew(); + context.Registration = registration; + + Log.HealthCheckBegin(_logger, registration); + + HealthReportEntry entry; + try + { + var result = await healthCheck.CheckHealthAsync(context, cancellationToken); + + entry = new HealthReportEntry( + result.Result ? HealthStatus.Healthy : registration.FailureStatus, + result.Description, + result.Exception, + result.Data); + + Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime()); + } + catch (Exception ex) + { + entry = new HealthReportEntry(HealthStatus.Failed, ex.Message, ex, data: null); + Log.HealthCheckError(_logger, registration, ex, stopwatch.GetElapsedTime()); + } + + entries[registration.Name] = entry; + } + } + + return new HealthReport(entries); + } + } + + private static void ValidateRegistrations(IEnumerable registrations) + { + // Scan the list for duplicate names to provide a better error if there are duplicates. + var duplicateNames = registrations + .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + if (duplicateNames.Count > 0) + { + throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(registrations)); + } + } + + private static class Log + { + public static class EventIds + { + public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin"); + public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd"); + public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError"); + } + + private static readonly Action _healthCheckBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckBegin, + "Running health check {HealthCheckName}"); + + private static readonly Action _healthCheckEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckEnd, + "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}"); + + private static readonly Action _healthCheckError = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckError, + "Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms"); + + public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration) + { + _healthCheckBegin(logger, registration.Name, null); + } + + public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration) + { + _healthCheckEnd(logger, registration.Name, duration.TotalMilliseconds, entry.Status, null); + } + + public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration) + { + _healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception); + } + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DelegateHealthCheck.cs similarity index 61% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks/DelegateHealthCheck.cs index da6a92d062..94069fd7d1 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DelegateHealthCheck.cs @@ -11,31 +11,25 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// A simple implementation of which uses a provided delegate to /// implement the check. /// - public sealed class HealthCheck : IHealthCheck + internal sealed class DelegateHealthCheck : IHealthCheck { private readonly Func> _check; /// - /// Create an instance of from the specified and . + /// Create an instance of from the specified delegate. /// - /// The name of the health check, which should indicate the component being checked. /// A delegate which provides the code to execute when the health check is run. - public HealthCheck(string name, Func> check) + public DelegateHealthCheck(Func> check) { - Name = name ?? throw new ArgumentNullException(nameof(name)); _check = check ?? throw new ArgumentNullException(nameof(check)); } - /// - /// Gets the name of the health check, which should indicate the component being checked. - /// - public string Name { get; } - /// /// Runs the health check, returning the status of the component being checked. /// + /// A context object associated with the current execution. /// A that can be used to cancel the health check. /// A that completes when the health check has finished, yielding the status of the component being checked. - public Task CheckHealthAsync(CancellationToken cancellationToken = default) => _check(cancellationToken); + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) => _check(cancellationToken); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceCollectionExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs similarity index 68% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceCollectionExtensions.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs index f0caa8db1f..76b0dc45c1 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceCollectionExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs @@ -7,24 +7,24 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Microsoft.Extensions.DependencyInjection { /// - /// Provides extension methods for registering in an . + /// Provides extension methods for registering in an . /// public static class HealthCheckServiceCollectionExtensions { /// - /// Adds the to the container, using the provided delegate to register + /// Adds the to the container, using the provided delegate to register /// health checks. /// /// /// This operation is idempotent - multiple invocations will still only result in a single - /// instance in the . It can be invoked + /// instance in the . It can be invoked /// multiple times in order to get access to the in multiple places. /// - /// The to add the to. + /// The to add the to. /// An instance of from which health checks can be registered. public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); return new HealthChecksBuilder(services); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs new file mode 100644 index 0000000000..231dd51717 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + internal class HealthChecksBuilder : IHealthChecksBuilder + { + public HealthChecksBuilder(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + + public IHealthChecksBuilder Add(HealthCheckRegistration registration) + { + if (registration == null) + { + throw new ArgumentNullException(nameof(registration)); + } + + Services.Configure(options => + { + options.Registrations.Add(registration); + }); + + return this; + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs new file mode 100644 index 0000000000..9508889054 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs @@ -0,0 +1,191 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Provides basic extension methods for registering instances in an . + /// + public static class HealthChecksBuilderAddCheckExtensions + { + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// An instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + IHealthCheck instance, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. If a service of type is registred in the dependency injection container + /// with any liftime it will be used. Otherwise an instance of type will be constructed with + /// access to services from the dependency injection container. + /// + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus = null, + IEnumerable tags = null) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.GetServiceOrCreateInstance(s), failureStatus, tags)); + } + + // NOTE: AddTypeActivatedCheck has overloads rather than default parameters values, because default parameter values don't + // play super well with params. + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// Additional arguments to provide to the constructor. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck(this IHealthChecksBuilder builder, string name, params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return AddTypeActivatedCheck(builder, name, failureStatus: null, tags: null); + } + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// Additional arguments to provide to the constructor. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return AddTypeActivatedCheck(builder, name, failureStatus, tags: null); + } + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// Additional arguments to provide to the constructor. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + IEnumerable tags, + params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.CreateInstance(s, args), failureStatus, tags)); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs new file mode 100644 index 0000000000..70edd0649d --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs @@ -0,0 +1,169 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Provides extension methods for registering delegates with the . + /// + public static class HealthChecksBuilderDelegateExtensions + { + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => Task.FromResult(check())); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct))); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => check()); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => check(ct)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs new file mode 100644 index 0000000000..eb78293f87 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// A builder used to register health checks. + /// + public interface IHealthChecksBuilder + { + /// + /// Adds a for a health check. + /// + /// The . + IHealthChecksBuilder Add(HealthCheckRegistration registration); + + /// + /// Gets the into which instances should be registered. + /// + IServiceCollection Services { get; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs index 111113ea77..e4a128148d 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs @@ -2,143 +2,60 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Internal; -using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.Diagnostics.HealthChecks { - internal class HealthCheckService : IHealthCheckService + /// + /// A service which can be used to check the status of instances + /// registered in the application. + /// + /// + /// + /// The default implementation of is registered in the dependency + /// injection container as a singleton service by calling + /// . + /// + /// + /// The returned by + /// + /// provides a convenience API for registering health checks. + /// + /// + /// implementations can be registered through extension methods provided by + /// . + /// + /// + public abstract class HealthCheckService { - private readonly IServiceScopeFactory _scopeFactory; - private readonly ILogger _logger; - - public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger logger) + /// + /// Runs all the health checks in the application and returns the aggregated status. + /// + /// A which can be used to cancel the health checks. + /// + /// A which will complete when all the health checks have been run, + /// yielding a containing the results. + /// + public Task CheckHealthAsync(CancellationToken cancellationToken = default) { - _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - // We're specifically going out of our way to do this at startup time. We want to make sure you - // get any kind of health-check related error as early as possible. Waiting until someone - // actually tries to **run** health checks would be real baaaaad. - using (var scope = _scopeFactory.CreateScope()) - { - var healthChecks = scope.ServiceProvider.GetRequiredService>(); - EnsureNoDuplicates(healthChecks); - } + return CheckHealthAsync(predicate: null, cancellationToken); } - public Task CheckHealthAsync(CancellationToken cancellationToken = default) => - CheckHealthAsync(predicate: null, cancellationToken); - - public async Task CheckHealthAsync( - Func predicate, - CancellationToken cancellationToken = default) - { - using (var scope = _scopeFactory.CreateScope()) - { - var healthChecks = scope.ServiceProvider.GetRequiredService>(); - - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var healthCheck in healthChecks) - { - if (predicate != null && !predicate(healthCheck)) - { - continue; - } - - cancellationToken.ThrowIfCancellationRequested(); - - // If the health check does things like make Database queries using EF or backend HTTP calls, - // it may be valuable to know that logs it generates are part of a health check. So we start a scope. - using (_logger.BeginScope(new HealthCheckLogScope(healthCheck.Name))) - { - HealthCheckResult result; - try - { - Log.HealthCheckBegin(_logger, healthCheck); - var stopwatch = ValueStopwatch.StartNew(); - result = await healthCheck.CheckHealthAsync(cancellationToken); - Log.HealthCheckEnd(_logger, healthCheck, result, stopwatch.GetElapsedTime()); - } - catch (Exception ex) - { - Log.HealthCheckError(_logger, healthCheck, ex); - result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null); - } - - // This can only happen if the result is default(HealthCheckResult) - if (result.Status == HealthCheckStatus.Unknown) - { - // This is different from the case above. We throw here because a health check is doing something specifically incorrect. - throw new InvalidOperationException($"Health check '{healthCheck.Name}' returned a result with a status of Unknown"); - } - - results[healthCheck.Name] = result; - } - } - - return new CompositeHealthCheckResult(results); - } - } - - private static void EnsureNoDuplicates(IEnumerable healthChecks) - { - // Scan the list for duplicate names to provide a better error if there are duplicates. - var duplicateNames = healthChecks - .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) - .Where(g => g.Count() > 1) - .Select(g => g.Key) - .ToList(); - - if (duplicateNames.Count > 0) - { - throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(healthChecks)); - } - } - - private static class Log - { - public static class EventIds - { - public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin"); - public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd"); - public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError"); - } - - private static readonly Action _healthCheckBegin = LoggerMessage.Define( - LogLevel.Debug, - EventIds.HealthCheckBegin, - "Running health check {HealthCheckName}"); - - private static readonly Action _healthCheckEnd = LoggerMessage.Define( - LogLevel.Debug, - EventIds.HealthCheckEnd, - "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}"); - - private static readonly Action _healthCheckError = LoggerMessage.Define( - LogLevel.Error, - EventIds.HealthCheckError, - "Health check {HealthCheckName} threw an unhandled exception"); - - public static void HealthCheckBegin(ILogger logger, IHealthCheck healthCheck) - { - _healthCheckBegin(logger, healthCheck.Name, null); - } - - public static void HealthCheckEnd(ILogger logger, IHealthCheck healthCheck, HealthCheckResult result, TimeSpan duration) - { - _healthCheckEnd(logger, healthCheck.Name, duration.TotalMilliseconds, result.Status, null); - } - - public static void HealthCheckError(ILogger logger, IHealthCheck healthCheck, Exception exception) - { - _healthCheckError(logger, healthCheck.Name, exception); - } - } + /// + /// Runs the provided health checks and returns the aggregated status + /// + /// + /// A predicate that can be used to include health checks based on user-defined criteria. + /// + /// A which can be used to cancel the health checks. + /// + /// A which will complete when all the health checks have been run, + /// yielding a containing the results. + /// + public abstract Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs new file mode 100644 index 0000000000..b8dfdb9b40 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Options for the default implementation of + /// + public sealed class HealthCheckServiceOptions + { + /// + /// Gets the health check registrations. + /// + public ICollection Registrations { get; } = new List(); + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs deleted file mode 100644 index 4e1d851eff..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - internal class HealthChecksBuilder : IHealthChecksBuilder - { - public IServiceCollection Services { get; } - - public HealthChecksBuilder(IServiceCollection services) - { - Services = services; - } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs deleted file mode 100644 index d3af5b17c2..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// Provides extension methods for registering instances in an . - /// - public static class HealthChecksBuilderAddCheckExtensions - { - /// - /// Adds a new health check with the specified name and implementation. - /// - /// The to add the check to. - /// The name of the health check, which should indicate the component being checked. - /// A delegate which provides the code to execute when the health check is run. - /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (check == null) - { - throw new ArgumentNullException(nameof(check)); - } - - return builder.AddCheck(new HealthCheck(name, check)); - } - - /// - /// Adds a new health check with the specified name and implementation. - /// - /// The to add the check to. - /// The name of the health check, which should indicate the component being checked. - /// A delegate which provides the code to execute when the health check is run. - /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (check == null) - { - throw new ArgumentNullException(nameof(check)); - } - - return builder.AddCheck(name, _ => check()); - } - - /// - /// Adds a new health check with the provided implementation. - /// - /// The to add the check to. - /// An implementation. - /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, IHealthCheck check) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (check == null) - { - throw new ArgumentNullException(nameof(check)); - } - - builder.Services.AddSingleton(check); - return builder; - } - - /// - /// Adds a new health check as a transient dependency injected service with the provided type. - /// - /// The health check implementation type. - /// The . - /// The . - /// - /// This method will register a transient service of type with the - /// provided implementation type . Using this method to register a health - /// check allows you to register a health check that depends on transient and scoped services. - /// - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder) where T : class, IHealthCheck - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - builder.Services.Add(ServiceDescriptor.Transient(typeof(IHealthCheck), typeof(T))); - return builder; - } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs new file mode 100644 index 0000000000..29359a547f --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represents the result of executing a group of instances. + /// + public sealed class HealthReport + { + /// + /// Create a new from the specified results. + /// + /// A containing the results from each health check. + public HealthReport(IReadOnlyDictionary entries) + { + Entries = entries; + Status = CalculateAggregateStatus(entries.Values); + } + + /// + /// A containing the results from each health check. + /// + /// + /// The keys in this dictionary map the name of each executed health check to a for the + /// result data retruned from the corresponding health check. + /// + public IReadOnlyDictionary Entries { get; } + + /// + /// Gets a representing the aggregate status of all the health checks. The value of + /// will be the most servere status reported by a health check. If no checks were executed, the value is always . + /// + public HealthStatus Status { get; } + + private HealthStatus CalculateAggregateStatus(IEnumerable entries) + { + // This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list + var currentValue = HealthStatus.Healthy; + foreach (var entry in entries) + { + if (currentValue > entry.Status) + { + currentValue = entry.Status; + } + + if (currentValue == HealthStatus.Failed) + { + // Game over, man! Game over! + // (We hit the worst possible status, so there's no need to keep iterating) + return currentValue; + } + } + + return currentValue; + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs new file mode 100644 index 0000000000..17ed5ae288 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represents an entry in a . Corresponds to the result of a single . + /// + public struct HealthReportEntry + { + private static readonly IReadOnlyDictionary _emptyReadOnlyDictionary = new Dictionary(); + + /// + /// Creates a new with the specified values for , , + /// , and . + /// + /// A value indicating the health status of the component that was checked. + /// A human-readable description of the status of the component that was checked. + /// An representing the exception that was thrown when checking for status (if any). + /// Additional key-value pairs describing the health of the component. + public HealthReportEntry(HealthStatus status, string description, Exception exception, IReadOnlyDictionary data) + { + Status = status; + Description = description; + Exception = exception; + Data = data ?? _emptyReadOnlyDictionary; + } + + /// + /// Gets additional key-value pairs describing the health of the component. + /// + public IReadOnlyDictionary Data { get; } + + /// + /// Gets a human-readable description of the status of the component that was checked. + /// + public string Description { get; } + + /// + /// Gets an representing the exception that was thrown when checking for status (if any). + /// + public Exception Exception { get; } + + /// + /// Gets the health status of the component that was checked. The is based on the pass/fail value of + /// and the configured value of . + /// + public HealthStatus Status { get; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs deleted file mode 100644 index f931475a4d..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - /// - /// A service which can be used to check the status of instances - /// registered in the application. - /// - /// - /// - /// The default implementation of is registered in the dependency - /// injection container as a singleton service by calling - /// . - /// - /// - /// The returned by - /// - /// provides a convenience API for registering health checks. - /// - /// - /// The default implementation of will use all services - /// of type registered in the dependency injection container. - /// implementations may be registered with any service lifetime. The implementation will create a scope - /// for each aggregate health check operation and use the scope to resolve services. The scope - /// created for executing health checks is controlled by the health checks service and does not - /// share scoped services with any other scope in the application. - /// - /// - public interface IHealthCheckService - { - /// - /// Runs all the health checks in the application and returns the aggregated status. - /// - /// A which can be used to cancel the health checks. - /// - /// A which will complete when all the health checks have been run, - /// yielding a containing the results. - /// - Task CheckHealthAsync(CancellationToken cancellationToken = default); - - /// - /// Runs the provided health checks and returns the aggregated status - /// - /// - /// A predicate that can be used to include health checks based on user-defined criteria. - /// - /// A which can be used to cancel the health checks. - /// - /// A which will complete when all the health checks have been run, - /// yielding a containing the results. - /// - Task CheckHealthAsync(Func predicate, - CancellationToken cancellationToken = default); - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs deleted file mode 100644 index 22cd691aa2..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - /// - /// A builder used to collect instances of and register them on an . - /// - /// - /// This type wraps an and provides a place for health check components to attach extension - /// methods for registering themselves in the . - /// - public interface IHealthChecksBuilder - { - /// - /// Gets the into which instances should be registered. - /// - IServiceCollection Services { get; } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index 2b3d0e6261..725226023e 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -3,8 +3,8 @@ Components for performing health checks in .NET applications Commonly Used Types: -Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService -Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder +Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService +Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder netstandard2.0 $(NoWarn);CS1591 @@ -14,6 +14,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder + diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 16fe68ac2a..7a2435948c 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -100,7 +98,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } - [Fact] public async Task StatusCodeIs200IfAllChecksHealthy() { @@ -112,9 +109,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!")) + .AddCheck("Bar", () => HealthCheckResult.Passed("A-ok!")) + .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!")); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -137,9 +134,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Not so great."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!")) + .AddCheck("Bar", () => HealthCheckResult.Failed("Not so great."), failureStatus: HealthStatus.Degraded) + .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!")); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -162,9 +159,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -187,9 +184,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(new HealthCheckResult(HealthCheckStatus.Failed, null, null, null))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => throw null) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -225,9 +222,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -254,9 +251,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -277,7 +274,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { ResultStatusCodes = { - [HealthCheckStatus.Healthy] = 201, + [HealthStatus.Healthy] = 201, } }); }) @@ -308,10 +305,10 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) // Will get filtered out - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("A-ok!"))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs deleted file mode 100644 index 3852969cda..0000000000 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Xunit; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Tests -{ - public class CompositeHealthCheckResultTests - { - [Theory] - [InlineData(HealthCheckStatus.Healthy)] - [InlineData(HealthCheckStatus.Degraded)] - [InlineData(HealthCheckStatus.Unhealthy)] - [InlineData(HealthCheckStatus.Failed)] - public void Status_MatchesWorstStatusInResults(HealthCheckStatus statusValue) - { - var result = new CompositeHealthCheckResult(new Dictionary() - { - {"Foo", HealthCheckResult.Healthy() }, - {"Bar", HealthCheckResult.Healthy() }, - {"Baz", new HealthCheckResult(statusValue, exception: null, description: null, data: null) }, - {"Quick", HealthCheckResult.Healthy() }, - {"Quack", HealthCheckResult.Healthy() }, - {"Quock", HealthCheckResult.Healthy() }, - }); - - Assert.Equal(statusValue, result.Status); - } - } -} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs similarity index 59% rename from test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs rename to test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs index 6e7c28e9ef..024309a024 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs @@ -8,11 +8,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.Extensions.Diagnostics.HealthChecks { - public class HealthCheckServiceTests + public class DefaultHealthCheckServiceTest { [Fact] public void Constructor_ThrowsUsefulExceptionForDuplicateNames() @@ -25,22 +26,23 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks serviceCollection.AddLogging(); serviceCollection.AddOptions(); serviceCollection.AddHealthChecks() - .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) - .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) - .AddCheck(new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy()))) - .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))) - .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))); + .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))); var services = serviceCollection.BuildServiceProvider(); - + var scopeFactory = services.GetRequiredService(); - var logger = services.GetRequiredService>(); + var options = services.GetRequiredService>(); + var logger = services.GetRequiredService>(); // Act - var exception = Assert.Throws(() => new HealthCheckService(scopeFactory, logger)); + var exception = Assert.Throws(() => new DefaultHealthCheckService(scopeFactory, options, logger)); // Assert - Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message); + Assert.StartsWith($"Duplicate health checks were registered with the name(s): Foo, Baz", exception.Message); } [Fact] @@ -61,21 +63,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); - b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); - b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); + b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data))); + b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded); + b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Results, + Assert.Collection(results.Entries, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Collection(actual.Value.Data, item => { @@ -87,7 +89,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { Assert.Equal("DegradedCheck", actual.Key); Assert.Equal(DegradedMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status); + Assert.Equal(HealthStatus.Degraded, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Empty(actual.Value.Data); }, @@ -95,7 +97,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { Assert.Equal("UnhealthyCheck", actual.Key); Assert.Equal(UnhealthyMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Unhealthy, actual.Value.Status); + Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); Assert.Same(exception, actual.Value.Exception); Assert.Empty(actual.Value.Data); }); @@ -119,21 +121,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); - b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); - b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); + b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data))); + b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded); + b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck"); // Assert - Assert.Collection(results.Results, + Assert.Collection(results.Entries, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Collection(actual.Value.Data, item => { @@ -144,7 +146,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks } [Fact] - public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckerToFailedResultAsync() + public async Task CheckHealthAsync_SetsRegistrationForEachCheck() { // Arrange var thrownException = new InvalidOperationException("Whoops!"); @@ -152,35 +154,79 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - b.AddCheck("Throws", ct => throw thrownException); - b.AddCheck("Faults", ct => Task.FromException(faultedException)); - b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())); + b.AddCheck("A"); + b.AddCheck("B"); + b.AddCheck("C"); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Results, + Assert.Collection( + results.Entries, + actual => + { + Assert.Equal("A", actual.Key); + Assert.Collection( + actual.Value.Data, + kvp => Assert.Equal(kvp, new KeyValuePair("name", "A"))); + }, + actual => + { + Assert.Equal("B", actual.Key); + Assert.Collection( + actual.Value.Data, + kvp => Assert.Equal(kvp, new KeyValuePair("name", "B"))); + }, + actual => + { + Assert.Equal("C", actual.Key); + Assert.Collection( + actual.Value.Data, + kvp => Assert.Equal(kvp, new KeyValuePair("name", "C"))); + }); + } + + [Fact] + public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToFailedResultAsync() + { + // Arrange + var thrownException = new InvalidOperationException("Whoops!"); + var faultedException = new InvalidOperationException("Ohnoes!"); + + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("Throws", ct => throw thrownException); + b.AddAsyncCheck("Faults", ct => Task.FromException(faultedException)); + b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Passed())); + }); + + // Act + var results = await service.CheckHealthAsync(); + + // Assert + Assert.Collection( + results.Entries, actual => { Assert.Equal("Throws", actual.Key); Assert.Equal(thrownException.Message, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status); + Assert.Equal(HealthStatus.Failed, actual.Value.Status); Assert.Same(thrownException, actual.Value.Exception); }, actual => { Assert.Equal("Faults", actual.Key); Assert.Equal(faultedException.Message, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status); + Assert.Equal(HealthStatus.Failed, actual.Value.Status); Assert.Same(faultedException, actual.Value.Exception); }, actual => { Assert.Equal("Succeeds", actual.Key); - Assert.Empty(actual.Value.Description); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Null(actual.Value.Description); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); }); } @@ -190,12 +236,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { // Arrange var sink = new TestSink(); - var check = new HealthCheck("TestScope", cancellationToken => + var check = new DelegateHealthCheck(cancellationToken => { Assert.Collection(sink.Scopes, actual => { - Assert.Equal(actual.LoggerName, typeof(HealthCheckService).FullName); + Assert.Equal(actual.LoggerName, typeof(DefaultHealthCheckService).FullName); Assert.Collection((IEnumerable>)actual.Scope, item => { @@ -203,7 +249,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks Assert.Equal("TestScope", item.Value); }); }); - return Task.FromResult(HealthCheckResult.Healthy()); + return Task.FromResult(HealthCheckResult.Passed()); }); var loggerFactory = new TestLoggerFactory(sink, enabled: true); @@ -212,36 +258,20 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Override the logger factory for testing b.Services.AddSingleton(loggerFactory); - b.AddCheck(check); + b.AddCheck("TestScope", check); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Results, actual => + Assert.Collection(results.Entries, actual => { Assert.Equal("TestScope", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } - [Fact] - public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult() - { - // Arrange - var service = CreateHealthChecksService(b => - { - b.AddCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))); - }); - - // Act - var ex = await Assert.ThrowsAsync(() => service.CheckHealthAsync()); - - // Assert - Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message); - } - [Fact] public async Task CheckHealthAsync_CheckCanDependOnTransientService() { @@ -250,7 +280,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.Services.AddTransient(); - b.AddCheck(); + b.AddCheck("Test"); }); // Act @@ -258,11 +288,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Collection( - results.Results, + results.Entries, actual => { Assert.Equal("Test", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } @@ -274,7 +304,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.Services.AddScoped(); - b.AddCheck(); + b.AddCheck("Test"); }); // Act @@ -282,11 +312,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Collection( - results.Results, + results.Entries, actual => { Assert.Equal("Test", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } @@ -298,7 +328,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.Services.AddSingleton(); - b.AddCheck(); + b.AddCheck("Test"); }); // Act @@ -306,15 +336,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Collection( - results.Results, + results.Entries, actual => { Assert.Equal("Test", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } - private static HealthCheckService CreateHealthChecksService(Action configure) + private static DefaultHealthCheckService CreateHealthChecksService(Action configure) { var services = new ServiceCollection(); services.AddLogging(); @@ -326,7 +356,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks configure(builder); } - return (HealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService(); + return (DefaultHealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService(); } private class AnotherService { } @@ -336,12 +366,22 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public CheckWithServiceDependency(AnotherService _) { } - - public string Name => "Test"; - - public Task CheckHealthAsync(CancellationToken cancellationToken = default) + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - return Task.FromResult(HealthCheckResult.Healthy()); + return Task.FromResult(HealthCheckResult.Passed()); + } + } + + private class NameCapturingCheck : IHealthCheck + { + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var data = new Dictionary() + { + { "name", context.Registration.Name }, + }; + return Task.FromResult(HealthCheckResult.Passed(data: data)); } } } diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs new file mode 100644 index 0000000000..c4974013c7 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs @@ -0,0 +1,257 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection +{ + // Integration tests for extension methods on IHealthCheckBuilder + // + // We test the longest overload of each 'family' of Add...Check methods, since they chain to each other. + public class HealthChecksBuilderTest + { + [Fact] + public void AddCheck_Instance() + { + // Arrange + var instance = new DelegateHealthCheck((_) => + { + return Task.FromResult(HealthCheckResult.Passed()); + }); + + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded,tags: new[] { "tag", }, instance: instance); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.Same(instance, registration.Factory(serviceProvider)); + } + + [Fact] + public void AddCheck_T_TypeActivated() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddCheck_T_Service() + { + // Arrange + var instance = new TestHealthCheck(); + + var services = CreateServices(); + services.AddSingleton(instance); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.Same(instance, registration.Factory(serviceProvider)); + } + + [Fact] + public void AddTypeActivatedCheck() + { + // Arrange + var services = CreateServices(); + services + .AddHealthChecks() + .AddTypeActivatedCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, args: new object[] { 5, "hi", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + + var check = Assert.IsType(registration.Factory(serviceProvider)); + Assert.Equal(5, check.I); + Assert.Equal("hi", check.S); + } + + [Fact] + public void AddDelegateCheck_NoArg() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, check: () => + { + return HealthCheckResult.Passed(); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddDelegateCheck_CancellationToken() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", (_) => + { + return HealthCheckResult.Passed(); + }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddAsyncDelegateCheck_NoArg() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddAsyncCheck("test", () => + { + return Task.FromResult(HealthCheckResult.Passed()); + }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddAsyncDelegateCheck_CancellationToken() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddAsyncCheck("test", (_) => + { + return Task.FromResult(HealthCheckResult.Passed()); + }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks() + { + var services = new ServiceCollection(); + services + .AddHealthChecks() + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed())); + services + .AddHealthChecks() + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Passed())); + + // Act + var options = services.BuildServiceProvider().GetRequiredService>(); + + // Assert + Assert.Collection( + options.Value.Registrations, + actual => Assert.Equal("Foo", actual.Name), + actual => Assert.Equal("Bar", actual.Name)); + } + + private IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + return services; + } + + private class TestHealthCheck : IHealthCheck + { + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + } + + private class TestHealthCheckWithArgs : IHealthCheck + { + public TestHealthCheckWithArgs(int i, string s) + { + I = i; + S = s; + } + + public int I { get; set; } + + public string S { get; set; } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/ServiceCollectionExtensionsTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs similarity index 76% rename from test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/ServiceCollectionExtensionsTests.cs rename to test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs index b33bd36eb6..34b92b7b5e 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/ServiceCollectionExtensionsTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs @@ -1,12 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Xunit; -namespace Microsoft.Extensions.Diagnostics.HealthChecks +namespace Microsoft.Extensions.DependencyInjection { - public class ServiceCollectionExtensionsTests + public class ServiceCollectionExtensionsTest { [Fact] public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently() @@ -23,8 +23,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks actual => { Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); - Assert.Equal(typeof(IHealthCheckService), actual.ServiceType); - Assert.Equal(typeof(HealthCheckService), actual.ImplementationType); + Assert.Equal(typeof(HealthCheckService), actual.ServiceType); + Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType); Assert.Null(actual.ImplementationInstance); Assert.Null(actual.ImplementationFactory); }); diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs deleted file mode 100644 index f1bcbbf00b..0000000000 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - public class HealthChecksBuilderTests - { - [Fact] - public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks() - { - var services = new ServiceCollection(); - services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy())); - services.AddHealthChecks() - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy())); - - // Act - var checks = services.BuildServiceProvider().GetRequiredService>(); - - // Assert - Assert.Collection( - checks, - actual => Assert.Equal("Foo", actual.Name), - actual => Assert.Equal("Bar", actual.Name)); - } - } -} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs new file mode 100644 index 0000000000..9ce0a4b620 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public class HealthReportTest + { + [Theory] + [InlineData(HealthStatus.Healthy)] + [InlineData(HealthStatus.Degraded)] + [InlineData(HealthStatus.Unhealthy)] + [InlineData(HealthStatus.Failed)] + public void Status_MatchesWorstStatusInResults(HealthStatus status) + { + var result = new HealthReport(new Dictionary() + { + {"Foo", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Bar", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Baz", new HealthReportEntry(status, exception: null, description: null, data: null) }, + {"Quick", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Quack", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Quock", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + }); + + Assert.Equal(status, result.Status); + } + } +} diff --git a/version.props b/version.props index 3cba6cd9d8..0c50eff262 100644 --- a/version.props +++ b/version.props @@ -8,7 +8,6 @@ a- $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) $(VersionSuffix)-$(BuildNumber) - 0.6.0 alpha1 $(ExperimentalVersionPrefix)