Skip to content

Commit 8c29229

Browse files
committed
feat(health): 添加健康检查端点支持存活和就绪探针
添加健康检查功能,包含存活探针(/health/live)和就绪探针(/health/ready)端点,用于容器编排和负载均衡健康监控。存活探针仅检查进程运行状态,就绪探针会检查FreeSql业务库和SQLite日志数据库等关键依赖。同时提供汇总端点(/health, /healthz)并返回标准化的JSON响应格式。
1 parent ddfe827 commit 8c29229

6 files changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System.Text.Json;
2+
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
3+
using Microsoft.Extensions.Diagnostics.HealthChecks;
4+
using StarBlog.Api.HealthChecks;
5+
6+
namespace StarBlog.Api.Extensions;
7+
8+
public static class ConfigureHealthChecks {
9+
/// <summary>
10+
/// 为 StarBlog.Api 注册健康检查(Health Checks)。
11+
/// - Liveness:仅用于判断进程是否还“活着”(不依赖外部资源)
12+
/// - Readiness:用于判断服务是否“就绪”(会检查关键依赖是否可用,如数据库)
13+
/// </summary>
14+
public static void AddStarBlogHealthChecks(this IServiceCollection services) {
15+
services
16+
.AddHealthChecks()
17+
.AddCheck<LiveHealthCheck>(
18+
name: "live",
19+
tags: new[] { HealthCheckTags.Live }
20+
)
21+
.AddCheck<SqliteLogHealthCheck>(
22+
name: "sqlite-log",
23+
tags: new[] { HealthCheckTags.Ready }
24+
)
25+
.AddCheck<FreeSqlHealthCheck>(
26+
name: "freesql",
27+
tags: new[] { HealthCheckTags.Ready }
28+
);
29+
}
30+
31+
/// <summary>
32+
/// 映射健康检查端点:
33+
/// - /health/live:存活探针
34+
/// - /health/ready:就绪探针
35+
/// - /health、/healthz:汇总探针(兼容别名)
36+
/// </summary>
37+
public static WebApplication MapStarBlogHealthChecks(this WebApplication app) {
38+
// HealthChecks:最常见的三种探针(以及 /healthz 兼容别名)
39+
app.MapHealthChecks(
40+
"/health/live",
41+
CreateHealthCheckOptions(r => r.Tags.Contains(HealthCheckTags.Live))
42+
)
43+
.AllowAnonymous();
44+
45+
app.MapHealthChecks(
46+
"/health/ready",
47+
CreateHealthCheckOptions(r => r.Tags.Contains(HealthCheckTags.Ready))
48+
)
49+
.AllowAnonymous();
50+
51+
app.MapHealthChecks(
52+
"/health",
53+
CreateHealthCheckOptions(_ => true)
54+
)
55+
.AllowAnonymous();
56+
57+
app.MapHealthChecks(
58+
"/healthz",
59+
CreateHealthCheckOptions(_ => true)
60+
)
61+
.AllowAnonymous();
62+
63+
return app;
64+
}
65+
66+
/// <summary>
67+
/// 统一的 HealthCheckOptions,输出 JSON,并按健康状态返回 HTTP 状态码。
68+
/// </summary>
69+
public static HealthCheckOptions CreateHealthCheckOptions(Func<HealthCheckRegistration, bool> predicate) {
70+
return new HealthCheckOptions {
71+
Predicate = predicate,
72+
AllowCachingResponses = false,
73+
ResultStatusCodes = {
74+
[HealthStatus.Healthy] = StatusCodes.Status200OK,
75+
[HealthStatus.Degraded] = StatusCodes.Status200OK,
76+
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
77+
},
78+
ResponseWriter = WriteResponseAsync
79+
};
80+
}
81+
82+
private static Task WriteResponseAsync(HttpContext context, HealthReport report) {
83+
context.Response.ContentType = "application/json; charset=utf-8";
84+
85+
var payload = new {
86+
status = report.Status.ToString(),
87+
totalDurationMs = report.TotalDuration.TotalMilliseconds,
88+
checks = report.Entries.Select(entry => new {
89+
name = entry.Key,
90+
status = entry.Value.Status.ToString(),
91+
durationMs = entry.Value.Duration.TotalMilliseconds,
92+
description = entry.Value.Description,
93+
error = entry.Value.Exception?.Message,
94+
data = entry.Value.Data.Count == 0 ? null : entry.Value.Data
95+
})
96+
};
97+
98+
return context.Response.WriteAsync(JsonSerializer.Serialize(payload));
99+
}
100+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using FreeSql;
2+
using Microsoft.Extensions.Diagnostics.HealthChecks;
3+
4+
namespace StarBlog.Api.HealthChecks;
5+
6+
/// <summary>
7+
/// Readiness:检查 FreeSql 业务库是否可用。
8+
/// 这里使用最轻量的探测语句(select 1),用于验证连接与执行通道正常。
9+
/// </summary>
10+
public sealed class FreeSqlHealthCheck : IHealthCheck {
11+
private readonly IFreeSql _freeSql;
12+
13+
public FreeSqlHealthCheck(IFreeSql freeSql) {
14+
_freeSql = freeSql;
15+
}
16+
17+
public Task<HealthCheckResult> CheckHealthAsync(
18+
HealthCheckContext context,
19+
CancellationToken cancellationToken = default
20+
) {
21+
try {
22+
_freeSql.Ado.ExecuteScalar("select 1");
23+
return Task.FromResult(HealthCheckResult.Healthy("FreeSql database is reachable."));
24+
}
25+
catch (Exception ex) {
26+
return Task.FromResult(HealthCheckResult.Unhealthy("FreeSql database check failed.", ex));
27+
}
28+
}
29+
}
30+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace StarBlog.Api.HealthChecks;
2+
3+
/// <summary>
4+
/// Health Check 标签:
5+
/// - Live:存活探针(不检查外部依赖)
6+
/// - Ready:就绪探针(检查关键依赖)
7+
/// </summary>
8+
public static class HealthCheckTags {
9+
public const string Live = "live";
10+
public const string Ready = "ready";
11+
}
12+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Microsoft.Extensions.Diagnostics.HealthChecks;
2+
3+
namespace StarBlog.Api.HealthChecks;
4+
5+
/// <summary>
6+
/// 存活探针:只要进程能响应 HTTP,就返回 Healthy。
7+
/// 适合容器/负载均衡用于判断服务是否需要重启(不依赖数据库/缓存等外部资源)。
8+
/// </summary>
9+
public sealed class LiveHealthCheck : IHealthCheck {
10+
public Task<HealthCheckResult> CheckHealthAsync(
11+
HealthCheckContext context,
12+
CancellationToken cancellationToken = default
13+
) {
14+
return Task.FromResult(HealthCheckResult.Healthy("Service is running."));
15+
}
16+
}
17+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.Extensions.Diagnostics.HealthChecks;
2+
using StarBlog.Data;
3+
4+
namespace StarBlog.Api.HealthChecks;
5+
6+
/// <summary>
7+
/// Readiness:检查 EF Core 的 SQLite-Log(访问统计库)是否可连接。
8+
/// </summary>
9+
public sealed class SqliteLogHealthCheck : IHealthCheck {
10+
private readonly AppDbContext _dbContext;
11+
12+
public SqliteLogHealthCheck(AppDbContext dbContext) {
13+
_dbContext = dbContext;
14+
}
15+
16+
public async Task<HealthCheckResult> CheckHealthAsync(
17+
HealthCheckContext context,
18+
CancellationToken cancellationToken = default
19+
) {
20+
try {
21+
var canConnect = await _dbContext.Database.CanConnectAsync(cancellationToken);
22+
return canConnect
23+
? HealthCheckResult.Healthy("SQLite-Log database is reachable.")
24+
: HealthCheckResult.Degraded("SQLite-Log database is not reachable (degraded).");
25+
}
26+
catch (Exception ex) {
27+
return HealthCheckResult.Unhealthy("SQLite-Log database check failed.", ex);
28+
}
29+
}
30+
}

src/StarBlog.Api/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959
builder.Services.AddVisitRecord();
6060
builder.Services.AddHttpClient();
6161

62+
// HealthChecks:提供 /health(汇总)、/health/live(存活)、/health/ready(就绪)端点
63+
builder.Services.AddStarBlogHealthChecks();
64+
6265
// CORS:Next.js 前端跨域调用需要(带 Cookie/凭据时必须显式列出允许的 Origin)
6366
builder.Services.AddCors(options => {
6467
options.AddDefaultPolicy(policyBuilder => {
@@ -146,6 +149,8 @@
146149
// Swagger UI 默认需要已认证用户才能访问(避免生产环境直接暴露文档)
147150
app.UseSwaggerPkg();
148151

152+
app.MapStarBlogHealthChecks();
153+
149154
app.MapControllers();
150155

151156
app.Run();

0 commit comments

Comments
 (0)