diff --git a/src/YhCore.WebApi/HealthChecks/DependencyHealthChecks.cs b/src/YhCore.WebApi/HealthChecks/DependencyHealthChecks.cs new file mode 100644 index 0000000..ab303b0 --- /dev/null +++ b/src/YhCore.WebApi/HealthChecks/DependencyHealthChecks.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using StackExchange.Redis; +using YhCore.Repository; + +namespace YhCore.WebApi.HealthChecks +{ + /// + /// 数据库连通性检查:复用已注册的 YHDbContext(随 DatabaseProvider 自动适配 SqlServer/MySql)。 + /// 每个检查自带 2 秒超时,防止连接池耗尽时探针自身挂死、反而探不出来。 + /// + public class DbContextHealthCheck : IHealthCheck + { + private readonly YHDbContext _dbContext; + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); + + public DbContextHealthCheck(YHDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var probe = Task.Run(async () => + { + await _dbContext.Database.OpenConnectionAsync(cancellationToken); + using var command = _dbContext.Database.GetDbConnection().CreateCommand(); + command.CommandText = "SELECT 1"; + await command.ExecuteScalarAsync(cancellationToken); + }, CancellationToken.None); + + var finished = await Task.WhenAny(probe, Task.Delay(Timeout, CancellationToken.None)); + if (finished != probe) + return HealthCheckResult.Unhealthy("database probe timed out after 2s"); + + await probe; // 传播异常 + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy("database probe failed: " + ex.Message, ex); + } + } + } + + /// + /// Redis 连通性检查:复用全局单例 IConnectionMultiplexer(YhCore.Common.Cache 注册)。 + /// + public class RedisHealthCheck : IHealthCheck + { + private readonly IConnectionMultiplexer _multiplexer; + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); + + public RedisHealthCheck(IConnectionMultiplexer multiplexer) + { + _multiplexer = multiplexer; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + if (!_multiplexer.IsConnected) + return HealthCheckResult.Unhealthy("redis connection is not established"); + + var probe = _multiplexer.GetDatabase().PingAsync(); + var finished = await Task.WhenAny(probe, Task.Delay(Timeout, CancellationToken.None)); + if (finished != probe) + return HealthCheckResult.Unhealthy("redis ping timed out after 2s"); + + var latency = await probe; + return HealthCheckResult.Healthy("ping " + latency); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy("redis probe failed: " + ex.Message, ex); + } + } + } +} diff --git a/src/YhCore.WebApi/Startup.cs b/src/YhCore.WebApi/Startup.cs index b486b13..ea5c05b 100644 --- a/src/YhCore.WebApi/Startup.cs +++ b/src/YhCore.WebApi/Startup.cs @@ -205,6 +205,9 @@ namespace YhCore.WebApi { endpoints.MapControllers(); + // 免认证健康探针:健康 200 / 不健康 503(容器与负载均衡用) + endpoints.MapHealthChecks("/health"); + // 根路径登录状态页:SSO 开启时未登录会先被中间件 302 到 CAS;登录后在此显示当前用户。 endpoints.MapGet("/", async context => {