diff --git a/src/HealthChecks/Abstractions/Directory.Build.props b/src/HealthChecks/Abstractions/Directory.Build.props
new file mode 100644
index 0000000000..f25c1d90ce
--- /dev/null
+++ b/src/HealthChecks/Abstractions/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+ true
+
+
diff --git a/src/HealthChecks/Abstractions/src/HealthCheckContext.cs b/src/HealthChecks/Abstractions/src/HealthCheckContext.cs
new file mode 100644
index 0000000000..027451c0d2
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/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/HealthChecks/Abstractions/src/HealthCheckRegistration.cs b/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs
new file mode 100644
index 0000000000..9291c38846
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/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/HealthChecks/Abstractions/src/HealthCheckResult.cs b/src/HealthChecks/Abstractions/src/HealthCheckResult.cs
new file mode 100644
index 0000000000..e01cb5aceb
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/HealthCheckResult.cs
@@ -0,0 +1,88 @@
+// 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 the result of a health check.
+ ///
+ public struct HealthCheckResult
+ {
+ private static readonly IReadOnlyDictionary _emptyReadOnlyDictionary = new Dictionary();
+
+ ///
+ /// Creates a new with the specified values for ,
+ /// , , and .
+ ///
+ /// A value indicating the 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(HealthStatus status, string description = null, Exception exception = null, IReadOnlyDictionary data = null)
+ {
+ 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 a value indicating the status of the component that was checked.
+ ///
+ public HealthStatus Status { get; }
+
+ ///
+ /// Creates a representing a healthy 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.
+ /// A representing a healthy component.
+ public static HealthCheckResult Healthy(string description = null, IReadOnlyDictionary data = null)
+ {
+ return new HealthCheckResult(status: HealthStatus.Healthy, description, exception: null, data);
+ }
+
+
+ ///
+ /// Creates a representing a degraded component.
+ ///
+ /// 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 a degraged component.
+ public static HealthCheckResult Degraded(string description = null, Exception exception = null, IReadOnlyDictionary data = null)
+ {
+ return new HealthCheckResult(status: HealthStatus.Degraded, description, exception: null, data);
+ }
+
+ ///
+ /// Creates a representing an unhealthy component.
+ ///
+ /// 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 unhealthy component.
+ public static HealthCheckResult Unhealthy(string description = null, Exception exception = null, IReadOnlyDictionary data = null)
+ {
+ return new HealthCheckResult(status: HealthStatus.Unhealthy, description, exception, data);
+ }
+ }
+}
diff --git a/src/HealthChecks/Abstractions/src/HealthReport.cs b/src/HealthChecks/Abstractions/src/HealthReport.cs
new file mode 100644
index 0000000000..91ed798811
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/HealthReport.cs
@@ -0,0 +1,68 @@
+// 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 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.
+ /// A value indicating the time the health check service took to execute.
+ public HealthReport(IReadOnlyDictionary entries, TimeSpan totalDuration)
+ {
+ Entries = entries;
+ Status = CalculateAggregateStatus(entries.Values);
+ TotalDuration = totalDuration;
+ }
+
+ ///
+ /// 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; }
+
+ ///
+ /// Gets the time the health check service took to execute.
+ ///
+ public TimeSpan TotalDuration { 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.Unhealthy)
+ {
+ // 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/HealthChecks/Abstractions/src/HealthReportEntry.cs b/src/HealthChecks/Abstractions/src/HealthReportEntry.cs
new file mode 100644
index 0000000000..6e7d6c6b8e
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/HealthReportEntry.cs
@@ -0,0 +1,59 @@
+// 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.
+ /// A value indicating the health execution duration.
+ /// 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, TimeSpan duration, Exception exception, IReadOnlyDictionary data)
+ {
+ Status = status;
+ Description = description;
+ Duration = duration;
+ 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 the health check execution duration.
+ ///
+ public TimeSpan Duration { 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.
+ ///
+ public HealthStatus Status { get; }
+ }
+}
diff --git a/src/HealthChecks/Abstractions/src/HealthStatus.cs b/src/HealthChecks/Abstractions/src/HealthStatus.cs
new file mode 100644
index 0000000000..61b76d54fa
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/HealthStatus.cs
@@ -0,0 +1,37 @@
+// 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
+{
+ ///
+ /// Represents the reported status of a health check result.
+ ///
+ ///
+ ///
+ /// A status of should be considered the default value for a failing health check. Application
+ /// developers may configure a health check to report a different status as desired.
+ ///
+ ///
+ /// The values of this enum or ordered from least healthy to most healthy. So is
+ /// greater than but less than .
+ ///
+ ///
+ public enum HealthStatus
+ {
+ ///
+ /// Indicates that the health check determined that the component was unhealthy, or an unhandled
+ /// exception was thrown while executing the health check.
+ ///
+ Unhealthy = 0,
+
+ ///
+ /// Indicates that the health check determined that the component was in a degraded state.
+ ///
+ Degraded = 1,
+
+ ///
+ /// Indicates that the health check determined that the component was healthy.
+ ///
+ Healthy = 2,
+ }
+}
diff --git a/src/HealthChecks/Abstractions/src/IHealthCheck.cs b/src/HealthChecks/Abstractions/src/IHealthCheck.cs
new file mode 100644
index 0000000000..1b69953b67
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/IHealthCheck.cs
@@ -0,0 +1,23 @@
+// 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;
+
+namespace Microsoft.Extensions.Diagnostics.HealthChecks
+{
+ ///
+ /// Represents a health check, which can be used to check the status of a component in the application, such as a backend service, database or some internal
+ /// state.
+ ///
+ public interface IHealthCheck
+ {
+ ///
+ /// 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(HealthCheckContext context, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/HealthChecks/Abstractions/src/IHealthCheckPublisher.cs b/src/HealthChecks/Abstractions/src/IHealthCheckPublisher.cs
new file mode 100644
index 0000000000..f1809c4bb8
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/IHealthCheckPublisher.cs
@@ -0,0 +1,39 @@
+// 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;
+
+namespace Microsoft.Extensions.Diagnostics.HealthChecks
+{
+ ///
+ /// Represents a publisher of information.
+ ///
+ ///
+ ///
+ /// The default health checks implementation provided an IHostedService implementation that can
+ /// be used to execute health checks at regular intervals and provide the resulting
+ /// data to all registered instances.
+ ///
+ ///
+ /// To provide an implementation, register an instance or type as a singleton
+ /// service in the dependency injection container.
+ ///
+ ///
+ /// instances are provided with a after executing
+ /// health checks in a background thread. The use of depend on hosting in
+ /// an application using IWebHost or generic host (IHost). Execution of
+ /// instance is not related to execution of health checks via a middleware.
+ ///
+ ///
+ public interface IHealthCheckPublisher
+ {
+ ///
+ /// Publishes the provided .
+ ///
+ /// The . The result of executing a set of health checks.
+ /// The .
+ /// A which will complete when publishing is complete.
+ Task PublishAsync(HealthReport report, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/HealthChecks/Abstractions/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj b/src/HealthChecks/Abstractions/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj
new file mode 100644
index 0000000000..b95d66f7b3
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Abstractions for defining health checks in .NET applications
+
+Commonly Used Types
+Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck
+
+ Microsoft.Extensions.Diagnostics.HealthChecks
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ diagnostics;healthchecks
+
+
+
diff --git a/src/HealthChecks/Abstractions/src/baseline.netcore.json b/src/HealthChecks/Abstractions/src/baseline.netcore.json
new file mode 100644
index 0000000000..871db4c089
--- /dev/null
+++ b/src/HealthChecks/Abstractions/src/baseline.netcore.json
@@ -0,0 +1,5 @@
+{
+ "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ ]
+}
\ No newline at end of file
diff --git a/src/HealthChecks/HealthChecks/Directory.Build.props b/src/HealthChecks/HealthChecks/Directory.Build.props
new file mode 100644
index 0000000000..f25c1d90ce
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+ true
+
+
diff --git a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs
new file mode 100644
index 0000000000..d5d71d9cb4
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs
@@ -0,0 +1,304 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+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);
+
+ var totalTime = ValueStopwatch.StartNew();
+ Log.HealthCheckProcessingBegin(_logger);
+
+ 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);
+ var duration = stopwatch.GetElapsedTime();
+
+ entry = new HealthReportEntry(
+ status: result.Status,
+ description: result.Description,
+ duration: duration,
+ exception: result.Exception,
+ data: result.Data);
+
+ Log.HealthCheckEnd(_logger, registration, entry, duration);
+ Log.HealthCheckData(_logger, registration, entry);
+ }
+
+ // Allow cancellation to propagate.
+ catch (Exception ex) when (ex as OperationCanceledException == null)
+ {
+ var duration = stopwatch.GetElapsedTime();
+ entry = new HealthReportEntry(
+ status: HealthStatus.Unhealthy,
+ description: ex.Message,
+ duration: duration,
+ exception: ex,
+ data: null);
+
+ Log.HealthCheckError(_logger, registration, ex, duration);
+ }
+
+ entries[registration.Name] = entry;
+ }
+ }
+
+ var totalElapsedTime = totalTime.GetElapsedTime();
+ var report = new HealthReport(entries, totalElapsedTime);
+ Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime);
+ return report;
+ }
+ }
+
+ 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));
+ }
+ }
+
+ internal static class EventIds
+ {
+ public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin");
+ public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd");
+
+ public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin");
+ public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd");
+ public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError");
+ public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData");
+ }
+
+ private static class Log
+ {
+ private static readonly Action _healthCheckProcessingBegin = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckProcessingBegin,
+ "Running health checks");
+
+ private static readonly Action _healthCheckProcessingEnd = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckProcessingEnd,
+ "Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}");
+
+ private static readonly Action _healthCheckBegin = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckBegin,
+ "Running health check {HealthCheckName}");
+
+ // These are separate so they can have different log levels
+ private static readonly string HealthCheckEndText = "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'";
+
+ private static readonly Action _healthCheckEndHealthy = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckEnd,
+ HealthCheckEndText);
+
+ private static readonly Action _healthCheckEndDegraded = LoggerMessage.Define(
+ LogLevel.Warning,
+ EventIds.HealthCheckEnd,
+ HealthCheckEndText);
+
+ private static readonly Action _healthCheckEndUnhealthy = LoggerMessage.Define(
+ LogLevel.Error,
+ EventIds.HealthCheckEnd,
+ HealthCheckEndText);
+
+ private static readonly Action _healthCheckEndFailed = LoggerMessage.Define(
+ LogLevel.Error,
+ EventIds.HealthCheckEnd,
+ HealthCheckEndText);
+
+ private static readonly Action _healthCheckError = LoggerMessage.Define(
+ LogLevel.Error,
+ EventIds.HealthCheckError,
+ "Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms");
+
+ public static void HealthCheckProcessingBegin(ILogger logger)
+ {
+ _healthCheckProcessingBegin(logger, null);
+ }
+
+ public static void HealthCheckProcessingEnd(ILogger logger, HealthStatus status, TimeSpan duration)
+ {
+ _healthCheckProcessingEnd(logger, duration.TotalMilliseconds, status, null);
+ }
+
+ 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)
+ {
+ switch (entry.Status)
+ {
+ case HealthStatus.Healthy:
+ _healthCheckEndHealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
+ break;
+
+ case HealthStatus.Degraded:
+ _healthCheckEndDegraded(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
+ break;
+
+ case HealthStatus.Unhealthy:
+ _healthCheckEndUnhealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
+ break;
+ }
+ }
+
+ public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration)
+ {
+ _healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception);
+ }
+
+ public static void HealthCheckData(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry)
+ {
+ if (entry.Data.Count > 0 && logger.IsEnabled(LogLevel.Debug))
+ {
+ logger.Log(
+ LogLevel.Debug,
+ EventIds.HealthCheckData,
+ new HealthCheckDataLogValue(registration.Name, entry.Data),
+ null,
+ (state, ex) => state.ToString());
+ }
+ }
+ }
+
+ internal class HealthCheckDataLogValue : IReadOnlyList>
+ {
+ private readonly string _name;
+ private readonly List> _values;
+
+ private string _formatted;
+
+ public HealthCheckDataLogValue(string name, IReadOnlyDictionary values)
+ {
+ _name = name;
+ _values = values.ToList();
+
+ // We add the name as a kvp so that you can filter by health check name in the logs.
+ // This is the same parameter name used in the other logs.
+ _values.Add(new KeyValuePair("HealthCheckName", name));
+ }
+
+ public KeyValuePair this[int index]
+ {
+ get
+ {
+ if (index < 0 || index >= Count)
+ {
+ throw new IndexOutOfRangeException(nameof(index));
+ }
+
+ return _values[index];
+ }
+ }
+
+ public int Count => _values.Count;
+
+ public IEnumerator> GetEnumerator()
+ {
+ return _values.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _values.GetEnumerator();
+ }
+
+ public override string ToString()
+ {
+ if (_formatted == null)
+ {
+ var builder = new StringBuilder();
+ builder.AppendLine($"Health check data for {_name}:");
+
+ var values = _values;
+ for (var i = 0; i < values.Count; i++)
+ {
+ var kvp = values[i];
+ builder.Append(" ");
+ builder.Append(kvp.Key);
+ builder.Append(": ");
+
+ builder.AppendLine(kvp.Value?.ToString());
+ }
+
+ _formatted = builder.ToString();
+ }
+
+ return _formatted;
+ }
+ }
+ }
+}
diff --git a/src/HealthChecks/HealthChecks/src/DelegateHealthCheck.cs b/src/HealthChecks/HealthChecks/src/DelegateHealthCheck.cs
new file mode 100644
index 0000000000..94069fd7d1
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/DelegateHealthCheck.cs
@@ -0,0 +1,35 @@
+// 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 simple implementation of which uses a provided delegate to
+ /// implement the check.
+ ///
+ internal sealed class DelegateHealthCheck : IHealthCheck
+ {
+ private readonly Func> _check;
+
+ ///
+ /// Create an instance of from the specified delegate.
+ ///
+ /// A delegate which provides the code to execute when the health check is run.
+ public DelegateHealthCheck(Func> check)
+ {
+ _check = check ?? throw new ArgumentNullException(nameof(check));
+ }
+
+ ///
+ /// 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(HealthCheckContext context, CancellationToken cancellationToken = default) => _check(cancellationToken);
+ }
+}
diff --git a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthCheckServiceCollectionExtensions.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthCheckServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..d6df03d2ae
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthCheckServiceCollectionExtensions.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 Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// Provides extension methods for registering in an .
+ ///
+ public static class HealthCheckServiceCollectionExtensions
+ {
+ ///
+ /// 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
+ /// multiple times in order to get access to the in multiple places.
+ ///
+ /// The to add the to.
+ /// An instance of from which health checks can be registered.
+ public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
+ {
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ return new HealthChecksBuilder(services);
+ }
+ }
+}
diff --git a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilder.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilder.cs
new file mode 100644
index 0000000000..231dd51717
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/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/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs
new file mode 100644
index 0000000000..9508889054
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/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/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs
new file mode 100644
index 0000000000..d7dfdd90ae
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.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.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.
+ /// 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,
+ 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: null, tags));
+ }
+
+ ///
+ /// Adds a new health check with the specified name and implementation.
+ ///
+ /// The .
+ /// The name of the health check.
+ /// 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,
+ 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: null, tags));
+ }
+
+ ///
+ /// Adds a new health check with the specified name and implementation.
+ ///
+ /// The .
+ /// The name of the health check.
+ /// 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,
+ 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: null, tags));
+ }
+
+ ///
+ /// Adds a new health check with the specified name and implementation.
+ ///
+ /// The .
+ /// The name of the health check.
+ /// 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,
+ 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: null, tags));
+ }
+ }
+}
diff --git a/src/HealthChecks/HealthChecks/src/DependencyInjection/IHealthChecksBuilder.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/IHealthChecksBuilder.cs
new file mode 100644
index 0000000000..eb78293f87
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/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/HealthChecks/HealthChecks/src/HealthCheckLogScope.cs b/src/HealthChecks/HealthChecks/src/HealthCheckLogScope.cs
new file mode 100644
index 0000000000..c7ef3ff5bd
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/HealthCheckLogScope.cs
@@ -0,0 +1,48 @@
+// 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;
+using System.Collections.Generic;
+
+namespace Microsoft.Extensions.Diagnostics.HealthChecks
+{
+ internal class HealthCheckLogScope : IReadOnlyList>
+ {
+ public string HealthCheckName { get; }
+
+ int IReadOnlyCollection>.Count { get; } = 1;
+
+ KeyValuePair IReadOnlyList>.this[int index]
+ {
+ get
+ {
+ if (index == 0)
+ {
+ return new KeyValuePair(nameof(HealthCheckName), HealthCheckName);
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ }
+
+ ///
+ /// Creates a new instance of with the provided name.
+ ///
+ /// The name of the health check being executed.
+ public HealthCheckLogScope(string healthCheckName)
+ {
+ HealthCheckName = healthCheckName;
+ }
+
+ IEnumerator> IEnumerable>.GetEnumerator()
+ {
+ yield return new KeyValuePair(nameof(HealthCheckName), HealthCheckName);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable>)this).GetEnumerator();
+ }
+ }
+}
diff --git a/src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs b/src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs
new file mode 100644
index 0000000000..d124ffa2e3
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs
@@ -0,0 +1,262 @@
+// 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.Hosting;
+using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Diagnostics.HealthChecks
+{
+ internal sealed class HealthCheckPublisherHostedService : IHostedService
+ {
+ private readonly HealthCheckService _healthCheckService;
+ private readonly IOptions _options;
+ private readonly ILogger _logger;
+ private readonly IHealthCheckPublisher[] _publishers;
+
+ private CancellationTokenSource _stopping;
+ private Timer _timer;
+
+ public HealthCheckPublisherHostedService(
+ HealthCheckService healthCheckService,
+ IOptions options,
+ ILogger logger,
+ IEnumerable publishers)
+ {
+ if (healthCheckService == null)
+ {
+ throw new ArgumentNullException(nameof(healthCheckService));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+
+ if (publishers == null)
+ {
+ throw new ArgumentNullException(nameof(publishers));
+ }
+
+ _healthCheckService = healthCheckService;
+ _options = options;
+ _logger = logger;
+ _publishers = publishers.ToArray();
+
+ _stopping = new CancellationTokenSource();
+ }
+
+ internal bool IsStopping => _stopping.IsCancellationRequested;
+
+ internal bool IsTimerRunning => _timer != null;
+
+ public Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ if (_publishers.Length == 0)
+ {
+ return Task.CompletedTask;
+ }
+
+ // IMPORTANT - make sure this is the last thing that happens in this method. The timer can
+ // fire before other code runs.
+ _timer = NonCapturingTimer.Create(Timer_Tick, null, dueTime: _options.Value.Delay, period: _options.Value.Period);
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ _stopping.Cancel();
+ }
+ catch
+ {
+ // Ignore exceptions thrown as a result of a cancellation.
+ }
+
+ if (_publishers.Length == 0)
+ {
+ return Task.CompletedTask;
+ }
+
+ _timer?.Dispose();
+ _timer = null;
+
+
+ return Task.CompletedTask;
+ }
+
+ // Yes, async void. We need to be async. We need to be void. We handle the exceptions in RunAsync
+ private async void Timer_Tick(object state)
+ {
+ await RunAsync();
+ }
+
+ // Internal for testing
+ internal async Task RunAsync()
+ {
+ var duration = ValueStopwatch.StartNew();
+ Logger.HealthCheckPublisherProcessingBegin(_logger);
+
+ CancellationTokenSource cancellation = null;
+ try
+ {
+ var timeout = _options.Value.Timeout;
+
+ cancellation = CancellationTokenSource.CreateLinkedTokenSource(_stopping.Token);
+ cancellation.CancelAfter(timeout);
+
+ await RunAsyncCore(cancellation.Token);
+
+ Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime());
+ }
+ catch (OperationCanceledException) when (IsStopping)
+ {
+ // This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
+ // a timeout and we want to log it.
+ }
+ catch (Exception ex)
+ {
+ // This is an error, publishing failed.
+ Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime(), ex);
+ }
+ finally
+ {
+ cancellation.Dispose();
+ }
+ }
+
+ private async Task RunAsyncCore(CancellationToken cancellationToken)
+ {
+ // Forcibly yield - we want to unblock the timer thread.
+ await Task.Yield();
+
+ // The health checks service does it's own logging, and doesn't throw exceptions.
+ var report = await _healthCheckService.CheckHealthAsync(_options.Value.Predicate, cancellationToken);
+
+ var publishers = _publishers;
+ var tasks = new Task[publishers.Length];
+ for (var i = 0; i < publishers.Length; i++)
+ {
+ tasks[i] = RunPublisherAsync(publishers[i], report, cancellationToken);
+ }
+
+ await Task.WhenAll(tasks);
+ }
+
+ private async Task RunPublisherAsync(IHealthCheckPublisher publisher, HealthReport report, CancellationToken cancellationToken)
+ {
+ var duration = ValueStopwatch.StartNew();
+
+ try
+ {
+ Logger.HealthCheckPublisherBegin(_logger, publisher);
+
+ await publisher.PublishAsync(report, cancellationToken);
+ Logger.HealthCheckPublisherEnd(_logger, publisher, duration.GetElapsedTime());
+ }
+ catch (OperationCanceledException) when (IsStopping)
+ {
+ // This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
+ // a timeout and we want to log it.
+ }
+ catch (OperationCanceledException ocex)
+ {
+ Logger.HealthCheckPublisherTimeout(_logger, publisher, duration.GetElapsedTime());
+ throw ocex;
+ }
+ catch (Exception ex)
+ {
+ Logger.HealthCheckPublisherError(_logger, publisher, duration.GetElapsedTime(), ex);
+ throw ex;
+ }
+ }
+
+ internal static class EventIds
+ {
+ public static readonly EventId HealthCheckPublisherProcessingBegin = new EventId(100, "HealthCheckPublisherProcessingBegin");
+ public static readonly EventId HealthCheckPublisherProcessingEnd = new EventId(101, "HealthCheckPublisherProcessingEnd");
+ public static readonly EventId HealthCheckPublisherProcessingError = new EventId(101, "HealthCheckPublisherProcessingError");
+
+ public static readonly EventId HealthCheckPublisherBegin = new EventId(102, "HealthCheckPublisherBegin");
+ public static readonly EventId HealthCheckPublisherEnd = new EventId(103, "HealthCheckPublisherEnd");
+ public static readonly EventId HealthCheckPublisherError = new EventId(104, "HealthCheckPublisherError");
+ public static readonly EventId HealthCheckPublisherTimeout = new EventId(104, "HealthCheckPublisherTimeout");
+ }
+
+ private static class Logger
+ {
+ private static readonly Action _healthCheckPublisherProcessingBegin = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckPublisherProcessingBegin,
+ "Running health check publishers");
+
+ private static readonly Action _healthCheckPublisherProcessingEnd = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckPublisherProcessingEnd,
+ "Health check publisher processing completed after {ElapsedMilliseconds}ms");
+
+ private static readonly Action _healthCheckPublisherBegin = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckPublisherBegin,
+ "Running health check publisher '{HealthCheckPublisher}'");
+
+ private static readonly Action _healthCheckPublisherEnd = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckPublisherEnd,
+ "Health check '{HealthCheckPublisher}' completed after {ElapsedMilliseconds}ms");
+
+ private static readonly Action _healthCheckPublisherError = LoggerMessage.Define(
+ LogLevel.Error,
+ EventIds.HealthCheckPublisherError,
+ "Health check {HealthCheckPublisher} threw an unhandled exception after {ElapsedMilliseconds}ms");
+
+ private static readonly Action _healthCheckPublisherTimeout = LoggerMessage.Define(
+ LogLevel.Error,
+ EventIds.HealthCheckPublisherTimeout,
+ "Health check {HealthCheckPublisher} was canceled after {ElapsedMilliseconds}ms");
+
+ public static void HealthCheckPublisherProcessingBegin(ILogger logger)
+ {
+ _healthCheckPublisherProcessingBegin(logger, null);
+ }
+
+ public static void HealthCheckPublisherProcessingEnd(ILogger logger, TimeSpan duration, Exception exception = null)
+ {
+ _healthCheckPublisherProcessingEnd(logger, duration.TotalMilliseconds, exception);
+ }
+
+ public static void HealthCheckPublisherBegin(ILogger logger, IHealthCheckPublisher publisher)
+ {
+ _healthCheckPublisherBegin(logger, publisher, null);
+ }
+
+ public static void HealthCheckPublisherEnd(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
+ {
+ _healthCheckPublisherEnd(logger, publisher, duration.TotalMilliseconds, null);
+ }
+
+ public static void HealthCheckPublisherError(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration, Exception exception)
+ {
+ _healthCheckPublisherError(logger, publisher, duration.TotalMilliseconds, exception);
+ }
+
+ public static void HealthCheckPublisherTimeout(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
+ {
+ _healthCheckPublisherTimeout(logger, publisher, duration.TotalMilliseconds, null);
+ }
+ }
+ }
+}
diff --git a/src/HealthChecks/HealthChecks/src/HealthCheckPublisherOptions.cs b/src/HealthChecks/HealthChecks/src/HealthCheckPublisherOptions.cs
new file mode 100644
index 0000000000..1313718af8
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/HealthCheckPublisherOptions.cs
@@ -0,0 +1,84 @@
+// 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;
+
+namespace Microsoft.Extensions.Diagnostics.HealthChecks
+{
+ ///
+ /// Options for the default service that executes instances.
+ ///
+ public sealed class HealthCheckPublisherOptions
+ {
+ private TimeSpan _delay;
+ private TimeSpan _period;
+
+ public HealthCheckPublisherOptions()
+ {
+ _delay = TimeSpan.FromSeconds(5);
+ _period = TimeSpan.FromSeconds(30);
+ }
+
+ ///
+ /// Gets or sets the initial delay applied after the application starts before executing
+ /// instances. The delay is applied once at startup, and does
+ /// not apply to subsequent iterations. The default value is 5 seconds.
+ ///
+ public TimeSpan Delay
+ {
+ get => _delay;
+ set
+ {
+ if (value == System.Threading.Timeout.InfiniteTimeSpan)
+ {
+ throw new ArgumentException($"The {nameof(Delay)} must not be infinite.", nameof(value));
+ }
+
+ _delay = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the period of execution. The default value is
+ /// 30 seconds.
+ ///
+ ///
+ /// The cannot be set to a value lower than 1 second.
+ ///
+ public TimeSpan Period
+ {
+ get => _period;
+ set
+ {
+ if (value < TimeSpan.FromSeconds(1))
+ {
+ throw new ArgumentException($"The {nameof(Period)} must be greater than or equal to one second.", nameof(value));
+ }
+
+ if (value == System.Threading.Timeout.InfiniteTimeSpan)
+ {
+ throw new ArgumentException($"The {nameof(Period)} must not be infinite.", nameof(value));
+ }
+
+ _delay = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a predicate that is used to filter the set of health checks executed.
+ ///
+ ///
+ /// If is null, the health check publisher service will run all
+ /// registered health checks - this is the default behavior. To run a subset of health checks,
+ /// provide a function that filters the set of checks. The predicate will be evaluated each period.
+ ///
+ public Func Predicate { get; set; }
+
+ ///
+ /// Gets or sets the timeout for executing the health checks an all
+ /// instances. Use to execute with no timeout.
+ /// The default value is 30 seconds.
+ ///
+ public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
+ }
+}
diff --git a/src/HealthChecks/HealthChecks/src/HealthCheckService.cs b/src/HealthChecks/HealthChecks/src/HealthCheckService.cs
new file mode 100644
index 0000000000..e4a128148d
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/HealthCheckService.cs
@@ -0,0 +1,61 @@
+// 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.DependencyInjection;
+
+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.
+ ///
+ ///
+ /// implementations can be registered through extension methods provided by
+ /// .
+ ///
+ ///
+ public abstract class HealthCheckService
+ {
+ ///
+ /// 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)
+ {
+ return CheckHealthAsync(predicate: null, cancellationToken);
+ }
+
+ ///
+ /// 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/HealthChecks/HealthChecks/src/HealthCheckServiceOptions.cs b/src/HealthChecks/HealthChecks/src/HealthCheckServiceOptions.cs
new file mode 100644
index 0000000000..b8dfdb9b40
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/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/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
new file mode 100644
index 0000000000..d0b1c97ef0
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
@@ -0,0 +1,26 @@
+
+
+ Components for performing health checks in .NET applications
+
+Commonly Used Types:
+Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService
+Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
+
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ diagnostics;healthchecks
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/HealthChecks/HealthChecks/src/Properties/AssemblyInfo.cs b/src/HealthChecks/HealthChecks/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..13e969bfad
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
\ No newline at end of file
diff --git a/src/HealthChecks/HealthChecks/src/baseline.netcore.json b/src/HealthChecks/HealthChecks/src/baseline.netcore.json
new file mode 100644
index 0000000000..cb2fe053f1
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/src/baseline.netcore.json
@@ -0,0 +1,5 @@
+{
+ "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ ]
+}
\ No newline at end of file
diff --git a/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs
new file mode 100644
index 0000000000..9ab991204e
--- /dev/null
+++ b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs
@@ -0,0 +1,419 @@
+// 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.Logging;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Extensions.Diagnostics.HealthChecks
+{
+ public class DefaultHealthCheckServiceTest
+ {
+ [Fact]
+ public void Constructor_ThrowsUsefulExceptionForDuplicateNames()
+ {
+ // Arrange
+ //
+ // Doing this the old fashioned way so we can verify that the exception comes
+ // from the constructor.
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging();
+ serviceCollection.AddOptions();
+ serviceCollection.AddHealthChecks()
+ .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
+ .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
+ .AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
+ .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
+ .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())));
+
+ var services = serviceCollection.BuildServiceProvider();
+
+ var scopeFactory = services.GetRequiredService();
+ var options = services.GetRequiredService>();
+ var logger = services.GetRequiredService>();
+
+ // Act
+ var exception = Assert.Throws(() => new DefaultHealthCheckService(scopeFactory, options, logger));
+
+ // Assert
+ Assert.StartsWith($"Duplicate health checks were registered with the name(s): Foo, Baz", exception.Message);
+ }
+
+ [Fact]
+ public async Task CheckAsync_RunsAllChecksAndAggregatesResultsAsync()
+ {
+ const string DataKey = "Foo";
+ const string DataValue = "Bar";
+ const string DegradedMessage = "I'm not feeling so good";
+ const string UnhealthyMessage = "Halp!";
+ const string HealthyMessage = "Everything is A-OK";
+ var exception = new Exception("Things are pretty bad!");
+
+ // Arrange
+ var data = new Dictionary()
+ {
+ { DataKey, DataValue }
+ };
+
+ var service = CreateHealthChecksService(b =>
+ {
+ b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
+ b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
+ b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
+ });
+
+ // Act
+ var results = await service.CheckHealthAsync();
+
+ // Assert
+ Assert.Collection(
+ results.Entries.OrderBy(kvp => kvp.Key),
+ actual =>
+ {
+ Assert.Equal("DegradedCheck", actual.Key);
+ Assert.Equal(DegradedMessage, actual.Value.Description);
+ Assert.Equal(HealthStatus.Degraded, actual.Value.Status);
+ Assert.Null(actual.Value.Exception);
+ Assert.Empty(actual.Value.Data);
+ },
+ actual =>
+ {
+ Assert.Equal("HealthyCheck", actual.Key);
+ Assert.Equal(HealthyMessage, actual.Value.Description);
+ Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
+ Assert.Null(actual.Value.Exception);
+ Assert.Collection(actual.Value.Data, item =>
+ {
+ Assert.Equal(DataKey, item.Key);
+ Assert.Equal(DataValue, item.Value);
+ });
+ },
+ actual =>
+ {
+ Assert.Equal("UnhealthyCheck", actual.Key);
+ Assert.Equal(UnhealthyMessage, actual.Value.Description);
+ Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
+ Assert.Same(exception, actual.Value.Exception);
+ Assert.Empty(actual.Value.Data);
+ });
+ }
+
+ [Fact]
+ public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync()
+ {
+ const string DataKey = "Foo";
+ const string DataValue = "Bar";
+ const string DegradedMessage = "I'm not feeling so good";
+ const string UnhealthyMessage = "Halp!";
+ const string HealthyMessage = "Everything is A-OK";
+ var exception = new Exception("Things are pretty bad!");
+
+ // Arrange
+ var data = new Dictionary
+ {
+ { DataKey, DataValue }
+ };
+
+ var service = CreateHealthChecksService(b =>
+ {
+ b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
+ b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
+ b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
+ });
+
+ // Act
+ var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck");
+
+ // Assert
+ Assert.Collection(results.Entries,
+ actual =>
+ {
+ Assert.Equal("HealthyCheck", actual.Key);
+ Assert.Equal(HealthyMessage, actual.Value.Description);
+ Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
+ Assert.Null(actual.Value.Exception);
+ Assert.Collection(actual.Value.Data, item =>
+ {
+ Assert.Equal(DataKey, item.Key);
+ Assert.Equal(DataValue, item.Value);
+ });
+ });
+ }
+
+ [Fact]
+ public async Task CheckHealthAsync_SetsRegistrationForEachCheck()
+ {
+ // Arrange
+ var thrownException = new InvalidOperationException("Whoops!");
+ var faultedException = new InvalidOperationException("Ohnoes!");
+
+ var service = CreateHealthChecksService(b =>
+ {
+ b.AddCheck("A");
+ b.AddCheck("B");
+ b.AddCheck("C");
+ });
+
+ // Act
+ var results = await service.CheckHealthAsync();
+
+ // Assert
+ 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_Cancellation_CanPropagate()
+ {
+ // Arrange
+ var insideCheck = new TaskCompletionSource