Merge the release/2.2 branch of Diagnostics
This commit is contained in:
commit
925746158e
|
|
@ -33,6 +33,7 @@
|
||||||
<MicrosoftAspNetCoreTestingPackageVersion>2.2.0</MicrosoftAspNetCoreTestingPackageVersion>
|
<MicrosoftAspNetCoreTestingPackageVersion>2.2.0</MicrosoftAspNetCoreTestingPackageVersion>
|
||||||
<MicrosoftEntityFrameworkCoreInMemoryPackageVersion>2.2.0</MicrosoftEntityFrameworkCoreInMemoryPackageVersion>
|
<MicrosoftEntityFrameworkCoreInMemoryPackageVersion>2.2.0</MicrosoftEntityFrameworkCoreInMemoryPackageVersion>
|
||||||
<MicrosoftEntityFrameworkCorePackageVersion>2.2.0</MicrosoftEntityFrameworkCorePackageVersion>
|
<MicrosoftEntityFrameworkCorePackageVersion>2.2.0</MicrosoftEntityFrameworkCorePackageVersion>
|
||||||
|
<MicrosoftEntityFrameworkCoreRelationalPackageVersion>2.2.0</MicrosoftEntityFrameworkCoreRelationalPackageVersion>
|
||||||
<MicrosoftEntityFrameworkCoreSqlServerPackageVersion>2.2.0</MicrosoftEntityFrameworkCoreSqlServerPackageVersion>
|
<MicrosoftEntityFrameworkCoreSqlServerPackageVersion>2.2.0</MicrosoftEntityFrameworkCoreSqlServerPackageVersion>
|
||||||
<MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>2.2.0</MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>
|
<MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>2.2.0</MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsCachingAbstractionsPackageVersion>2.2.0</MicrosoftExtensionsCachingAbstractionsPackageVersion>
|
<MicrosoftExtensionsCachingAbstractionsPackageVersion>2.2.0</MicrosoftExtensionsCachingAbstractionsPackageVersion>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
<LatestPackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(MicrosoftEntityFrameworkCoreInMemoryPackageVersion)" />
|
<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.SqlServer" Version="$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftEntityFrameworkCorePackageVersion)" />
|
<LatestPackageReference Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftEntityFrameworkCorePackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.ActivatorUtilities.Sources" Version="$(MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.ActivatorUtilities.Sources" Version="$(MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion)" />
|
||||||
|
|
|
||||||
|
|
@ -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.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.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.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.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" />
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.WebSockets" ProjectPath="$(RepositoryRoot)src\Middleware\WebSockets\src\Microsoft.AspNetCore.WebSockets.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<PackageTags>aspnetcore;testing</PackageTags>
|
<PackageTags>aspnetcore;testing</PackageTags>
|
||||||
<EnableApiCheck>false</EnableApiCheck>
|
<EnableApiCheck>false</EnableApiCheck>
|
||||||
<UseLatestPackageReferences>true</UseLatestPackageReferences>
|
<UseLatestPackageReferences>true</UseLatestPackageReferences>
|
||||||
|
<UseProjectReferences>true</UseProjectReferences>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
<SignAssembly>false</SignAssembly>
|
<SignAssembly>false</SignAssembly>
|
||||||
<PublicSign>false</PublicSign>
|
|
||||||
<!-- Mitigation for long path issues -->
|
|
||||||
<AssemblyName>Diagnostics.FunctionalTests</AssemblyName>
|
<AssemblyName>Diagnostics.FunctionalTests</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@
|
||||||
<EmbeddedResource Include="Resources\**" Exclude="$(DefaultItemExcludes)" />
|
<EmbeddedResource Include="Resources\**" Exclude="$(DefaultItemExcludes)" />
|
||||||
<Content Include="TestFiles\**\*" CopyToOutputDirectory="PreserveNewest" />
|
<Content Include="TestFiles\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\testassets\ClassLibraryWithPortablePdbs\ClassLibraryWithPortablePdbs.csproj" />
|
<ProjectReference Include="..\testassets\ClassLibraryWithPortablePdbs\ClassLibraryWithPortablePdbs.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
|
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
|
||||||
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
|
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
|
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
|
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
|
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
|
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||||
|
"Types": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net461;netcoreapp2.0;netcoreapp2.1</TargetFrameworks>
|
<TargetFrameworks>net461;netcoreapp2.2</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue