C#健康检查中如何获取HttpContext?多端点场景异常排查
ASP.NET Core自定义健康检查中获取调用端点上下文的问题
核心问题
在ASP.NET Core中实现自定义健康检查时,已在实现IHealthCheck的类中注入IHttpContextAccessor,依赖解析正常,但访问其HttpContext属性始终为空。当前定义了多个健康检查端点,需要在检查逻辑中识别当前调用的是哪个端点,请问如何让HttpContext可访问,或通过其他方式获取调用端点的上下文?
补充说明
添加Debug.WriteLine()输出HttpContext中的路径后发现:
- 访问
/healthz端点时偶尔能输出路径,但也会出现空值; - 访问
/healthz/live-probe或/healthz/readiness-probe端点时,路径始终为空,且输出时机与请求发起时间不匹配。
推测健康检查可能在后台循环执行,端点仅返回最新检查结果,但无法解释/healthz端点的偶发现象,除官方文档外,是否有其他文档可解释该行为?
相关代码实现
健康检查端点扩展方法
public static IApplicationBuilder UseHealthCheckEndpoint(this IApplicationBuilder app) => app.UseEndpoints(endpoints => { endpoints.MapHealthChecks("/healthz", new HealthCheckOptions { Predicate = _ => true, ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, ResultStatusCodes = Statuses }); endpoints.MapHealthChecks("/healthz/live-probe", new HealthCheckOptions { Predicate = check => check.Tags.Contains(LivenessTag), ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, ResultStatusCodes = Statuses }); endpoints.MapHealthChecks("/healthz/ready-probe", new HealthCheckOptions { Predicate = check => check.Tags.Contains(ReadinessTag), ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, ResultStatusCodes = Statuses }); });
微服务添加健康检查的扩展方法
public static IHealthChecksBuilder AddHealthCheckUrls(this IHealthChecksBuilder healthChecksBuilder, string name, HealthStatus? failureStatus = null, IEnumerable<string> tags = null, TimeSpan? timeout = null, HttpMethod httpMethod = null, params string[] urls) { return healthChecksBuilder.Add(new HealthCheckRegistration( name: name, factory: s => new UrlHealthCheck(s.GetService<IHttpClientFactory>(), s.GetService<IHttpContextAccessor>(), httpMethod, failureStatus.HasValue, urls), failureStatus: failureStatus, tags: new List<string> { "urls" }.Concat(tags ?? Enumerable.Empty<string>()), timeout: timeout)); }
自定义健康检查实现类
internal class UrlHealthCheck : IHealthCheck { private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpContextAccessor _httpContextAccessor; private readonly string[] _urls; private readonly HttpMethod _httpMethod = HttpMethod.Head; private readonly bool _hasUserFailureStatus; public UrlHealthCheck(bool hasUserFailureStatus) => _hasUserFailureStatus = hasUserFailureStatus; public UrlHealthCheck(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor, HttpMethod httpMethod, bool hasUserFailureStatus, params string[] urls) { if ((urls?.Length ?? 0) == 0) throw new ArgumentException("Urls cannot be empty", nameof(urls)); _urls = urls; _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory), "IHttpClientFactory could not be resolved. Ensure you are calling 'services.AddHttpClient();' from your ConfigureServices method."); _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor), "IHttpContextAccessor could not be resolved. Ensure you are calling 'services.AddHttpContextAccessor();' from your ConfigureServices method."); _httpMethod = httpMethod ?? HttpMethod.Head; _hasUserFailureStatus = hasUserFailureStatus; } public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new()) { var isLivenessProbe = _httpContextAccessor?.HttpContext?.Request?.Path.Value?.Contains("live-probe") ?? false; var isReadinessProbe = !isLivenessProbe && (_httpContextAccessor?.HttpContext?.Request?.Path.Value?.Contains("ready-probe") ?? false); var defaultFailureStatus = isReadinessProbe ? HealthStatus.Degraded : HealthStatus.Unhealthy; var failureStatus = _hasUserFailureStatus ? context.Registration.FailureStatus : defaultFailureStatus; try { // Prevent infinite dependency loops. var depthCount = 0; StringValues dependencyDepthCount = default; _httpContextAccessor?.HttpContext?.Request?.Query.TryGetValue(nameof(dependencyDepthCount), out dependencyDepthCount); if (dependencyDepthCount.Count > 0 && int.TryParse(dependencyDepthCount[0], out depthCount)) { if (depthCount > 5) { return new HealthCheckResult(HealthStatus.Healthy, "Dependency depth count exceeded"); } } using var client = _httpClientFactory.CreateClient(); var tasks = _urls.Select(url => { var uriBuilder = new UriBuilder(url); var query = HttpUtility.ParseQueryString(uriBuilder.Query); query["dependencyDepthCount"] = (depthCount + 1).ToString(); uriBuilder.Query = query.ToString(); var request = new HttpRequestMessage(_httpMethod, uriBuilder.Uri); return client.SendAsync(request, cancellationToken); }); var result = await Task.WhenAll(tasks); return result.All(x => x.IsSuccessStatusCode) ? new HealthCheckResult(HealthStatus.Healthy) : new HealthCheckResult(failureStatus, "One or more requests failed", data: new Dictionary<string, object>(result.Where(r => !r.IsSuccessStatusCode).Select(r => new KeyValuePair<string, object>(r.RequestMessage.RequestUri.AbsolutePath, r.ReasonPhrase)))); } catch (Exception ex) { return new HealthCheckResult(failureStatus, exception: ex); } } }
微服务注册健康检查代码
services.AddOurCompanyHealthChecks<UserServiceContext>(tags: new string[] { "User Microservice" }) .AddHealthCheckUrls("Some dependency", tags: new string[] { HealthCheckBuilderExtensions.ReadinessTag }, urls: $"{Configuration["Services:UserService:BaseUrl"]}/healthz");
解决方案
1. 为什么HttpContext为空?
健康检查有两种执行模式:
- 按需执行:用户访问健康检查端点时实时执行,此时
HttpContext可用; - 后台定期执行:若配置了后台服务(如通过健康检查UI搭配定时任务),检查会在后台线程执行,无关联
HttpContext,因此IHttpContextAccessor获取到空值。
/healthz偶尔能获取到HttpContext,是因为某次请求触发了按需执行,空值则是后台定期执行的检查结果被返回。
2. 正确识别调用端点的方案
不要依赖HttpContext,推荐以下两种方案:
方案一:利用健康检查标签直接判断
你已经给不同端点配置了对应标签的Predicate,当前执行的检查必然包含对应标签,可直接通过context.Registration.Tags判断:
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new()) { var isLivenessProbe = context.Registration.Tags.Contains(LivenessTag); var isReadinessProbe = context.Registration.Tags.Contains(ReadinessTag); var defaultFailureStatus = isReadinessProbe ? HealthStatus.Degraded : HealthStatus.Unhealthy; var failureStatus = _hasUserFailureStatus ? context.Registration.FailureStatus : defaultFailureStatus; // 后续逻辑保持不变 }
方案二:通过HealthCheckOptions传递自定义参数
修改端点配置,给每个端点添加标识参数:
endpoints.MapHealthChecks("/healthz/live-probe", new HealthCheckOptions { Predicate = check => check.Tags.Contains(LivenessTag), ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, ResultStatusCodes = Statuses, AdditionalData = new Dictionary<string, object> { { "ProbeType", "Liveness" } } }); endpoints.MapHealthChecks("/healthz/ready-probe", new HealthCheckOptions { Predicate = check => check.Tags.Contains(ReadinessTag), ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, ResultStatusCodes = Statuses, AdditionalData = new Dictionary<string, object> { { "ProbeType", "Readiness" } } });
然后在检查逻辑中获取参数:
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new()) { string probeType = null; if (context.Registration.Options is HealthCheckOptions options && options.AdditionalData.TryGetValue("ProbeType", out var value)) { probeType = value.ToString(); } var isLivenessProbe = probeType == "Liveness"; var isReadinessProbe = probeType == "Readiness"; // 后续逻辑保持不变 }
3. 禁用后台执行(可选)
如果不需要后台定期执行检查,可在注册时禁用:
services.AddHealthChecks() .AddCheck<UrlHealthCheck>("url-check", tags: new[] { "readiness" }, options: new HealthCheckRegistrationOptions { DisableBackgroundExecution = true });
内容的提问来源于stack exchange,提问作者adam0101
相关产品推荐
相关产品推荐

