Merge branch 'release/2.2'

\n\nCommit migrated from 39e7539b49
This commit is contained in:
Nate McMaster 2018-11-21 13:19:48 -08:00
commit fcdba6256a
33 changed files with 3077 additions and 0 deletions

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<IsProductComponent>true</IsProductComponent>
</PropertyGroup>
</Project>

View File

@ -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
{
/// <summary>
/// Gets or sets the <see cref="HealthCheckRegistration"/> of the currently executing <see cref="IHealthCheck"/>.
/// </summary>
public HealthCheckRegistration Registration { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Represent the registration information associated with an <see cref="IHealthCheck"/> implementation.
/// </summary>
/// <remarks>
/// <para>
/// The health check registration is provided as a separate object so that application developers can customize
/// how health check implementations are configured.
/// </para>
/// <para>
/// The registration is provided to an <see cref="IHealthCheck"/> implementation during execution through
/// <see cref="HealthCheckContext.Registration"/>. This allows a health check implementation to access named
/// options or perform other operations based on the registered name.
/// </para>
/// </remarks>
public sealed class HealthCheckRegistration
{
private Func<IServiceProvider, IHealthCheck> _factory;
private string _name;
/// <summary>
/// Creates a new <see cref="HealthCheckRegistration"/> for an existing <see cref="IHealthCheck"/> instance.
/// </summary>
/// <param name="name">The health check name.</param>
/// <param name="instance">The <see cref="IHealthCheck"/> instance.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported upon failure of the health check. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used for filtering health checks.</param>
public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable<string> 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<string>(tags ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
Factory = (_) => instance;
}
/// <summary>
/// Creates a new <see cref="HealthCheckRegistration"/> for an existing <see cref="IHealthCheck"/> instance.
/// </summary>
/// <param name="name">The health check name.</param>
/// <param name="factory">A delegate used to create the <see cref="IHealthCheck"/> instance.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used for filtering health checks.</param>
public HealthCheckRegistration(
string name,
Func<IServiceProvider, IHealthCheck> factory,
HealthStatus? failureStatus,
IEnumerable<string> 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<string>(tags ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
Factory = factory;
}
/// <summary>
/// Gets or sets a delegate used to create the <see cref="IHealthCheck"/> instance.
/// </summary>
public Func<IServiceProvider, IHealthCheck> Factory
{
get => _factory;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_factory = value;
}
}
/// <summary>
/// Gets or sets the <see cref="HealthStatus"/> that should be reported upon failure of the health check.
/// </summary>
public HealthStatus FailureStatus { get; set; }
/// <summary>
/// Gets or sets the health check name.
/// </summary>
public string Name
{
get => _name;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_name = value;
}
}
/// <summary>
/// Gets a list of tags that can be used for filtering health checks.
/// </summary>
public ISet<string> Tags { get; }
}
}

View File

@ -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
{
/// <summary>
/// Represents the result of a health check.
/// </summary>
public struct HealthCheckResult
{
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
/// <summary>
/// Creates a new <see cref="HealthCheckResult"/> with the specified values for <paramref name="status"/>,
/// <paramref name="exception"/>, <paramref name="description"/>, and <paramref name="data"/>.
/// </summary>
/// <param name="status">A value indicating the status of the component that was checked.</param>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
public HealthCheckResult(HealthStatus status, string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
{
Status = status;
Description = description;
Exception = exception;
Data = data ?? _emptyReadOnlyDictionary;
}
/// <summary>
/// Gets additional key-value pairs describing the health of the component.
/// </summary>
public IReadOnlyDictionary<string, object> Data { get; }
/// <summary>
/// Gets a human-readable description of the status of the component that was checked.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets an <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).
/// </summary>
public Exception Exception { get; }
/// <summary>
/// Gets a value indicating the status of the component that was checked.
/// </summary>
public HealthStatus Status { get; }
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
/// </summary>
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
public static HealthCheckResult Healthy(string description = null, IReadOnlyDictionary<string, object> data = null)
{
return new HealthCheckResult(status: HealthStatus.Healthy, description, exception: null, data);
}
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a degraded component.
/// </summary>
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status. Optional.</param>
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
/// <returns>A <see cref="HealthCheckResult"/> representing a degraged component.</returns>
public static HealthCheckResult Degraded(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
{
return new HealthCheckResult(status: HealthStatus.Degraded, description, exception: null, data);
}
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
/// </summary>
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status. Optional.</param>
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
public static HealthCheckResult Unhealthy(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
{
return new HealthCheckResult(status: HealthStatus.Unhealthy, description, exception, data);
}
}
}

View File

@ -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
{
/// <summary>
/// Represents the result of executing a group of <see cref="IHealthCheck"/> instances.
/// </summary>
public sealed class HealthReport
{
/// <summary>
/// Create a new <see cref="HealthReport"/> from the specified results.
/// </summary>
/// <param name="entries">A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.</param>
/// <param name="totalDuration">A value indicating the time the health check service took to execute.</param>
public HealthReport(IReadOnlyDictionary<string, HealthReportEntry> entries, TimeSpan totalDuration)
{
Entries = entries;
Status = CalculateAggregateStatus(entries.Values);
TotalDuration = totalDuration;
}
/// <summary>
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.
/// </summary>
/// <remarks>
/// The keys in this dictionary map the name of each executed health check to a <see cref="HealthReportEntry"/> for the
/// result data retruned from the corresponding health check.
/// </remarks>
public IReadOnlyDictionary<string, HealthReportEntry> Entries { get; }
/// <summary>
/// Gets a <see cref="HealthStatus"/> representing the aggregate status of all the health checks. The value of <see cref="Status"/>
/// will be the most servere status reported by a health check. If no checks were executed, the value is always <see cref="HealthStatus.Healthy"/>.
/// </summary>
public HealthStatus Status { get; }
/// <summary>
/// Gets the time the health check service took to execute.
/// </summary>
public TimeSpan TotalDuration { get; }
private HealthStatus CalculateAggregateStatus(IEnumerable<HealthReportEntry> 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;
}
}
}

View File

@ -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
{
/// <summary>
/// Represents an entry in a <see cref="HealthReport"/>. Corresponds to the result of a single <see cref="IHealthCheck"/>.
/// </summary>
public struct HealthReportEntry
{
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
/// <summary>
/// Creates a new <see cref="HealthReportEntry"/> with the specified values for <paramref name="status"/>, <paramref name="exception"/>,
/// <paramref name="description"/>, and <paramref name="data"/>.
/// </summary>
/// <param name="status">A value indicating the health status of the component that was checked.</param>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="duration">A value indicating the health execution duration.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
public HealthReportEntry(HealthStatus status, string description, TimeSpan duration, Exception exception, IReadOnlyDictionary<string, object> data)
{
Status = status;
Description = description;
Duration = duration;
Exception = exception;
Data = data ?? _emptyReadOnlyDictionary;
}
/// <summary>
/// Gets additional key-value pairs describing the health of the component.
/// </summary>
public IReadOnlyDictionary<string, object> Data { get; }
/// <summary>
/// Gets a human-readable description of the status of the component that was checked.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the health check execution duration.
/// </summary>
public TimeSpan Duration { get; }
/// <summary>
/// Gets an <see cref="System.Exception"/> representing the exception that was thrown when checking for status (if any).
/// </summary>
public Exception Exception { get; }
/// <summary>
/// Gets the health status of the component that was checked.
/// </summary>
public HealthStatus Status { get; }
}
}

View File

@ -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
{
/// <summary>
/// Represents the reported status of a health check result.
/// </summary>
/// <remarks>
/// <para>
/// A status of <see cref="Unhealthy"/> 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.
/// </para>
/// <para>
/// The values of this enum or ordered from least healthy to most healthy. So <see cref="HealthStatus.Degraded"/> is
/// greater than <see cref="HealthStatus.Unhealthy"/> but less than <see cref="HealthStatus.Healthy"/>.
/// </para>
/// </remarks>
public enum HealthStatus
{
/// <summary>
/// Indicates that the health check determined that the component was unhealthy, or an unhandled
/// exception was thrown while executing the health check.
/// </summary>
Unhealthy = 0,
/// <summary>
/// Indicates that the health check determined that the component was in a degraded state.
/// </summary>
Degraded = 1,
/// <summary>
/// Indicates that the health check determined that the component was healthy.
/// </summary>
Healthy = 2,
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
public interface IHealthCheck
{
/// <summary>
/// Runs the health check, returning the status of the component being checked.
/// </summary>
/// <param name="context">A context object associated with the current execution.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default);
}
}

View File

@ -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
{
/// <summary>
/// Represents a publisher of <see cref="HealthReport"/> information.
/// </summary>
/// <remarks>
/// <para>
/// The default health checks implementation provided an <c>IHostedService</c> implementation that can
/// be used to execute health checks at regular intervals and provide the resulting <see cref="HealthReport"/>
/// data to all registered <see cref="IHealthCheckPublisher"/> instances.
/// </para>
/// <para>
/// To provide an <see cref="IHealthCheckPublisher"/> implementation, register an instance or type as a singleton
/// service in the dependency injection container.
/// </para>
/// <para>
/// <see cref="IHealthCheckPublisher"/> instances are provided with a <see cref="HealthReport"/> after executing
/// health checks in a background thread. The use of <see cref="IHealthCheckPublisher"/> depend on hosting in
/// an application using <c>IWebHost</c> or generic host (<c>IHost</c>). Execution of <see cref="IHealthCheckPublisher"/>
/// instance is not related to execution of health checks via a middleware.
/// </para>
/// </remarks>
public interface IHealthCheckPublisher
{
/// <summary>
/// Publishes the provided <paramref name="report"/>.
/// </summary>
/// <param name="report">The <see cref="HealthReport"/>. The result of executing a set of health checks.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns>A <see cref="Task"/> which will complete when publishing is complete.</returns>
Task PublishAsync(HealthReport report, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Abstractions for defining health checks in .NET applications
Commonly Used Types
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck
</Description>
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>diagnostics;healthchecks</PackageTags>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,5 @@
{
"AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
"Types": [
]
}

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<IsProductComponent>true</IsProductComponent>
</PropertyGroup>
</Project>

View File

@ -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<HealthCheckServiceOptions> _options;
private readonly ILogger<DefaultHealthCheckService> _logger;
public DefaultHealthCheckService(
IServiceScopeFactory scopeFactory,
IOptions<HealthCheckServiceOptions> options,
ILogger<DefaultHealthCheckService> 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<HealthReport> CheckHealthAsync(
Func<HealthCheckRegistration, bool> predicate,
CancellationToken cancellationToken = default)
{
var registrations = _options.Value.Registrations;
using (var scope = _scopeFactory.CreateScope())
{
var context = new HealthCheckContext();
var entries = new Dictionary<string, HealthReportEntry>(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<HealthCheckRegistration> 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<ILogger, Exception> _healthCheckProcessingBegin = LoggerMessage.Define(
LogLevel.Debug,
EventIds.HealthCheckProcessingBegin,
"Running health checks");
private static readonly Action<ILogger, double, HealthStatus, Exception> _healthCheckProcessingEnd = LoggerMessage.Define<double, HealthStatus>(
LogLevel.Debug,
EventIds.HealthCheckProcessingEnd,
"Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}");
private static readonly Action<ILogger, string, Exception> _healthCheckBegin = LoggerMessage.Define<string>(
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<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndHealthy = LoggerMessage.Define<string, double, HealthStatus, string>(
LogLevel.Debug,
EventIds.HealthCheckEnd,
HealthCheckEndText);
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndDegraded = LoggerMessage.Define<string, double, HealthStatus, string>(
LogLevel.Warning,
EventIds.HealthCheckEnd,
HealthCheckEndText);
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndUnhealthy = LoggerMessage.Define<string, double, HealthStatus, string>(
LogLevel.Error,
EventIds.HealthCheckEnd,
HealthCheckEndText);
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndFailed = LoggerMessage.Define<string, double, HealthStatus, string>(
LogLevel.Error,
EventIds.HealthCheckEnd,
HealthCheckEndText);
private static readonly Action<ILogger, string, double, Exception> _healthCheckError = LoggerMessage.Define<string, double>(
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<KeyValuePair<string, object>>
{
private readonly string _name;
private readonly List<KeyValuePair<string, object>> _values;
private string _formatted;
public HealthCheckDataLogValue(string name, IReadOnlyDictionary<string, object> 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<string, object>("HealthCheckName", name));
}
public KeyValuePair<string, object> this[int index]
{
get
{
if (index < 0 || index >= Count)
{
throw new IndexOutOfRangeException(nameof(index));
}
return _values[index];
}
}
public int Count => _values.Count;
public IEnumerator<KeyValuePair<string, object>> 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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// A simple implementation of <see cref="IHealthCheck"/> which uses a provided delegate to
/// implement the check.
/// </summary>
internal sealed class DelegateHealthCheck : IHealthCheck
{
private readonly Func<CancellationToken, Task<HealthCheckResult>> _check;
/// <summary>
/// Create an instance of <see cref="DelegateHealthCheck"/> from the specified delegate.
/// </summary>
/// <param name="check">A delegate which provides the code to execute when the health check is run.</param>
public DelegateHealthCheck(Func<CancellationToken, Task<HealthCheckResult>> check)
{
_check = check ?? throw new ArgumentNullException(nameof(check));
}
/// <summary>
/// Runs the health check, returning the status of the component being checked.
/// </summary>
/// <param name="context">A context object associated with the current execution.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) => _check(cancellationToken);
}
}

View File

@ -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
{
/// <summary>
/// Provides extension methods for registering <see cref="HealthCheckService"/> in an <see cref="IServiceCollection"/>.
/// </summary>
public static class HealthCheckServiceCollectionExtensions
{
/// <summary>
/// Adds the <see cref="HealthCheckService"/> to the container, using the provided delegate to register
/// health checks.
/// </summary>
/// <remarks>
/// This operation is idempotent - multiple invocations will still only result in a single
/// <see cref="HealthCheckService"/> instance in the <see cref="IServiceCollection"/>. It can be invoked
/// multiple times in order to get access to the <see cref="IHealthChecksBuilder"/> in multiple places.
/// </remarks>
/// <param name="services">The <see cref="IServiceCollection"/> to add the <see cref="HealthCheckService"/> to.</param>
/// <returns>An instance of <see cref="IHealthChecksBuilder"/> from which health checks can be registered.</returns>
public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
{
services.TryAddSingleton<HealthCheckService, DefaultHealthCheckService>();
services.TryAddSingleton<IHostedService, HealthCheckPublisherHostedService>();
return new HealthChecksBuilder(services);
}
}
}

View File

@ -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<HealthCheckServiceOptions>(options =>
{
options.Registrations.Add(registration);
});
return this;
}
}
}

View File

@ -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
{
/// <summary>
/// Provides basic extension methods for registering <see cref="IHealthCheck"/> instances in an <see cref="IHealthChecksBuilder"/>.
/// </summary>
public static class HealthChecksBuilderAddCheckExtensions
{
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="instance">An <see cref="IHealthCheck"/> instance.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
public static IHealthChecksBuilder AddCheck(
this IHealthChecksBuilder builder,
string name,
IHealthCheck instance,
HealthStatus? failureStatus = null,
IEnumerable<string> 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));
}
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </summary>
/// <typeparam name="T">The health check implementation type.</typeparam>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// This method will use <see cref="ActivatorUtilities.GetServiceOrCreateInstance{T}(IServiceProvider)"/> to create the health check
/// instance when needed. If a service of type <typeparamref name="T"/> is registred in the dependency injection container
/// with any liftime it will be used. Otherwise an instance of type <typeparamref name="T"/> will be constructed with
/// access to services from the dependency injection container.
/// </remarks>
public static IHealthChecksBuilder AddCheck<T>(
this IHealthChecksBuilder builder,
string name,
HealthStatus? failureStatus = null,
IEnumerable<string> 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<T>(s), failureStatus, tags));
}
// NOTE: AddTypeActivatedCheck has overloads rather than default parameters values, because default parameter values don't
// play super well with params.
/// <summary>
/// Adds a new type activated health check with the specified name and implementation.
/// </summary>
/// <typeparam name="T">The health check implementation type.</typeparam>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="args">Additional arguments to provide to the constructor.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
/// </remarks>
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(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<T>(builder, name, failureStatus: null, tags: null);
}
/// <summary>
/// Adds a new type activated health check with the specified name and implementation.
/// </summary>
/// <typeparam name="T">The health check implementation type.</typeparam>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="args">Additional arguments to provide to the constructor.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
/// </remarks>
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(
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<T>(builder, name, failureStatus, tags: null);
}
/// <summary>
/// Adds a new type activated health check with the specified name and implementation.
/// </summary>
/// <typeparam name="T">The health check implementation type.</typeparam>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <param name="args">Additional arguments to provide to the constructor.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
/// </remarks>
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(
this IHealthChecksBuilder builder,
string name,
HealthStatus? failureStatus,
IEnumerable<string> 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<T>(s, args), failureStatus, tags));
}
}
}

View File

@ -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
{
/// <summary>
/// Provides extension methods for registering delegates with the <see cref="IHealthChecksBuilder"/>.
/// </summary>
public static class HealthChecksBuilderDelegateExtensions
{
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <param name="check">A delegate that provides the health check implementation.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
public static IHealthChecksBuilder AddCheck(
this IHealthChecksBuilder builder,
string name,
Func<HealthCheckResult> check,
IEnumerable<string> 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));
}
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <param name="check">A delegate that provides the health check implementation.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
public static IHealthChecksBuilder AddCheck(
this IHealthChecksBuilder builder,
string name,
Func<CancellationToken, HealthCheckResult> check,
IEnumerable<string> 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));
}
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <param name="check">A delegate that provides the health check implementation.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
public static IHealthChecksBuilder AddAsyncCheck(
this IHealthChecksBuilder builder,
string name,
Func<Task<HealthCheckResult>> check,
IEnumerable<string> 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));
}
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the health check.</param>
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
/// <param name="check">A delegate that provides the health check implementation.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
public static IHealthChecksBuilder AddAsyncCheck(
this IHealthChecksBuilder builder,
string name,
Func<CancellationToken, Task<HealthCheckResult>> check,
IEnumerable<string> 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));
}
}
}

View File

@ -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
{
/// <summary>
/// A builder used to register health checks.
/// </summary>
public interface IHealthChecksBuilder
{
/// <summary>
/// Adds a <see cref="HealthCheckRegistration"/> for a health check.
/// </summary>
/// <param name="registration">The <see cref="HealthCheckRegistration"/>.</param>
IHealthChecksBuilder Add(HealthCheckRegistration registration);
/// <summary>
/// Gets the <see cref="IServiceCollection"/> into which <see cref="IHealthCheck"/> instances should be registered.
/// </summary>
IServiceCollection Services { get; }
}
}

View File

@ -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<KeyValuePair<string, object>>
{
public string HealthCheckName { get; }
int IReadOnlyCollection<KeyValuePair<string, object>>.Count { get; } = 1;
KeyValuePair<string, object> IReadOnlyList<KeyValuePair<string, object>>.this[int index]
{
get
{
if (index == 0)
{
return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
}
throw new ArgumentOutOfRangeException(nameof(index));
}
}
/// <summary>
/// Creates a new instance of <see cref="HealthCheckLogScope"/> with the provided name.
/// </summary>
/// <param name="healthCheckName">The name of the health check being executed.</param>
public HealthCheckLogScope(string healthCheckName)
{
HealthCheckName = healthCheckName;
}
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
{
yield return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
}
}
}

View File

@ -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<HealthCheckPublisherOptions> _options;
private readonly ILogger _logger;
private readonly IHealthCheckPublisher[] _publishers;
private CancellationTokenSource _stopping;
private Timer _timer;
public HealthCheckPublisherHostedService(
HealthCheckService healthCheckService,
IOptions<HealthCheckPublisherOptions> options,
ILogger<HealthCheckPublisherHostedService> logger,
IEnumerable<IHealthCheckPublisher> 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<ILogger, Exception> _healthCheckPublisherProcessingBegin = LoggerMessage.Define(
LogLevel.Debug,
EventIds.HealthCheckPublisherProcessingBegin,
"Running health check publishers");
private static readonly Action<ILogger, double, Exception> _healthCheckPublisherProcessingEnd = LoggerMessage.Define<double>(
LogLevel.Debug,
EventIds.HealthCheckPublisherProcessingEnd,
"Health check publisher processing completed after {ElapsedMilliseconds}ms");
private static readonly Action<ILogger, IHealthCheckPublisher, Exception> _healthCheckPublisherBegin = LoggerMessage.Define<IHealthCheckPublisher>(
LogLevel.Debug,
EventIds.HealthCheckPublisherBegin,
"Running health check publisher '{HealthCheckPublisher}'");
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherEnd = LoggerMessage.Define<IHealthCheckPublisher, double>(
LogLevel.Debug,
EventIds.HealthCheckPublisherEnd,
"Health check '{HealthCheckPublisher}' completed after {ElapsedMilliseconds}ms");
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherError = LoggerMessage.Define<IHealthCheckPublisher, double>(
LogLevel.Error,
EventIds.HealthCheckPublisherError,
"Health check {HealthCheckPublisher} threw an unhandled exception after {ElapsedMilliseconds}ms");
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherTimeout = LoggerMessage.Define<IHealthCheckPublisher, double>(
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);
}
}
}
}

View File

@ -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
{
/// <summary>
/// Options for the default service that executes <see cref="IHealthCheckPublisher"/> instances.
/// </summary>
public sealed class HealthCheckPublisherOptions
{
private TimeSpan _delay;
private TimeSpan _period;
public HealthCheckPublisherOptions()
{
_delay = TimeSpan.FromSeconds(5);
_period = TimeSpan.FromSeconds(30);
}
/// <summary>
/// Gets or sets the initial delay applied after the application starts before executing
/// <see cref="IHealthCheckPublisher"/> instances. The delay is applied once at startup, and does
/// not apply to subsequent iterations. The default value is 5 seconds.
/// </summary>
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;
}
}
/// <summary>
/// Gets or sets the period of <see cref="IHealthCheckPublisher"/> execution. The default value is
/// 30 seconds.
/// </summary>
/// <remarks>
/// The <see cref="Period"/> cannot be set to a value lower than 1 second.
/// </remarks>
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;
}
}
/// <summary>
/// Gets or sets a predicate that is used to filter the set of health checks executed.
/// </summary>
/// <remarks>
/// If <see cref="Predicate"/> is <c>null</c>, 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.
/// </remarks>
public Func<HealthCheckRegistration, bool> Predicate { get; set; }
/// <summary>
/// Gets or sets the timeout for executing the health checks an all <see cref="IHealthCheckPublisher"/>
/// instances. Use <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> to execute with no timeout.
/// The default value is 30 seconds.
/// </summary>
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
}
}

View File

@ -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
{
/// <summary>
/// A service which can be used to check the status of <see cref="IHealthCheck"/> instances
/// registered in the application.
/// </summary>
/// <remarks>
/// <para>
/// The default implementation of <see cref="HealthCheckService"/> is registered in the dependency
/// injection container as a singleton service by calling
/// <see cref="HealthCheckServiceCollectionExtensions.AddHealthChecks(IServiceCollection)"/>.
/// </para>
/// <para>
/// The <see cref="IHealthChecksBuilder"/> returned by
/// <see cref="HealthCheckServiceCollectionExtensions.AddHealthChecks(IServiceCollection)"/>
/// provides a convenience API for registering health checks.
/// </para>
/// <para>
/// <see cref="IHealthCheck"/> implementations can be registered through extension methods provided by
/// <see cref="IHealthChecksBuilder"/>.
/// </para>
/// </remarks>
public abstract class HealthCheckService
{
/// <summary>
/// Runs all the health checks in the application and returns the aggregated status.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
/// <returns>
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
/// yielding a <see cref="HealthReport"/> containing the results.
/// </returns>
public Task<HealthReport> CheckHealthAsync(CancellationToken cancellationToken = default)
{
return CheckHealthAsync(predicate: null, cancellationToken);
}
/// <summary>
/// Runs the provided health checks and returns the aggregated status
/// </summary>
/// <param name="predicate">
/// A predicate that can be used to include health checks based on user-defined criteria.
/// </param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
/// <returns>
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
/// yielding a <see cref="HealthReport"/> containing the results.
/// </returns>
public abstract Task<HealthReport> CheckHealthAsync(
Func<HealthCheckRegistration, bool> predicate,
CancellationToken cancellationToken = default);
}
}

View File

@ -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
{
/// <summary>
/// Options for the default implementation of <see cref="HealthCheckService"/>
/// </summary>
public sealed class HealthCheckServiceOptions
{
/// <summary>
/// Gets the health check registrations.
/// </summary>
public ICollection<HealthCheckRegistration> Registrations { get; } = new List<HealthCheckRegistration>();
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Components for performing health checks in .NET applications
Commonly Used Types:
Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>diagnostics;healthchecks</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(SharedSourceRoot)NonCapturingTimer\**\*.cs" />
<Compile Include="$(SharedSourceRoot)ValueStopwatch\**\*.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
<Reference Include="Microsoft.Extensions.Hosting.Abstractions" />
<Reference Include="Microsoft.Extensions.Options" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,5 @@
{
"AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
"Types": [
]
}

View File

@ -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<IServiceScopeFactory>();
var options = services.GetRequiredService<IOptions<HealthCheckServiceOptions>>();
var logger = services.GetRequiredService<ILogger<DefaultHealthCheckService>>();
// Act
var exception = Assert.Throws<ArgumentException>(() => 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<string, object>()
{
{ 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<string, object>
{
{ 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<NameCapturingCheck>("A");
b.AddCheck<NameCapturingCheck>("B");
b.AddCheck<NameCapturingCheck>("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<string, object>("name", "A")));
},
actual =>
{
Assert.Equal("B", actual.Key);
Assert.Collection(
actual.Value.Data,
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "B")));
},
actual =>
{
Assert.Equal("C", actual.Key);
Assert.Collection(
actual.Value.Data,
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "C")));
});
}
[Fact]
public async Task CheckHealthAsync_Cancellation_CanPropagate()
{
// Arrange
var insideCheck = new TaskCompletionSource<object>();
var service = CreateHealthChecksService(b =>
{
b.AddAsyncCheck("cancels", async ct =>
{
insideCheck.SetResult(null);
await Task.Delay(10000, ct);
return HealthCheckResult.Unhealthy();
});
});
var cancel = new CancellationTokenSource();
var task = service.CheckHealthAsync(cancel.Token);
// After this returns we know the check has started
await insideCheck.Task;
cancel.Cancel();
// Act & Assert
await Assert.ThrowsAsync<TaskCanceledException>(async () => await task);
}
[Fact]
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToUnhealthyResultAsync()
{
// 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<HealthCheckResult>(faultedException));
b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy()));
});
// 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(HealthStatus.Unhealthy, actual.Value.Status);
Assert.Same(thrownException, actual.Value.Exception);
},
actual =>
{
Assert.Equal("Faults", actual.Key);
Assert.Equal(faultedException.Message, actual.Value.Description);
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
Assert.Same(faultedException, actual.Value.Exception);
},
actual =>
{
Assert.Equal("Succeeds", actual.Key);
Assert.Null(actual.Value.Description);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
});
}
[Fact]
public async Task CheckHealthAsync_SetsUpALoggerScopeForEachCheck()
{
// Arrange
var sink = new TestSink();
var check = new DelegateHealthCheck(cancellationToken =>
{
Assert.Collection(sink.Scopes,
actual =>
{
Assert.Equal(actual.LoggerName, typeof(DefaultHealthCheckService).FullName);
Assert.Collection((IEnumerable<KeyValuePair<string, object>>)actual.Scope,
item =>
{
Assert.Equal("HealthCheckName", item.Key);
Assert.Equal("TestScope", item.Value);
});
});
return Task.FromResult(HealthCheckResult.Healthy());
});
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var service = CreateHealthChecksService(b =>
{
// Override the logger factory for testing
b.Services.AddSingleton<ILoggerFactory>(loggerFactory);
b.AddCheck("TestScope", check);
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(results.Entries, actual =>
{
Assert.Equal("TestScope", actual.Key);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
});
}
[Fact]
public async Task CheckHealthAsync_CheckCanDependOnTransientService()
{
// Arrange
var service = CreateHealthChecksService(b =>
{
b.Services.AddTransient<AnotherService>();
b.AddCheck<CheckWithServiceDependency>("Test");
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(
results.Entries,
actual =>
{
Assert.Equal("Test", actual.Key);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
});
}
[Fact]
public async Task CheckHealthAsync_CheckCanDependOnScopedService()
{
// Arrange
var service = CreateHealthChecksService(b =>
{
b.Services.AddScoped<AnotherService>();
b.AddCheck<CheckWithServiceDependency>("Test");
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(
results.Entries,
actual =>
{
Assert.Equal("Test", actual.Key);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
});
}
[Fact]
public async Task CheckHealthAsync_CheckCanDependOnSingletonService()
{
// Arrange
var service = CreateHealthChecksService(b =>
{
b.Services.AddSingleton<AnotherService>();
b.AddCheck<CheckWithServiceDependency>("Test");
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(
results.Entries,
actual =>
{
Assert.Equal("Test", actual.Key);
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
});
}
private static DefaultHealthCheckService CreateHealthChecksService(Action<IHealthChecksBuilder> configure)
{
var services = new ServiceCollection();
services.AddLogging();
services.AddOptions();
var builder = services.AddHealthChecks();
if (configure != null)
{
configure(builder);
}
return (DefaultHealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService<HealthCheckService>();
}
private class AnotherService { }
private class CheckWithServiceDependency : IHealthCheck
{
public CheckWithServiceDependency(AnotherService _)
{
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
return Task.FromResult(HealthCheckResult.Healthy());
}
}
private class NameCapturingCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var data = new Dictionary<string, object>()
{
{ "name", context.Registration.Name },
};
return Task.FromResult(HealthCheckResult.Healthy(data: data));
}
}
}
}

View File

@ -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.Healthy());
});
var services = CreateServices();
services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded,tags: new[] { "tag", }, instance: instance);
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.Same(instance, registration.Factory(serviceProvider));
}
[Fact]
public void AddCheck_T_TypeActivated()
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddCheck<TestHealthCheck>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<TestHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void AddCheck_T_Service()
{
// Arrange
var instance = new TestHealthCheck();
var services = CreateServices();
services.AddSingleton(instance);
services.AddHealthChecks().AddCheck<TestHealthCheck>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.Same(instance, registration.Factory(serviceProvider));
}
[Fact]
public void AddTypeActivatedCheck()
{
// Arrange
var services = CreateServices();
services
.AddHealthChecks()
.AddTypeActivatedCheck<TestHealthCheckWithArgs>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, args: new object[] { 5, "hi", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
var check = Assert.IsType<TestHealthCheckWithArgs>(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", tags: new[] { "tag", }, check: () =>
{
return HealthCheckResult.Healthy();
});
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void AddDelegateCheck_CancellationToken()
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddCheck("test", (_) =>
{
return HealthCheckResult.Degraded();
}, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void AddAsyncDelegateCheck_NoArg()
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddAsyncCheck("test", () =>
{
return Task.FromResult(HealthCheckResult.Healthy());
}, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void AddAsyncDelegateCheck_CancellationToken()
{
// Arrange
var services = CreateServices();
services.AddHealthChecks().AddAsyncCheck("test", (_) =>
{
return Task.FromResult(HealthCheckResult.Unhealthy());
}, tags: new[] { "tag", });
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
// Assert
var registration = Assert.Single(options.Registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
}
[Fact]
public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks()
{
var services = new ServiceCollection();
services
.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy()));
services
.AddHealthChecks()
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy()));
// Act
var options = services.BuildServiceProvider().GetRequiredService<IOptions<HealthCheckServiceOptions>>();
// 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<HealthCheckResult> 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<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
}
}
}

View File

@ -0,0 +1,43 @@
// 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.Linq;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Xunit;
namespace Microsoft.Extensions.DependencyInjection
{
public class ServiceCollectionExtensionsTest
{
[Fact]
public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently()
{
// Arrange
var services = new ServiceCollection();
// Act
services.AddHealthChecks();
services.AddHealthChecks();
// Assert
Assert.Collection(services.OrderBy(s => s.ServiceType.FullName),
actual =>
{
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
Assert.Equal(typeof(HealthCheckService), actual.ServiceType);
Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType);
Assert.Null(actual.ImplementationInstance);
Assert.Null(actual.ImplementationFactory);
},
actual =>
{
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
Assert.Equal(typeof(IHostedService), actual.ServiceType);
Assert.Equal(typeof(HealthCheckPublisherHostedService), actual.ImplementationType);
Assert.Null(actual.ImplementationInstance);
Assert.Null(actual.ImplementationFactory);
});
}
}
}

View File

@ -0,0 +1,528 @@
// 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.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
public class HealthCheckPublisherHostedServiceTest
{
[Fact]
public async Task StartAsync_WithoutPublishers_DoesNotStartTimer()
{
// Arrange
var publishers = new IHealthCheckPublisher[]
{
};
var service = CreateService(publishers);
try
{
// Act
await service.StartAsync();
// Assert
Assert.False(service.IsTimerRunning);
Assert.False(service.IsStopping);
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
}
[Fact]
public async Task StartAsync_WithPublishers_StartsTimer()
{
// Arrange
var publishers = new IHealthCheckPublisher[]
{
new TestPublisher(),
};
var service = CreateService(publishers);
try
{
// Act
await service.StartAsync();
// Assert
Assert.True(service.IsTimerRunning);
Assert.False(service.IsStopping);
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
}
[Fact]
public async Task StartAsync_WithPublishers_StartsTimer_RunsPublishers()
{
// Arrange
var unblock0 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var unblock1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var unblock2 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var publishers = new TestPublisher[]
{
new TestPublisher() { Wait = unblock0.Task, },
new TestPublisher() { Wait = unblock1.Task, },
new TestPublisher() { Wait = unblock2.Task, },
};
var service = CreateService(publishers, configure: (options) =>
{
options.Delay = TimeSpan.FromMilliseconds(0);
});
try
{
// Act
await service.StartAsync();
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
unblock0.SetResult(null);
unblock1.SetResult(null);
unblock2.SetResult(null);
// Assert
Assert.True(service.IsTimerRunning);
Assert.False(service.IsStopping);
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
}
[Fact]
public async Task StopAsync_CancelsExecution()
{
// Arrange
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var publishers = new TestPublisher[]
{
new TestPublisher() { Wait = unblock.Task, }
};
var service = CreateService(publishers);
try
{
await service.StartAsync();
// Start execution
var running = service.RunAsync();
// Wait for the publisher to see the cancellation token
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
Assert.Single(publishers[0].Entries);
// Act
await service.StopAsync(); // Trigger cancellation
// Assert
await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken);
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
unblock.SetResult(null);
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
}
[Fact]
public async Task RunAsync_WaitsForCompletion_Single()
{
// Arrange
var sink = new TestSink();
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var publishers = new TestPublisher[]
{
new TestPublisher() { Wait = unblock.Task, },
};
var service = CreateService(publishers, sink: sink);
try
{
await service.StartAsync();
// Act
var running = service.RunAsync();
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
unblock.SetResult(null);
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
// Assert
Assert.True(service.IsTimerRunning);
Assert.False(service.IsStopping);
for (var i = 0; i < publishers.Length; i++)
{
var report = Assert.Single(publishers[i].Entries).report;
Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k));
}
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
Assert.Collection(
sink.Writes,
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); },
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherEnd, entry.EventId); },
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
}
// Not testing logs here to avoid differences in logging order
[Fact]
public async Task RunAsync_WaitsForCompletion_Multiple()
{
// Arrange
var unblock0 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var unblock1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var unblock2 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var publishers = new TestPublisher[]
{
new TestPublisher() { Wait = unblock0.Task, },
new TestPublisher() { Wait = unblock1.Task, },
new TestPublisher() { Wait = unblock2.Task, },
};
var service = CreateService(publishers);
try
{
await service.StartAsync();
// Act
var running = service.RunAsync();
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
unblock0.SetResult(null);
unblock1.SetResult(null);
unblock2.SetResult(null);
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
// Assert
Assert.True(service.IsTimerRunning);
Assert.False(service.IsStopping);
for (var i = 0; i < publishers.Length; i++)
{
var report = Assert.Single(publishers[i].Entries).report;
Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k));
}
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
}
[Fact]
public async Task RunAsync_PublishersCanTimeout()
{
// Arrange
var sink = new TestSink();
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var publishers = new TestPublisher[]
{
new TestPublisher() { Wait = unblock.Task, },
};
var service = CreateService(publishers, sink: sink, configure: (options) =>
{
options.Timeout = TimeSpan.FromMilliseconds(50);
});
try
{
await service.StartAsync();
// Act
var running = service.RunAsync();
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken);
unblock.SetResult(null);
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
// Assert
Assert.True(service.IsTimerRunning);
Assert.False(service.IsStopping);
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
Assert.Collection(
sink.Writes,
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); },
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherTimeout, entry.EventId); },
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
}
[Fact]
public async Task RunAsync_CanFilterHealthChecks()
{
// Arrange
var publishers = new TestPublisher[]
{
new TestPublisher(),
new TestPublisher(),
};
var service = CreateService(publishers, configure: (options) =>
{
options.Predicate = (r) => r.Name == "one";
});
try
{
await service.StartAsync();
// Act
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
// Assert
for (var i = 0; i < publishers.Length; i++)
{
var report = Assert.Single(publishers[i].Entries).report;
Assert.Equal(new[] { "one", }, report.Entries.Keys.OrderBy(k => k));
}
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
}
[Fact]
public async Task RunAsync_HandlesExceptions()
{
// Arrange
var sink = new TestSink();
var publishers = new TestPublisher[]
{
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
};
var service = CreateService(publishers, sink: sink);
try
{
await service.StartAsync();
// Act
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
Assert.Collection(
sink.Writes,
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); },
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherError, entry.EventId); },
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
}
// Not testing logging here to avoid flaky ordering issues
[Fact]
public async Task RunAsync_HandlesExceptions_Multiple()
{
// Arrange
var sink = new TestSink();
var publishers = new TestPublisher[]
{
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
new TestPublisher(),
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
};
var service = CreateService(publishers, sink: sink);
try
{
await service.StartAsync();
// Act
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
}
finally
{
await service.StopAsync();
Assert.False(service.IsTimerRunning);
Assert.True(service.IsStopping);
}
}
private HealthCheckPublisherHostedService CreateService(
IHealthCheckPublisher[] publishers,
Action<HealthCheckPublisherOptions> configure = null,
TestSink sink = null)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddOptions();
serviceCollection.AddLogging();
serviceCollection.AddHealthChecks()
.AddCheck("one", () => { return HealthCheckResult.Healthy(); })
.AddCheck("two", () => { return HealthCheckResult.Healthy(); });
// Choosing big values for tests to make sure that we're not dependent on the defaults.
// All of the tests that rely on the timer will set their own values for speed.
serviceCollection.Configure<HealthCheckPublisherOptions>(options =>
{
options.Delay = TimeSpan.FromMinutes(5);
options.Period = TimeSpan.FromMinutes(5);
options.Timeout = TimeSpan.FromMinutes(5);
});
if (publishers != null)
{
for (var i = 0; i < publishers.Length; i++)
{
serviceCollection.AddSingleton<IHealthCheckPublisher>(publishers[i]);
}
}
if (configure != null)
{
serviceCollection.Configure(configure);
}
if (sink != null)
{
serviceCollection.AddSingleton<ILoggerFactory>(new TestLoggerFactory(sink, enabled: true));
}
var services = serviceCollection.BuildServiceProvider();
return services.GetServices<IHostedService>().OfType< HealthCheckPublisherHostedService>().Single();
}
private static async Task AssertCancelledAsync(CancellationToken cancellationToken)
{
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Delay(TimeSpan.FromSeconds(10), cancellationToken));
}
private class TestPublisher : IHealthCheckPublisher
{
private TaskCompletionSource<object> _started;
public TestPublisher()
{
_started = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
}
public List<(HealthReport report, CancellationToken cancellationToken)> Entries { get; } = new List<(HealthReport report, CancellationToken cancellationToken)>();
public Exception Exception { get; set; }
public Task Started => _started.Task;
public Task Wait { get; set; }
public async Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
{
Entries.Add((report, cancellationToken));
// Signal that we've started
_started.SetResult(null);
if (Wait != null)
{
await Wait;
}
if (Exception != null)
{
throw Exception;
}
cancellationToken.ThrowIfCancellationRequested();
}
}
}
}

View File

@ -0,0 +1,45 @@
// 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 Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
public class HealthReportTest
{
[Theory]
[InlineData(HealthStatus.Healthy)]
[InlineData(HealthStatus.Degraded)]
[InlineData(HealthStatus.Unhealthy)]
public void Status_MatchesWorstStatusInResults(HealthStatus status)
{
var result = new HealthReport(new Dictionary<string, HealthReportEntry>()
{
{"Foo", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) },
{"Bar", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue,null, null) },
{"Baz", new HealthReportEntry(status, exception: null, description: null,duration:TimeSpan.MinValue, data: null) },
{"Quick", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) },
{"Quack", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) },
{"Quock", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) },
}, totalDuration: TimeSpan.MinValue);
Assert.Equal(status, result.Status);
}
[Theory]
[InlineData(200)]
[InlineData(300)]
[InlineData(400)]
public void TotalDuration_MatchesTotalDurationParameter(int milliseconds)
{
var result = new HealthReport(new Dictionary<string, HealthReportEntry>()
{
{"Foo", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) }
}, totalDuration: TimeSpan.FromMilliseconds(milliseconds));
Assert.Equal(TimeSpan.FromMilliseconds(milliseconds), result.TotalDuration);
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(RepositoryRoot)src\Logging\Logging.Testing\src\build\Microsoft.Extensions.Logging.Testing.props" />
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
</ItemGroup>
</Project>