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