commit
fcdba6256a
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsProductComponent>true</IsProductComponent>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsProductComponent>true</IsProductComponent>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
]
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue