.NET CORE health check
原文:
Health checks in ASP.NET Core | Microsoft Learn
var builder = WebApplication.CreateBuilder(args); builder.Services.AddHealthChecks(); var app = builder.Build(); app.MapHealthChecks("/healthz"); app.Run();
docker
HEALTHCHECK CMD curl --fail http://localhost:5000/healthz || exit
自定义health check、
public class SampleHealthCheck : IHealthCheck
实战参考:Health Checks in ASP.Net Core - .Net Core Central (dotnetcorecentral.com)
Health checks are a critical part of any distributed system. Especially in the era of microservices, it is extremely important to understand if the service is running healthy or not. In this blog, I will walk through how to implement Health Checks in ASP.Net Core.
ASP.Net Core provides support for health checks out of the box through middleware and extension methods.
What are the different aspects of an application that we need through health checks?
- Firstly, finding out dependencies such as a database or other services are alive or not
- Secondly, the usage of physical resources on the machine
We usually use the health checks for monitoring applications and to see how the applications are behaving. The health check results are also in use for scaling out applications based on how the service is degrading in response time.
To further dig deeper into the topic let us start by creating an ASP.Net Core Web application. And let us demonstrate the feature.
Creating a new ASP.Net Core Web Application
First of all, we will create a new ASP.Net Core web application. To achieve that, I will open up Visual Studio 2019, and select the menu option File -> New -> Project.
Once the new project creation window pops up, I will select the ASP.Net Core Web Application. And then I will click on the Next button.
Secondly, on the next page of the pop-up, I will provide the name of the project in the Project Name field as HealthCheck.Demo. And then click on the Create button.
Finally, on the final page, I will select the API template option. And I will keep other values default (ASP.Net Core 3.1) and click on the Create button.
Setting up Health Checks in ASP.Net Core
Once the project is created, I will set up a basic health check. For that, I will start with the Startup class.
Change in ConfigureServices method
In the ConfigureServices
of the Startup
class, I will add the health check system into the dependency injections container. For that, I will call the AddHealthChecks
extension method on the IServiceCollection
instance. The method will return an instance of IHealthChecksBuilder
.
As the name suggests the IHealthChecksBuilder
provides methods to set up and chain health checks.
As a first step, I will call the AddCheck
method on the IHealthChecksBuilder
instance returned by the AddHealthChecks
.
The AddCheck
takes two parameters, the first one is a name for the health check. And the second one is a Func
delegate which returns the HealthCheckResult
struct.
For the time being, I will just return HealthCheckResult.Healthy
as the return.
1
2
|
services.AddHealthChecks() .AddCheck( "Test Health Check" , () => HealthCheckResult.Healthy()); |
Change in Configure method
Once the dependency injection container setup is complete, it is time to set up the health check middleware. For that, I will update the implementation of the UseEndpoints
extension method call to the IApplicationBuilder
instance.
Inside of the delegate call for the UseEndpoints
method parameter; I will call the MapHealthChecks
extension method on the IEndpointRouteBuilder
instance.
The MapHealthChecks
method takes the URL pattern as the parameter. I will pass api/health as the parameter value. You can use what suits your application.
Microsoft uses /health as the endpoint in all the examples of their documentation here.
1
2
3
4
5
|
app.UseEndpoints(endpoints => { endpoints.MapHealthChecks( "api/health" ); endpoints.MapControllers(); }); |
The complete change in code inside of the Startup
class is below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; namespace HealthCheck.Demo { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get ; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHealthChecks() .AddCheck( "Test Health Check" , () => HealthCheckResult.Healthy()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapHealthChecks( "api/health" ); endpoints.MapControllers(); }); } } } |
Running the application
Once the code is complete, I will run the application to test the health check endpoint.
After running the application, if I go to the /api/health endpoint I will see the response as Healthy.
Healthy Response
Using Multiple Health Checks Providers
Now, in a real-life scenario, we will never return a HealthCheckResult.Healthy
from the health checks return and call it a day. For health checks, we will try to validate multiple aspects of the application to see if they are working as expected.
For example, let us say we want to check the state of our database. And make sure that we are able to successfully connect to the database.
For that, we will create a new class, DbHealthCheckProvider
. The only responsibility of this class is to connect to the database. And make sure that the connection was successful.
I will create this class as a static class and it will have a single static method Check
. The method Check will take the connection string of the database. Now since that class will be called from the Startup
class, and will not be used anywhere, it is fair to do this. Also, we will never have to test this class, for obvious reasons; it does not add any value.
For this to work, I will install the NuGet package System.Data.SqlClient.
Inside of the Check
method, I will open a connection to my local SQL Server And if the connection was successful, I will return HealthCheckResult.Healthy
. Otherwise, I will return HealthCheckResult.Unhealthy
from the method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
using Microsoft.Extensions.Diagnostics.HealthChecks; using System.Data.SqlClient; namespace HealthCheck.Demo { public static class DbHealthCheckProvider { public static HealthCheckResult Check( string connectionString) { // Code to check if DB is running try { using var connection = new SqlConnection(connectionString); connection.Open(); return HealthCheckResult.Healthy(); } catch { return HealthCheckResult.Unhealthy(); } } } } |
Setting up Startup class
Once the DbHealthCheckProvider
class is ready, I will set up the Startup
to use this class.
For that, I will update the ConfigureServices
method. And I will change the delegate implementation inside the AddCheck
method. Instead of calling an inline delegate to return HealthCheckResult.Healthy
. Now, I will call the DbHealthCheckProvider.Check
.
And for the database connection string, I will pass an empty string. This should result in an Unhealthy
response when we run the application.
1
2
3
4
5
6
7
8
|
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHealthChecks() .AddCheck( "DB Health Check" , () => DbHealthCheckProvider.Check( "" )); } |
Once the code inside of Startup
is configured to use the new DbHealthCheckProvider
class, I will run the application.
Unhealthy Response
As expected, when we navigate to the /api/health endpoint, we can see an Unhealthy response back.
Console Response
In the above console response, we can see that the Health Checks system logs a fail in the console. This log can be used to identify what exactly failed. As you can see it prints the name of the health checks “DB Health Check”.
The HealthCheckResult.Unhealthy
or HealthCheckResult.Healthy
method takes an optional string description parameter. This is very handy when it comes to logging.
To demonstrate that I will update the implementation of the HealthCheckResult.Unhealthy
method inside of the DbHealthCheckProvider
. And I will add a description to the Unhealthy
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
using Microsoft.Extensions.Diagnostics.HealthChecks; using System.Data.SqlClient; namespace HealthCheck.Demo { public static class DbHealthCheckProvider { public static HealthCheckResult Check( string connectionString) { // Code to check if DB is running try { using var connection = new SqlConnection(connectionString); connection.Open(); return HealthCheckResult.Healthy(); } catch { return HealthCheckResult.Unhealthy( "Could not connect to database!" ); } } } } |
Now if I run the application and navigate to the /api/health endpoint. I will see the description text “Could not connect to database!” will print out in the console.
Unhealthy with description
Queue Health Checks Provider
Now, let us say we our application also connects to a RabbitMQ server for processing messages. Now to check if we are able to connect to RabbitMQ server, we will need to write a health check provider.
Putting all health checks inside of a single class makes things difficult to manage. As well as harder to find out what was the cause of the issue.
Hence, I will create a new class, MqHealthCheckProvider
. This will also be a static class with a single static method Check
. For this class, we will just return HealthCheckResult.Healthy
from the method, we will not implement any real RabbitMQ connectivity for the demo.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthCheck.Demo { public static class MqHealthCheckProvider { public static HealthCheckResult Check( string mqUri) { // Code to check if MQ is running return HealthCheckResult.Healthy(); } } } |
Once the MqHealthCheckProvider
is ready, I will update the Startup
class to configure the MqHealthCheckProvider
.
Since the AddCheck
method also returns IHealthChecksBuilder
instance, I will chain another AddCheck
to configure the MqHealthCheckProvider
.
And for this case as well when we call the Check
method of the MqHealthCheckProvider
class, we will pass the MQ Server string as an empty string.
1
2
3
4
5
6
7
8
9
10
|
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHealthChecks() .AddCheck( "DB Health Check" , () => DbHealthCheckProvider.Check( "" )) .AddCheck( "MQ Health Check" , () => MqHealthCheckProvider.Check( "" )); } |
Now when I run the application, I will still see the response as Unhealthy. This is because the health check is the aggregation of all the health checkpoints configured.
Hence, I will update the DbHealthCheckProvider.Check
call to pass a valid connection string to make that call as healthy as well.
I will get the connection string from the appsettings.json
file.
1
2
3
4
5
6
7
8
9
10
|
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHealthChecks() .AddCheck( "DB Health Check" , () => DbHealthCheckProvider.Check(Configuration[ "Connection" ])) .AddCheck( "MQ Health Check" , () => MqHealthCheckProvider.Check( "" )); } |
Now if I run the application, I will see Healthy in response.
Using Generic AddCheck method
The next thing I am going to walk through is how to use the generic AddCheck
method. For that let us consider our application is interacting with third part SAAS software like Sendgrid through an HTTP connection.
Firstly, I will create a new class that is responsible for checking if an HTTP request to Sendgrid is successful or not. I will name the class as SendgridHealthCheckProvider
.
This class will implement the interface IHealthCheck
, which is part of the namespace Microsoft.Extensions.Diagnostics.HealthChecks
. I will implement the method CheckHealthAsync
from the interface.
For the time being for the demo, I will return HealthCheckResult.Healthy
from the method. I will not make a real HTTP call outside.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
using Microsoft.Extensions.Diagnostics.HealthChecks; using System.Threading; using System.Threading.Tasks; namespace HealthCheck.Demo { public class SendgridHealthCheckProvider : IHealthCheck { public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default ) { return Task.FromResult(HealthCheckResult.Healthy()); } } } |
Once the SendgridHealthCheckProvider
is ready, I will go and update the Startup
class to configure it.
For the configuration, I will use the generic version of the AddCheck
method.
1
2
3
4
5
6
7
8
9
|
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHealthChecks() .AddCheck( "DB Health Check" , () => DbHealthCheckProvider.Check(Configuration[ "Connection" ])) .AddCheck( "MQ Health Check" , () => MqHealthCheckProvider.Check( "" )) .AddCheck<SendgridHealthCheckProvider>( "Sendgrid Health Check" ); } |
Finally, I will run the application. And as expected, I will get a Healthy response from the API call.
Implementing Degraded option
One of the options of the HealthCheckResult
struct that we have not tried so far is the Degraded
. Now the question is when we will use this option?
Whenever a database or some external service is taking more than the SLA, we can send a Degraded response. This can be achieved by using a timer to check the time taken by the external dependency.
To demonstrate that, let us update the MqHealthCheckProvider
class. Now in the Check
method, it will return HealthCheckResult.Degraded
.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthCheck.Demo { public static class MqHealthCheckProvider { public static HealthCheckResult Check( string mqUri) { // Code to check if MQ is running return HealthCheckResult.Degraded( "MQ is running slow!" ); } } } |
Now if we run the application, we can see that the response is Degraded.
Degraded response
And also as expected the console log will print a warning, along with the same of the health check. And the description message “MQ is running slow!”.
Degraded warning in console
Health Checks in ASP.Net Core through Postman
Now, the question is, how the monitoring tools will interpret this response. Well, the best way to implement that will be through checking the HTTP status code. And if needed further investigation then checking the response body.
Let us run the requests through the postman and see how it all works. So to test that, we will just run the application as it is running now. Which means it will return a Degraded response.
If we navigate to the /api/health, we will see the HTTP Status code is still 200, but the response body is Degraded.
Degraded in Postman
Now let us see how an Unhealthy response will look like in postman. Specifically what status code will it return.
For doing that, let us just update the Startup
classes ConfigureServices
method. And for the AddCheck
of the DbHealthCheckProvider
, let us pass an empty connection string. This will create an Unhealthy response.
1
2
3
4
5
6
7
8
9
|
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHealthChecks() .AddCheck( "DB Health Check" , () => DbHealthCheckProvider.Check( "" )) .AddCheck( "MQ Health Check" , () => MqHealthCheckProvider.Check( "" )) .AddCheck<SendgridHealthCheckProvider>( "Sendgrid Health Check" ); } |
Once I make this change, let us run the application and check the postman response.
Unhealthy response in Postman
In the response, we can see that the HTTP status code is 503 Service Unavailable. And the response string is Unhealthy. Based on the HTTP status code any motoring tool can find out if the service is up or down.
For a Healthy response, we will get an HTTP Status Code of 200 and Healthy in the response string.
Conclusion
The Health Checks in ASP.Net Core implementation is really awesome in my opinion. It is super simple to implement and integrate with individual implementation classes.
I have done a YouTube video going through the concepts here.
The source code is available in Github here.
参考:
Health monitoring | Microsoft Learn
Docker-HealthCheck指令探测ASP.NET Core容器健康状态-阿里云开发者社区 (aliyun.com)
使用ASP.NET Core实现Docker的HealthCheck指令 - 程序员大本营 (pianshen.com)
ASP.NET Core Health Checks Explained (elmah.io)
(142条消息) .NET Core 3.0之深入源码理解HealthCheck(一)_dotNET跨平台的博客-CSDN博客
(142条消息) .NET Core 3.1之深入源码理解HealthCheck(二)_dotNET跨平台的博客-CSDN博客
在.NET Core 中实现健康检查 - 腾讯云开发者社区-腾讯云 (tencent.com)
ASP.NET CORE在docker中的健康检查(healthcheck) - 波多尔斯基 - 博客园 (cnblogs.com)
ASP.NET CORE在docker中的健康检查(healthcheck)
在使用docker-compose的过程中,很多程序都提供了健康检查(healthcheck)的方法,通过健康检查,应用程序能够在确保其依赖的程序都已经启动的前提下启动,减少各种错误的发生,同时,合理设计的健康检查也能够提供给外界关于应用程序状态的一些信息。
大多数docker镜像的详细说明中,会交代如何进行健康检查,.NET 6也提供了文档。也有其他人写了文章说了更细节的内容。
实践一下,现在docker-compose里面是这样:
version: '3.7'
services:
postgres:
container_name: postgres-container
image: postgres:13
volumes:
- ./postgres_data:/var/lib/postgresql/data/pgdata
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: "123456"
PGDATA: "/var/lib/postgresql/data/pgdata"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- dotnet-network
dotnet-service:
image: dotnet-service:latest
container_name: dotnet-service-container
ports:
- 5000:5000
build:
context: ./TestWeb/
dockerfile: ./TestWeb/Dockerfile
environment:
# - ASPNETCORE_ENVIRONMENT=Docker
- ASPNETCORE_URLS=http://*:5000
- App__SelfUrl=http://*:5000
restart: on-failure
depends_on:
postgres:
condition: service_healthy
networks:
- dotnet-network
networks:
dotnet-network:
external: false
首先调整一下程序:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/healthz");
app.Run();
如果直接运行的话,web访问/healthz节点,那么可以页面将直接返回Healthy。看上去没有问题,那我们按照要求调整一下Dockerfile
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
HEALTHCHECK CMD curl --fail http://localhost:5000/healthz || exit
ENTRYPOINT ["dotnet", "TestWeb.dll"]
然后调整docker-compose.yml文件,让postgres启动依赖dotnet-service:
version: '3.7'
services:
postgres:
container_name: postgres-container
image: postgres:13
volumes:
- ./postgres_data:/var/lib/postgresql/data/pgdata
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: "123456"
PGDATA: "/var/lib/postgresql/data/pgdata"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
depends_on:
dotnet-service:
condition: service_healthy
networks:
- dotnet-network
dotnet-service:
image: dotnet-service:latest
container_name: dotnet-service-container
ports:
- 5000:5000
build:
context: ./TestWeb/
dockerfile: ./TestWeb/Dockerfile
environment:
# - ASPNETCORE_ENVIRONMENT=Docker
- ASPNETCORE_URLS=http://*:5000
- App__SelfUrl=http://*:5000
restart: on-failure
networks:
- dotnet-network
networks:
dotnet-network:
external: false
然后postgres就启动不了了,提示容器unhealthy。运行docker ps,发现.net容器的后面有一个unhealthy。但是程序访问正常,说明只是没有正确运行这段话而已。
Dockerfile方案#
文档说.NET 6的镜像里面不带curl这个工具了,需要手动装一下:
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
RUN apt-get update && apt-get install -y curl
HEALTHCHECK CMD curl --fail http://localhost:5000/healthz || exit
ENTRYPOINT ["dotnet", "TestWeb.dll"]
这样再看,就完全没问题了,如果把aspnet:6.0改成aspnet:6.0-alpine,空间更节省了(默认是239M,alpine版是108M),还自带了wget,也没必要这么麻烦先安装curl(比较费时间按)。
HEALTHCHECK CMD wget --spider http://localhost:5000/healthz || exit
docker-compose方案#
我不太喜欢去改Dockerfile,而是更倾向于修改docker-compose.yml文件。当然,没有curl依然还是得在Dockerfile中添加相应语句安装,但是可以删除HEALTHCHECK这一条了,直接在docker-compose.yml中对应服务下面添加:
healthcheck:
test: ["CMD-SHELL", "wget --spider http://localhost:5000/healthz || exit"]
interval: 10s
timeout: 5s
retries: 5
Health Checks in ASP.Net Core - .Net Core Central (dotnetcorecentral.com)