Merge the release/2.2 branch of Diagnostics

This commit is contained in:
Nate McMaster 2018-11-21 15:32:59 -08:00
commit 925746158e
No known key found for this signature in database
GPG Key ID: A778D9601BD78810
45 changed files with 2336 additions and 10 deletions

View File

@ -33,6 +33,7 @@
<MicrosoftAspNetCoreTestingPackageVersion>2.2.0</MicrosoftAspNetCoreTestingPackageVersion>
<MicrosoftEntityFrameworkCoreInMemoryPackageVersion>2.2.0</MicrosoftEntityFrameworkCoreInMemoryPackageVersion>
<MicrosoftEntityFrameworkCorePackageVersion>2.2.0</MicrosoftEntityFrameworkCorePackageVersion>
<MicrosoftEntityFrameworkCoreRelationalPackageVersion>2.2.0</MicrosoftEntityFrameworkCoreRelationalPackageVersion>
<MicrosoftEntityFrameworkCoreSqlServerPackageVersion>2.2.0</MicrosoftEntityFrameworkCoreSqlServerPackageVersion>
<MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>2.2.0</MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>
<MicrosoftExtensionsCachingAbstractionsPackageVersion>2.2.0</MicrosoftExtensionsCachingAbstractionsPackageVersion>

View File

@ -14,6 +14,7 @@
<LatestPackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
<LatestPackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
<LatestPackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(MicrosoftEntityFrameworkCoreInMemoryPackageVersion)" />
<LatestPackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(MicrosoftEntityFrameworkCoreRelationalPackageVersion)" />
<LatestPackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion)" />
<LatestPackageReference Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftEntityFrameworkCorePackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.ActivatorUtilities.Sources" Version="$(MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion)" />

View File

@ -45,6 +45,8 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.Abstractions" ProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.Abstractions\src\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" ProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics" ProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics\src\Microsoft.AspNetCore.Diagnostics.csproj" />
<ProjectReferenceProvider Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" ProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks.EntityFrameworkCore\src\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" ProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks\src\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.MiddlewareAnalysis" ProjectPath="$(RepositoryRoot)src\Middleware\MiddlewareAnalysis\src\Microsoft.AspNetCore.MiddlewareAnalysis.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.WebSockets" ProjectPath="$(RepositoryRoot)src\Middleware\WebSockets\src\Microsoft.AspNetCore.WebSockets.csproj" />
</ItemGroup>

View File

@ -12,6 +12,7 @@
<PackageTags>aspnetcore;testing</PackageTags>
<EnableApiCheck>false</EnableApiCheck>
<UseLatestPackageReferences>true</UseLatestPackageReferences>
<UseProjectReferences>true</UseProjectReferences>
<IsPackable>false</IsPackable>
</PropertyGroup>

View File

@ -3,8 +3,6 @@
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<SignAssembly>false</SignAssembly>
<PublicSign>false</PublicSign>
<!-- Mitigation for long path issues -->
<AssemblyName>Diagnostics.FunctionalTests</AssemblyName>
</PropertyGroup>

View File

@ -8,11 +8,11 @@
<EmbeddedResource Include="Resources\**" Exclude="$(DefaultItemExcludes)" />
<Content Include="TestFiles\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\testassets\ClassLibraryWithPortablePdbs\ClassLibraryWithPortablePdbs.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
<Reference Include="Microsoft.AspNetCore.TestHost" />

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,56 @@
// 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.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
internal sealed class DbContextHealthCheck<TContext> : IHealthCheck where TContext : DbContext
{
private static readonly Func<TContext, CancellationToken, Task<bool>> DefaultTestQuery = (dbContext, cancellationToken) =>
{
return dbContext.Database.CanConnectAsync(cancellationToken);
};
private readonly TContext _dbContext;
private readonly IOptionsMonitor<DbContextHealthCheckOptions<TContext>> _options;
public DbContextHealthCheck(TContext dbContext, IOptionsMonitor<DbContextHealthCheckOptions<TContext>> options)
{
if (dbContext == null)
{
throw new ArgumentNullException(nameof(dbContext));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_dbContext = dbContext;
_options = options;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var options = _options.Get(context.Registration.Name);
var testQuery = options.CustomTestQuery ?? DefaultTestQuery;
if (await testQuery(_dbContext, cancellationToken))
{
return HealthCheckResult.Healthy();
}
return HealthCheckResult.Unhealthy();
}
}
}

View File

@ -0,0 +1,15 @@
// 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.EntityFrameworkCore;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
internal sealed class DbContextHealthCheckOptions<TContext> where TContext : DbContext
{
public Func<TContext, CancellationToken, Task<bool>> CustomTestQuery { get; set; }
}
}

View File

@ -0,0 +1,79 @@
// 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.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection
{
public static class EntityFrameworkCoreHealthChecksBuilderExtensions
{
/// <summary>
/// Adds a health check for the specified <see cref="DbContext"/> type.
/// </summary>
/// <typeparam name="TContext">The <see cref="DbContext"/> type.</typeparam>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">
/// The health check name. Optional. If <c>null</c> the type name of <typeparamref name="TContext"/> will be used for the name.
/// </param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="customTestQuery">
/// A custom test query that will be executed when the health check executes to test the health of the database
/// connection and configurations.
/// </param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
/// <remarks>
/// <para>
/// The health check implementation added by this method will use the dependency injection container
/// to create an instance of <typeparamref name="TContext"/>.
/// </para>
/// <para>
/// By default the health check implementation will use the <see cref="DatabaseFacade.CanConnectAsync(CancellationToken)"/> method
/// to test connectivity to the database. This method requires that the database provider has correctly implemented the
/// <see cref="IDatabaseCreatorWithCanConnect" /> interface. If the database provide has not implemented this interface
/// then the health check will report a failure.
/// </para>
/// <para>
/// Providing a <paramref name="customTestQuery" /> will replace the use of <see cref="DatabaseFacade.CanConnectAsync(CancellationToken)"/>
/// to test database connectivity. An implementation of a test query should handle exceptions that can arise due to connectivity failure,
/// and should return a pass/fail result. The test query should be be designed to complete in a short and predicatable amount of time.
/// </para>
/// </remarks>
public static IHealthChecksBuilder AddDbContextCheck<TContext>(
this IHealthChecksBuilder builder,
string name = null,
HealthStatus? failureStatus = default,
IEnumerable<string> tags = default,
Func<TContext, CancellationToken, Task<bool>> customTestQuery = default)
where TContext : DbContext
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
name = typeof(TContext).Name;
}
if (customTestQuery != null)
{
builder.Services.Configure<DbContextHealthCheckOptions<TContext>>(name, options => options.CustomTestQuery = customTestQuery);
}
return builder.AddCheck<DbContextHealthCheck<TContext>>(name, failureStatus, tags);
}
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Components for performing health checks using EntityFrameworkCore.
</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>diagnostics;healthchecks;entityframeworkcore</PackageTags>
<BaseNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</BaseNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.EntityFrameworkCore.Relational" />
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
</ItemGroup>
</Project>

View File

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

View File

@ -0,0 +1,119 @@
// 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.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
public class DbContextHealthCheckTest
{
// Just testing healthy here since it would be complicated to simulate a failure. All of that logic lives in EF anyway.
[Fact]
public async Task CheckAsync_DefaultTest_Healthy()
{
// Arrange
var services = CreateServices();
using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var registration = Assert.Single(services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value.Registrations);
var check = ActivatorUtilities.CreateInstance<DbContextHealthCheck<TestDbContext>>(scope.ServiceProvider);
// Act
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
// Assert
Assert.Equal(HealthStatus.Healthy, result.Status);
}
}
[Fact]
public async Task CheckAsync_CustomTest_Healthy()
{
// Arrange
var services = CreateServices(async (c, ct) =>
{
return 0 < await c.Blogs.CountAsync();
});
using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var registration = Assert.Single(services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value.Registrations);
var check = ActivatorUtilities.CreateInstance<DbContextHealthCheck<TestDbContext>>(scope.ServiceProvider);
// Add a blog so that the custom test passes
var context = scope.ServiceProvider.GetRequiredService<TestDbContext>();
context.Add(new Blog());
await context.SaveChangesAsync();
// Act
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
// Assert
Assert.Equal(HealthStatus.Healthy, result.Status);
}
}
[Fact]
public async Task CheckAsync_CustomTest_Degraded()
{
// Arrange
var services = CreateServices(async (c, ct) =>
{
return 0 < await c.Blogs.CountAsync();
}, failureStatus: HealthStatus.Degraded);
using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var registration = Assert.Single(services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value.Registrations);
var check = ActivatorUtilities.CreateInstance<DbContextHealthCheck<TestDbContext>>(scope.ServiceProvider);
// Act
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
// Assert
Assert.Equal(HealthStatus.Unhealthy, result.Status);
}
}
[Fact]
public async Task CheckAsync_CustomTest_Unhealthy()
{
// Arrange
var services = CreateServices(async (c, ct) =>
{
return 0 < await c.Blogs.CountAsync();
}, failureStatus: HealthStatus.Unhealthy);
using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var registration = Assert.Single(services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value.Registrations);
var check = ActivatorUtilities.CreateInstance<DbContextHealthCheck<TestDbContext>>(scope.ServiceProvider);
// Act
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
// Assert
Assert.Equal(HealthStatus.Unhealthy, result.Status);
}
}
private static IServiceProvider CreateServices(
Func<TestDbContext, CancellationToken, Task<bool>> testQuery = null,
HealthStatus failureStatus = HealthStatus.Unhealthy)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDbContext<TestDbContext>(o => o.UseInMemoryDatabase("Test"));
var builder = serviceCollection.AddHealthChecks();
builder.AddDbContextCheck<TestDbContext>("test", failureStatus, new[] { "tag1", "tag2", }, testQuery);
return serviceCollection.BuildServiceProvider();
}
}
}

View File

@ -0,0 +1,47 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.Extensions.DependencyInjection
{
public class EntityFrameworkCoreHealthChecksBuilderExtensionsTest
{
[Fact]
public void AddDbContextCheck_RegistersDbContextHealthCheck()
{
// Arrange
var serviceCollection = new ServiceCollection();
serviceCollection.AddDbContext<TestDbContext>(o => o.UseInMemoryDatabase("Test"));
var builder = serviceCollection.AddHealthChecks();
// Act
builder.AddDbContextCheck<TestDbContext>("test", HealthStatus.Degraded, new[] { "tag1", "tag2", }, (c, ct) => Task.FromResult(true));
// Assert
var services = serviceCollection.BuildServiceProvider();
var registrations = services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value.Registrations;
var registration = Assert.Single(registrations);
Assert.Equal("test", registration.Name);
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
Assert.Equal(new[] { "tag1", "tag2", }, registration.Tags.ToArray());
var options = services.GetRequiredService<IOptionsMonitor<DbContextHealthCheckOptions<TestDbContext>>>();
Assert.NotNull(options.Get("test").CustomTestQuery);
using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var check = Assert.IsType<DbContextHealthCheck<TestDbContext>>(registration.Factory(scope.ServiceProvider));
}
}
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<RootNamespace>Microsoft.AspNetCore.Diagnostics.HealthChecks</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.EntityFrameworkCore.InMemory" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.EntityFrameworkCore;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
public class TestDbContext : DbContext
{
public TestDbContext(DbContextOptions<TestDbContext> options)
: base(options)
{
}
public DbSet<Blog> Blogs { get; set; }
}
public class Blog
{
public int Id { get; set; }
public int Name { get; set; }
}
}

View File

@ -0,0 +1,254 @@
// 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.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// <see cref="IApplicationBuilder"/> extension methods for the <see cref="HealthCheckMiddleware"/>.
/// </summary>
public static class HealthCheckApplicationBuilderExtensions
{
/// <summary>
/// Adds a middleware that provides health check status.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
/// <param name="path">The path on which to provide health check status.</param>
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
/// <remarks>
/// <para>
/// If <paramref name="path"/> is set to <c>null</c> or the empty string then the health check middleware
/// will ignore the URL path and process all requests. If <paramref name="path"/> is set to a non-empty
/// value, the health check middleware will process requests with a URL that matches the provided value
/// of <paramref name="path"/> case-insensitively, allowing for an extra trailing slash ('/') character.
/// </para>
/// <para>
/// The health check middleware will use default settings from <see cref="IOptions{HealthCheckOptions}"/>.
/// </para>
/// </remarks>
public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
UseHealthChecksCore(app, path, port: null, Array.Empty<object>());
return app;
}
/// <summary>
/// Adds a middleware that provides health check status.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
/// <param name="path">The path on which to provide health check status.</param>
/// <param name="options">A <see cref="HealthCheckOptions"/> used to configure the middleware.</param>
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
/// <remarks>
/// <para>
/// If <paramref name="path"/> is set to <c>null</c> or the empty string then the health check middleware
/// will ignore the URL path and process all requests. If <paramref name="path"/> is set to a non-empty
/// value, the health check middleware will process requests with a URL that matches the provided value
/// of <paramref name="path"/> case-insensitively, allowing for an extra trailing slash ('/') character.
/// </para>
/// </remarks>
public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, HealthCheckOptions options)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
UseHealthChecksCore(app, path, port: null, new[] { Options.Create(options), });
return app;
}
/// <summary>
/// Adds a middleware that provides health check status.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
/// <param name="path">The path on which to provide health check status.</param>
/// <param name="port">The port to listen on. Must be a local port on which the server is listening.</param>
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
/// <remarks>
/// <para>
/// If <paramref name="path"/> is set to <c>null</c> or the empty string then the health check middleware
/// will ignore the URL path and process all requests on the specified port. If <paramref name="path"/> is
/// set to a non-empty value, the health check middleware will process requests with a URL that matches the
/// provided value of <paramref name="path"/> case-insensitively, allowing for an extra trailing slash ('/')
/// character.
/// </para>
/// <para>
/// The health check middleware will use default settings from <see cref="IOptions{HealthCheckOptions}"/>.
/// </para>
/// </remarks>
public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, int port)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
UseHealthChecksCore(app, path, port, Array.Empty<object>());
return app;
}
/// <summary>
/// Adds a middleware that provides health check status.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
/// <param name="path">The path on which to provide health check status.</param>
/// <param name="port">The port to listen on. Must be a local port on which the server is listening.</param>
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
/// <remarks>
/// <para>
/// If <paramref name="path"/> is set to <c>null</c> or the empty string then the health check middleware
/// will ignore the URL path and process all requests on the specified port. If <paramref name="path"/> is
/// set to a non-empty value, the health check middleware will process requests with a URL that matches the
/// provided value of <paramref name="path"/> case-insensitively, allowing for an extra trailing slash ('/')
/// character.
/// </para>
/// <para>
/// The health check middleware will use default settings from <see cref="IOptions{HealthCheckOptions}"/>.
/// </para>
/// </remarks>
public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, string port)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (port == null)
{
throw new ArgumentNullException(nameof(port));
}
if (!int.TryParse(port, out var portAsInt))
{
throw new ArgumentException("The port must be a valid integer.", nameof(port));
}
UseHealthChecksCore(app, path, portAsInt, Array.Empty<object>());
return app;
}
/// <summary>
/// Adds a middleware that provides health check status.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
/// <param name="path">The path on which to provide health check status.</param>
/// <param name="port">The port to listen on. Must be a local port on which the server is listening.</param>
/// <param name="options">A <see cref="HealthCheckOptions"/> used to configure the middleware.</param>
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
/// <remarks>
/// <para>
/// If <paramref name="path"/> is set to <c>null</c> or the empty string then the health check middleware
/// will ignore the URL path and process all requests on the specified port. If <paramref name="path"/> is
/// set to a non-empty value, the health check middleware will process requests with a URL that matches the
/// provided value of <paramref name="path"/> case-insensitively, allowing for an extra trailing slash ('/')
/// character.
/// </para>
/// </remarks>
public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, int port, HealthCheckOptions options)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
UseHealthChecksCore(app, path, port, new[] { Options.Create(options), });
return app;
}
/// <summary>
/// Adds a middleware that provides health check status.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
/// <param name="path">The path on which to provide health check status.</param>
/// <param name="port">The port to listen on. Must be a local port on which the server is listening.</param>
/// <param name="options">A <see cref="HealthCheckOptions"/> used to configure the middleware.</param>
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
/// <remarks>
/// <para>
/// If <paramref name="path"/> is set to <c>null</c> or the empty string then the health check middleware
/// will ignore the URL path and process all requests on the specified port. If <paramref name="path"/> is
/// set to a non-empty value, the health check middleware will process requests with a URL that matches the
/// provided value of <paramref name="path"/> case-insensitively, allowing for an extra trailing slash ('/')
/// character.
/// </para>
/// </remarks>
public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, string port, HealthCheckOptions options)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (path == null)
{
throw new ArgumentNullException(nameof(port));
}
if (!int.TryParse(port, out var portAsInt))
{
throw new ArgumentException("The port must be a valid integer.", nameof(port));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
UseHealthChecksCore(app, path, portAsInt, new[] { Options.Create(options), });
return app;
}
private static void UseHealthChecksCore(IApplicationBuilder app, PathString path, int? port, object[] args)
{
// NOTE: we explicitly don't use Map here because it's really common for multiple health
// check middleware to overlap in paths. Ex: `/health`, `/health/detailed` - this is order
// sensititive with Map, and it's really surprising to people.
//
// See:
// https://github.com/aspnet/Diagnostics/issues/511
// https://github.com/aspnet/Diagnostics/issues/512
// https://github.com/aspnet/Diagnostics/issues/514
Func<HttpContext, bool> predicate = c =>
{
return
// Process the port if we have one
(port == null || c.Connection.LocalPort == port) &&
// We allow you to listen on all URLs by providing the empty PathString.
(!path.HasValue ||
// If you do provide a PathString, want to handle all of the special cases that
// StartsWithSegments handles, but we also want it to have exact match semantics.
//
// Ex: /Foo/ == /Foo (true)
// Ex: /Foo/Bar == /Foo (false)
(c.Request.Path.StartsWithSegments(path, out var remaining) &&
string.IsNullOrEmpty(remaining)));
};
app.MapWhen(predicate, b => b.UseMiddleware<HealthCheckMiddleware>(args));
}
}
}

View File

@ -0,0 +1,125 @@
// 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.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
public class HealthCheckMiddleware
{
private readonly RequestDelegate _next;
private readonly HealthCheckOptions _healthCheckOptions;
private readonly HealthCheckService _healthCheckService;
public HealthCheckMiddleware(
RequestDelegate next,
IOptions<HealthCheckOptions> healthCheckOptions,
HealthCheckService healthCheckService)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (healthCheckOptions == null)
{
throw new ArgumentNullException(nameof(healthCheckOptions));
}
if (healthCheckService == null)
{
throw new ArgumentNullException(nameof(healthCheckService));
}
_next = next;
_healthCheckOptions = healthCheckOptions.Value;
_healthCheckService = healthCheckService;
}
/// <summary>
/// Processes a request.
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
// Get results
var result = await _healthCheckService.CheckHealthAsync(_healthCheckOptions.Predicate, httpContext.RequestAborted);
// Map status to response code - this is customizable via options.
if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode))
{
var message =
$"No status code mapping found for {nameof(HealthStatus)} value: {result.Status}." +
$"{nameof(HealthCheckOptions)}.{nameof(HealthCheckOptions.ResultStatusCodes)} must contain" +
$"an entry for {result.Status}.";
throw new InvalidOperationException(message);
}
httpContext.Response.StatusCode = statusCode;
if (!_healthCheckOptions.AllowCachingResponses)
{
// Similar to: https://github.com/aspnet/Security/blob/7b6c9cf0eeb149f2142dedd55a17430e7831ea99/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs#L377-L379
var headers = httpContext.Response.Headers;
headers[HeaderNames.CacheControl] = "no-store, no-cache";
headers[HeaderNames.Pragma] = "no-cache";
headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT";
}
if (_healthCheckOptions.ResponseWriter != null)
{
await _healthCheckOptions.ResponseWriter(httpContext, result);
}
}
private static IHealthCheck[] FilterHealthChecks(
IReadOnlyDictionary<string, IHealthCheck> checks,
ISet<string> names)
{
// If there are no filters then include all checks.
if (names.Count == 0)
{
return checks.Values.ToArray();
}
// Keep track of what we don't find so we can report errors.
var notFound = new HashSet<string>(names, StringComparer.OrdinalIgnoreCase);
var matches = new List<IHealthCheck>();
foreach (var kvp in checks)
{
if (!notFound.Remove(kvp.Key))
{
// This check was excluded
continue;
}
matches.Add(kvp.Value);
}
if (notFound.Count > 0)
{
var message =
$"The following health checks were not found: '{string.Join(", ", notFound)}'. " +
$"Registered health checks: '{string.Join(", ", checks.Keys)}'.";
throw new InvalidOperationException(message);
}
return matches.ToArray();
}
}
}

View File

@ -0,0 +1,63 @@
// 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.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
/// <summary>
/// Contains options for the <see cref="HealthCheckMiddleware"/>.
/// </summary>
public class HealthCheckOptions
{
/// <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 <see cref="HealthCheckMiddleware"/> 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.
/// </remarks>
public Func<HealthCheckRegistration, bool> Predicate { get; set; }
/// <summary>
/// Gets a dictionary mapping the <see cref="HealthStatus"/> to an HTTP status code applied to the response.
/// This property can be used to configure the status codes returned for each status.
/// </summary>
public IDictionary<HealthStatus, int> ResultStatusCodes { get; } = new Dictionary<HealthStatus, int>()
{
{ HealthStatus.Healthy, StatusCodes.Status200OK },
{ HealthStatus.Degraded, StatusCodes.Status200OK },
{ HealthStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable },
};
/// <summary>
/// Gets or sets a delegate used to write the response.
/// </summary>
/// <remarks>
/// The default value is a delegate that will write a minimal <c>text/plain</c> response with the value
/// of <see cref="HealthReport.Status"/> as a string.
/// </remarks>
public Func<HttpContext, HealthReport, Task> ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext;
/// <summary>
/// Gets or sets a value that controls whether responses from the health check middleware can be cached.
/// </summary>
/// <remarks>
/// <para>
/// The health check middleware does not perform caching of any kind. This setting configures whether
/// the middleware will apply headers to the HTTP response that instruct clients to avoid caching.
/// </para>
/// <para>
/// If the value is <c>false</c> the health check middleware will set or override the
/// <c>Cache-Control</c>, <c>Expires</c>, and <c>Pragma</c> headers to prevent response caching. If the value
/// is <c>true</c> the health check middleware will not modify the cache headers of the response.
/// </para>
/// </remarks>
public bool AllowCachingResponses { get; set; }
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
internal static class HealthCheckResponseWriters
{
public static Task WriteMinimalPlaintext(HttpContext httpContext, HealthReport result)
{
httpContext.Response.ContentType = "text/plain";
return httpContext.Response.WriteAsync(result.Status.ToString());
}
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core middleware for returning the results of Health Checks in the application</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>diagnostics;healthchecks</PackageTags>
<Description>ASP.NET Core middleware for returning the results of Health Checks in the application
</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>diagnostics;healthchecks</PackageTags>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
<Reference Include="Microsoft.Extensions.Options" />
<Reference Include="Microsoft.Net.Http.Headers" />
</ItemGroup>
</Project>

View File

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

View File

@ -0,0 +1,75 @@
// 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.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Xunit;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
public class HealthCheckMiddlewareSampleTest
{
[Fact]
public async Task BasicStartup()
{
var builder = new WebHostBuilder()
.UseStartup<HealthChecksSample.BasicStartup>();
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CustomWriterStartup()
{
var builder = new WebHostBuilder()
.UseStartup<HealthChecksSample.CustomWriterStartup>();
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
// Ignoring the body since it contains a bunch of statistics
}
[Fact]
public async Task LivenessProbeStartup_Liveness()
{
var builder = new WebHostBuilder()
.UseStartup<HealthChecksSample.LivenessProbeStartup>();
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health/live");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task LivenessProbeStartup_Readiness()
{
var builder = new WebHostBuilder()
.UseStartup<HealthChecksSample.LivenessProbeStartup>();
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health/ready");
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync());
}
}
}

View File

@ -0,0 +1,688 @@
// 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.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using System.Net;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
public class HealthCheckMiddlewareTests
{
[Fact] // Matches based on '.Map'
public async Task IgnoresRequestThatDoesNotMatchPath()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/frob");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact] // Matches based on '.Map'
public async Task MatchIsCaseInsensitive()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/HEALTH");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task ReturnsPlainTextStatus()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task StatusCodeIs200IfNoChecks()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task StatusCodeIs200IfAllChecksHealthy()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => HealthCheckResult.Healthy("A-ok!"))
.AddCheck("Bar", () => HealthCheckResult.Healthy("A-ok!"))
.AddCheck("Baz", () => HealthCheckResult.Healthy("A-ok!"));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task StatusCodeIs200IfCheckIsDegraded()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => HealthCheckResult.Healthy("A-ok!"))
.AddCheck("Bar", () => HealthCheckResult.Degraded("Not so great."))
.AddCheck("Baz", () => HealthCheckResult.Healthy("A-ok!"));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Degraded", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task StatusCodeIs503IfCheckIsUnhealthy()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task StatusCodeIs503IfCheckHasUnhandledException()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddAsyncCheck("Bar", () => throw null)
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CanUseCustomWriter()
{
var expectedJson = JsonConvert.SerializeObject(new
{
status = "Unhealthy",
});
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
ResponseWriter = (c, r) =>
{
var json = JsonConvert.SerializeObject(new { status = r.Status.ToString(), });
c.Response.ContentType = "application/json";
return c.Response.WriteAsync(json);
},
});
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
var result = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedJson, result);
}
[Fact]
public async Task NoResponseWriterReturnsEmptyBody()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
ResponseWriter = null,
});
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad.")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CanSetCustomStatusCodes()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
ResultStatusCodes =
{
[HealthStatus.Healthy] = 201,
}
});
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task SetsCacheHeaders()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
Assert.Equal("no-store, no-cache", response.Headers.CacheControl.ToString());
Assert.Equal("no-cache", response.Headers.Pragma.ToString());
Assert.Equal(new string[] { "Thu, 01 Jan 1970 00:00:00 GMT" }, response.Content.Headers.GetValues(HeaderNames.Expires));
}
[Fact]
public async Task CanSuppressCacheHeaders()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
AllowCachingResponses = true,
});
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
Assert.Null(response.Headers.CacheControl);
Assert.Empty(response.Headers.Pragma.ToString());
Assert.False(response.Content.Headers.Contains(HeaderNames.Expires));
}
[Fact]
public async Task CanFilterChecks()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
Predicate = (check) => check.Name == "Foo" || check.Name == "Baz",
});
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
// Will get filtered out
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!")))
.AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CanListenWithoutPath_AcceptsRequest()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks(default);
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CanListenWithPath_AcceptsRequestWithExtraSlash()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/health/");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task CanListenWithPath_AcceptsRequestWithCaseInsensitiveMatch()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/HEALTH");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CanListenWithPath_RejectsRequestWithExtraSegments()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/health/detailed");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
// See: https://github.com/aspnet/Diagnostics/issues/511
[Fact]
public async Task CanListenWithPath_MultipleMiddleware_LeastSpecificFirst()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
// Throws if used
app.UseHealthChecks("/health", new HealthCheckOptions()
{
ResponseWriter = (c, r) => throw null,
});
app.UseHealthChecks("/health/detailed");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/health/detailed");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
// See: https://github.com/aspnet/Diagnostics/issues/511
[Fact]
public async Task CanListenWithPath_MultipleMiddleware_MostSpecificFirst()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health/detailed");
// Throws if used
app.UseHealthChecks("/health", new HealthCheckOptions()
{
ResponseWriter = (c, r) => throw null,
});
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/health/detailed");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.Use(next => async (context) =>
{
// Need to fake setting the connection info. TestServer doesn't
// do that, because it doesn't have a connection.
context.Connection.LocalPort = context.Request.Host.Port.Value;
await next(context);
});
app.UseHealthChecks("/health", port: 5001);
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CanListenOnPortWithoutPath_AcceptsRequest_OnSpecifiedPort()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.Use(next => async (context) =>
{
// Need to fake setting the connection info. TestServer doesn't
// do that, because it doesn't have a connection.
context.Connection.LocalPort = context.Request.Host.Port.Value;
await next(context);
});
app.UseHealthChecks(default, port: 5001);
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CanListenOnPort_RejectsRequest_OnOtherPort()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.Use(next => async (context) =>
{
// Need to fake setting the connection info. TestServer doesn't
// do that, because it doesn't have a connection.
context.Connection.LocalPort = context.Request.Host.Port.Value;
await next(context);
});
app.UseHealthChecks("/health", port: 5001);
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5000/health");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task CanListenOnPort_MultipleMiddleware()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.Use(next => async (context) =>
{
// Need to fake setting the connection info. TestServer doesn't
// do that, because it doesn't have a connection.
context.Connection.LocalPort = context.Request.Host.Port.Value;
await next(context);
});
// Throws if used
app.UseHealthChecks("/health", port: 5001, new HealthCheckOptions()
{
ResponseWriter = (c, r) => throw null,
});
app.UseHealthChecks("/health/detailed", port: 5001);
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/health/detailed");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task CanListenOnPort_MultipleMiddleware_DifferentPorts()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.Use(next => async (context) =>
{
// Need to fake setting the connection info. TestServer doesn't
// do that, because it doesn't have a connection.
context.Connection.LocalPort = context.Request.Host.Port.Value;
await next(context);
});
// Throws if used
app.UseHealthChecks("/health", port: 5002, new HealthCheckOptions()
{
ResponseWriter = (c, r) => throw null,
});
app.UseHealthChecks("/health", port: 5001);
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost:5001/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<RootNamespace>Microsoft.AspNetCore.Diagnostics.HealthChecks</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\testassets\HealthChecksSample\HealthChecksSample.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace HealthChecksSample
{
// Pass in `--scenario basic` at the command line to run this sample.
public class BasicStartup
{
public void ConfigureServices(IServiceCollection services)
{
// Registers required services for health checks
services.AddHealthChecks();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// This will register the health checks middleware at the URL /health.
//
// By default health checks will return a 200 with 'Healthy'.
// - No health checks are registered by default, the app is healthy if it is reachable
// - The default response writer writes the HealthCheckStatus as text/plain content
//
// This is the simplest way to use health checks, it is suitable for systems
// that want to check for 'liveness' of an application.
app.UseHealthChecks("/health");
app.Run(async (context) =>
{
await context.Response.WriteAsync("Go to /health to see the health status");
});
}
}
}

View File

@ -0,0 +1,58 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace HealthChecksSample
{
// Pass in `--scenario writer` at the command line to run this sample.
public class CustomWriterStartup
{
public void ConfigureServices(IServiceCollection services)
{
// Registers required services for health checks
services.AddHealthChecks()
// Registers a custom health check implementation
.AddGCInfoCheck("GCInfo");
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// This will register the health checks middleware at the URL /health
//
// This example overrides the HealthCheckResponseWriter to write the health
// check result in a totally custom way.
app.UseHealthChecks("/health", new HealthCheckOptions()
{
// This custom writer formats the detailed status as JSON.
ResponseWriter = WriteResponse,
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Go to /health to see the health status");
});
}
private static Task WriteResponse(HttpContext httpContext, HealthReport result)
{
httpContext.Response.ContentType = "application/json";
var json = new JObject(
new JProperty("status", result.Status.ToString()),
new JProperty("results", new JObject(result.Entries.Select(pair =>
new JProperty(pair.Key, new JObject(
new JProperty("status", pair.Value.Status.ToString()),
new JProperty("description", pair.Value.Description),
new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value))))))))));
return httpContext.Response.WriteAsync(json.ToString(Formatting.Indented));
}
}
}

View File

@ -0,0 +1,46 @@

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace HealthChecksSample
{
// Pass in `--scenario db` at the command line to run this sample.
public class DbHealthStartup
{
public DbHealthStartup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Registers required services for health checks
services.AddHealthChecks()
// Add a health check for a SQL database
.AddCheck("MyDatabase", new SqlConnectionHealthCheck(Configuration["ConnectionStrings:DefaultConnection"]));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// This will register the health checks middleware at the URL /health.
//
// By default health checks will return a 200 with 'Healthy' when the database is responsive
// - We've registered a SqlConnectionHealthCheck
// - The default response writer writes the HealthCheckStatus as text/plain content
//
// This is the simplest way to use health checks, it is suitable for systems
// that want to check for 'liveness' of an application with a database.
app.UseHealthChecks("/health");
app.Run(async (context) =>
{
await context.Response.WriteAsync("Go to /health to see the health status");
});
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace HealthChecksSample
{
public abstract class DbConnectionHealthCheck : IHealthCheck
{
protected DbConnectionHealthCheck(string connectionString)
: this(connectionString, testQuery: null)
{
}
protected DbConnectionHealthCheck(string connectionString, string testQuery)
{
ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
TestQuery = testQuery;
}
protected string ConnectionString { get; }
// This sample supports specifying a query to run as a boolean test of whether the database
// is responding. It is important to choose a query that will return quickly or you risk
// overloading the database.
//
// In most cases this is not necessary, but if you find it necessary, choose a simple query such as 'SELECT 1'.
protected string TestQuery { get; }
protected abstract DbConnection CreateConnection(string connectionString);
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
using (var connection = CreateConnection(ConnectionString))
{
try
{
await connection.OpenAsync(cancellationToken);
if (TestQuery != null)
{
var command = connection.CreateCommand();
command.CommandText = TestQuery;
await command.ExecuteNonQueryAsync(cancellationToken);
}
}
catch (DbException ex)
{
return new HealthCheckResult(status: context.Registration.FailureStatus, exception: ex);
}
}
return HealthCheckResult.Healthy();
}
}
}

View File

@ -0,0 +1,85 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace HealthChecksSample
{
// Pass in `--scenario dbcontext` at the command line to run this sample.
public class DbContextHealthStartup
{
public DbContextHealthStartup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Registers required services for health checks
services.AddHealthChecks()
// Registers a health check for the MyContext type. By default the name of the health check will be the
// name of the DbContext type. There are other options available through AddDbContextCheck to configure
// failure status, tags, and custom test query.
.AddDbContextCheck<MyContext>();
// Registers the MyContext type and configures the database provider.
//
// The health check added by AddDbContextCheck will create instances of MyContext from the service provider,
// and so will reuse the configuration provided here.
services.AddDbContext<MyContext>(options =>
{
options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// This will register the health checks middleware at the URL /health.
//
// Since this sample doesn't do anything to create the database by default, this will
// return unhealthy by default.
//
// You can to to /createdatabase and /deletedatabase to create and delete the database
// (respectively), and see how it immediately effects the health status.
//
app.UseHealthChecks("/health");
app.Map("/createdatabase", b => b.Run(async (context) =>
{
await context.Response.WriteAsync("Creating the database...\n");
await context.Response.Body.FlushAsync();
var myContext = context.RequestServices.GetRequiredService<MyContext>();
await myContext.Database.EnsureCreatedAsync();
await context.Response.WriteAsync("Done\n");
await context.Response.WriteAsync("Go to /health to see the health status\n");
}));
app.Map("/deletedatabase", b => b.Run(async (context) =>
{
await context.Response.WriteAsync("Deleting the database...\n");
await context.Response.Body.FlushAsync();
var myContext = context.RequestServices.GetRequiredService<MyContext>();
await myContext.Database.EnsureDeletedAsync();
await context.Response.WriteAsync("Done\n");
await context.Response.WriteAsync("Go to /health to see the health status\n");
}));
app.Run(async (context) =>
{
await context.Response.WriteAsync("Go to /health to see the health status\n");
await context.Response.WriteAsync("Go to /createdatabase to create the database\n");
await context.Response.WriteAsync("Go to /deletedatabase to delete the database\n");
});
}
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
namespace HealthChecksSample
{
// This is an example of a custom health check that implements IHealthCheck.
//
// This example also shows a technique for authoring a health check that needs to be registered
// with additional configuration data. This technique works via named options, and is useful
// for authoring health checks that can be disctributed as libraries.
public static class GCInfoHealthCheckBuilderExtensions
{
public static IHealthChecksBuilder AddGCInfoCheck(
this IHealthChecksBuilder builder,
string name,
HealthStatus? failureStatus = null,
IEnumerable<string> tags = null,
long? thresholdInBytes = null)
{
// Register a check of type GCInfo
builder.AddCheck<GCInfoHealthCheck>(name, failureStatus ?? HealthStatus.Degraded, tags);
// Configure named options to pass the threshold into the check.
if (thresholdInBytes.HasValue)
{
builder.Services.Configure<GCInfoOptions>(name, options =>
{
options.Threshold = thresholdInBytes.Value;
});
}
return builder;
}
}
public class GCInfoHealthCheck : IHealthCheck
{
private readonly IOptionsMonitor<GCInfoOptions> _options;
public GCInfoHealthCheck(IOptionsMonitor<GCInfoOptions> options)
{
_options = options;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var options = _options.Get(context.Registration.Name);
// This example will report degraded status if the application is using
// more than the configured amount of memory (1gb by default).
//
// Additionally we include some GC info in the reported diagnostics.
var allocated = GC.GetTotalMemory(forceFullCollection: false);
var data = new Dictionary<string, object>()
{
{ "Allocated", allocated },
{ "Gen0Collections", GC.CollectionCount(0) },
{ "Gen1Collections", GC.CollectionCount(1) },
{ "Gen2Collections", GC.CollectionCount(2) },
};
// Report failure if the allocated memory is >= the threshold.
//
// Using context.Registration.FailureStatus means that the application developer can configure
// how they want failures to appear.
var result = allocated >= options.Threshold ? context.Registration.FailureStatus : HealthStatus.Healthy;
return Task.FromResult(new HealthCheckResult(
result,
description: "reports degraded status if allocated bytes >= 1gb",
data: data));
}
}
public class GCInfoOptions
{
// The failure threshold (in bytes)
public long Threshold { get; set; } = 1024L * 1024L * 1024L;
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<!-- Used in our tests -->
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">netcoreapp2.2;net461</TargetFrameworks>
<TargetFrameworks Condition="'$(TargetFrameworks)'==''">netcoreapp2.2</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<Reference Include="Microsoft.Extensions.Configuration.CommandLine" />
<Reference Include="Microsoft.Extensions.Configuration.Json" />
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
<Reference Include="Newtonsoft.Json" />
<Reference Include="System.Data.SqlClient" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,68 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace HealthChecksSample
{
// Pass in `--scenario liveness` at the command line to run this sample.
public class LivenessProbeStartup
{
public void ConfigureServices(IServiceCollection services)
{
// Registers required services for health checks
services
.AddHealthChecks()
.AddCheck<SlowDependencyHealthCheck>("Slow", failureStatus: null, tags: new[] { "ready", });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// This will register the health checks middleware twice:
// - at /health/ready for 'readiness'
// - at /health/live for 'liveness'
//
// Using a separate liveness and readiness check is useful in an environment like Kubernetes
// when an application needs to do significant work before accepting requests. Using separate
// checks allows the orchestrator to distinguish whether the application is functioning but
// not yet ready or if the application has failed to start.
//
// For instance the liveness check will do a quick set of checks to determine if the process
// is functioning correctly.
//
// The readiness check might do a set of more expensive or time-consuming checks to determine
// if all other resources are responding.
//
// See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/ for
// more details about readiness and liveness probes in Kubernetes.
//
// In this example, the liveness check will us an 'identity' check that always returns healthy.
//
// In this example, the readiness check will run all registered checks, include a check with a
// long initialization time (15 seconds).
// The readiness check uses all registered checks with the 'ready' tag.
app.UseHealthChecks("/health/ready", new HealthCheckOptions()
{
Predicate = (check) => check.Tags.Contains("ready"),
});
// The liveness filters out all checks and just returns success
app.UseHealthChecks("/health/live", new HealthCheckOptions()
{
// Exclude all checks, just return a 200.
Predicate = (check) => false,
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Go to /health/ready to see the readiness status");
await context.Response.WriteAsync(Environment.NewLine);
await context.Response.WriteAsync("Go to /health/live to see the liveness status");
});
}
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace HealthChecksSample
{
// Pass in `--scenario port` at the command line to run this sample.
public class ManagementPortStartup
{
public ManagementPortStartup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Registers required services for health checks
services.AddHealthChecks();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// This will register the health checks middleware at the URL /health but only on the specified port.
//
// By default health checks will return a 200 with 'Healthy'.
// - No health checks are registered by default, the app is healthy if it is reachable
// - The default response writer writes the HealthCheckStatus as text/plain content
//
// Use UseHealthChecks with a port will only process health checks requests on connection
// to the specified port. This is typically used in a container environment where you can expose
// a port for monitoring services to have access to the service.
// - In this case the management is configured in the launchSettings.json and passed through
// an environment variable
// - Additionally, the server is also configured to listen to requests on the management port.
app.UseHealthChecks("/health", port: Configuration["ManagementPort"]);
app.Run(async (context) =>
{
await context.Response.WriteAsync($"Go to http://localhost:{Configuration["ManagementPort"]}/health to see the health status");
});
}
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
namespace HealthChecksSample
{
public class MyContext : DbContext
{
public MyContext(DbContextOptions options)
: base(options)
{
}
public DbSet<Blog> Blog { get; set; }
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace HealthChecksSample
{
public class Program
{
private static readonly Dictionary<string, Type> _scenarios;
static Program()
{
_scenarios = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
{
{ "", typeof(BasicStartup) },
{ "basic", typeof(BasicStartup) },
{ "writer", typeof(CustomWriterStartup) },
{ "liveness", typeof(LivenessProbeStartup) },
{ "port", typeof(ManagementPortStartup) },
{ "db", typeof(DbHealthStartup) },
{ "dbcontext", typeof(DbContextHealthStartup) },
};
}
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables(prefix: "ASPNETCORE_")
.AddCommandLine(args)
.Build();
var scenario = config["scenario"] ?? string.Empty;
if (!_scenarios.TryGetValue(scenario, out var startupType))
{
startupType = typeof(BasicStartup);
}
return new WebHostBuilder()
.UseConfiguration(config)
.ConfigureLogging(builder =>
{
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddConfiguration(config);
builder.AddConsole();
})
.UseKestrel()
.UseStartup(startupType)
.Build();
}
}
}

View File

@ -0,0 +1,15 @@
{
"profiles": {
"HealthChecksSample": {
"commandName": "Project",
"commandLineArgs": "",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:5000/;http://localhost:5001/",
"ASPNETCORE_MANAGEMENTPORT": "5001"
},
"applicationUrl": "http://localhost:5000/"
}
}
}

View File

@ -0,0 +1,32 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace HealthChecksSample
{
// Simulates a health check for an application dependency that takes a while to initialize.
// This is part of the readiness/liveness probe sample.
public class SlowDependencyHealthCheck : IHealthCheck
{
public static readonly string HealthCheckName = "slow_dependency";
private readonly Task _task;
public SlowDependencyHealthCheck()
{
_task = Task.Delay(15 * 1000);
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
if (_task.IsCompleted)
{
return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready"));
}
return Task.FromResult(new HealthCheckResult(
status: context.Registration.FailureStatus,
description: "Dependency is still initializing"));
}
}
}

View File

@ -0,0 +1,25 @@
using System.Data.Common;
using System.Data.SqlClient;
namespace HealthChecksSample
{
public class SqlConnectionHealthCheck : DbConnectionHealthCheck
{
private static readonly string DefaultTestQuery = "Select 1";
public SqlConnectionHealthCheck(string connectionString)
: this(connectionString, testQuery: DefaultTestQuery)
{
}
public SqlConnectionHealthCheck(string connectionString, string testQuery)
: base(connectionString, testQuery ?? DefaultTestQuery)
{
}
protected override DbConnection CreateConnection(string connectionString)
{
return new SqlConnection(connectionString);
}
}
}

View File

@ -0,0 +1,13 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=HealthCheckSample;Trusted_Connection=True;MultipleActiveResultSets=true;ConnectRetryCount=0"
},
"Logging": {
"LogLevel": {
"Default": "Debug"
},
"Console": {
"IncludeScopes": "true"
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
</PropertyGroup>
<ItemGroup>