Skip to content

Commit 697f349

Browse files
authored
Refactor healthchecks service mapping to support filtering on check (#2142)
1 parent ff1a07b commit 697f349

15 files changed

+449
-56
lines changed

src/Grpc.AspNetCore.HealthChecks/GrpcHealthChecksOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//

src/Grpc.AspNetCore.HealthChecks/GrpcHealthChecksPublisher.cs

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -16,32 +16,81 @@
1616

1717
#endregion
1818

19+
using System.Linq;
20+
using Grpc.Health.V1;
1921
using Grpc.HealthCheck;
2022
using Microsoft.Extensions.Diagnostics.HealthChecks;
23+
using Microsoft.Extensions.Logging;
2124
using Microsoft.Extensions.Options;
2225

2326
namespace Grpc.AspNetCore.HealthChecks;
2427

2528
internal sealed class GrpcHealthChecksPublisher : IHealthCheckPublisher
2629
{
2730
private readonly HealthServiceImpl _healthService;
31+
private readonly ILogger _logger;
2832
private readonly GrpcHealthChecksOptions _options;
2933

30-
public GrpcHealthChecksPublisher(HealthServiceImpl healthService, IOptions<GrpcHealthChecksOptions> options)
34+
public GrpcHealthChecksPublisher(HealthServiceImpl healthService, IOptions<GrpcHealthChecksOptions> options, ILoggerFactory loggerFactory)
3135
{
3236
_healthService = healthService;
3337
_options = options.Value;
38+
_logger = loggerFactory.CreateLogger<GrpcHealthChecksPublisher>();
3439
}
3540

3641
public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
3742
{
38-
foreach (var registration in _options.Services)
43+
Log.EvaluatingPublishedHealthReport(_logger, report.Entries.Count, _options.Services.Count);
44+
45+
foreach (var serviceMapping in _options.Services)
3946
{
40-
var resolvedStatus = HealthChecksStatusHelpers.GetStatus(report, registration.Predicate);
47+
IEnumerable<KeyValuePair<string, HealthReportEntry>> serviceEntries = report.Entries;
48+
49+
if (serviceMapping.HealthCheckPredicate != null)
50+
{
51+
serviceEntries = serviceEntries.Where(entry =>
52+
{
53+
var context = new HealthCheckMapContext(entry.Key, entry.Value.Tags);
54+
return serviceMapping.HealthCheckPredicate(context);
55+
});
56+
}
57+
58+
#pragma warning disable CS0618 // Type or member is obsolete
59+
if (serviceMapping.Predicate != null)
60+
{
61+
serviceEntries = serviceEntries.Where(entry =>
62+
{
63+
var result = new HealthResult(entry.Key, entry.Value.Tags, entry.Value.Status, entry.Value.Description, entry.Value.Duration, entry.Value.Exception, entry.Value.Data);
64+
return serviceMapping.Predicate(result);
65+
});
66+
}
67+
#pragma warning restore CS0618 // Type or member is obsolete
68+
69+
var (resolvedStatus, resultCount) = HealthChecksStatusHelpers.GetStatus(serviceEntries);
4170

42-
_healthService.SetStatus(registration.Name, resolvedStatus);
71+
Log.ServiceMappingStatusUpdated(_logger, serviceMapping.Name, resolvedStatus, resultCount);
72+
_healthService.SetStatus(serviceMapping.Name, resolvedStatus);
4373
}
4474

4575
return Task.CompletedTask;
4676
}
77+
78+
private static class Log
79+
{
80+
private static readonly Action<ILogger, int, int, Exception?> _evaluatingPublishedHealthReport =
81+
LoggerMessage.Define<int, int>(LogLevel.Trace, new EventId(1, "EvaluatingPublishedHealthReport"), "Evaluating {HealthReportEntryCount} published health report entries against {ServiceMappingCount} service mappings.");
82+
83+
private static readonly Action<ILogger, string, HealthCheckResponse.Types.ServingStatus, int, Exception?> _serviceMappingStatusUpdated =
84+
LoggerMessage.Define<string, HealthCheckResponse.Types.ServingStatus, int>(LogLevel.Debug, new EventId(2, "ServiceMappingStatusUpdated"), "Service '{ServiceName}' status updated to {Status}. {EntriesCount} health report entries evaluated.");
85+
86+
public static void EvaluatingPublishedHealthReport(ILogger logger, int healthReportEntryCount, int serviceMappingCount)
87+
{
88+
_evaluatingPublishedHealthReport(logger, healthReportEntryCount, serviceMappingCount, null);
89+
}
90+
91+
public static void ServiceMappingStatusUpdated(ILogger logger, string serviceName, HealthCheckResponse.Types.ServingStatus status, int entriesCount)
92+
{
93+
_serviceMappingStatusUpdated(logger, serviceName, status, entriesCount, null);
94+
}
95+
}
4796
}

src/Grpc.AspNetCore.HealthChecks/GrpcHealthChecksServiceExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -79,7 +79,7 @@ private static IHealthChecksBuilder AddGrpcHealthChecksCore(IServiceCollection s
7979
services.Configure<GrpcHealthChecksOptions>(options =>
8080
{
8181
// Add default registration that uses all results for default service: ""
82-
options.Services.MapService(string.Empty, r => true);
82+
options.Services.Map(string.Empty, r => true);
8383
});
8484

8585
return services.AddHealthChecks();
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
namespace Grpc.AspNetCore.HealthChecks;
20+
21+
/// <summary>
22+
/// Context used to map health check registrations to a service.
23+
/// </summary>
24+
public sealed class HealthCheckMapContext
25+
{
26+
/// <summary>
27+
/// Creates a new instance of <see cref="HealthCheckMapContext"/>.
28+
/// </summary>
29+
/// <param name="name">The health check name.</param>
30+
/// <param name="tags">Tags associated with the health check.</param>
31+
public HealthCheckMapContext(string name, IEnumerable<string> tags)
32+
{
33+
Name = name;
34+
Tags = tags;
35+
}
36+
37+
/// <summary>
38+
/// Gets the health check name.
39+
/// </summary>
40+
public string Name { get; }
41+
42+
/// <summary>
43+
/// Gets the tags associated with the health check.
44+
/// </summary>
45+
public IEnumerable<string> Tags { get; }
46+
}

src/Grpc.AspNetCore.HealthChecks/HealthResult.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -23,6 +23,7 @@ namespace Grpc.AspNetCore.HealthChecks;
2323
/// <summary>
2424
/// Represents the result of a single <see cref="IHealthCheck"/>.
2525
/// </summary>
26+
[Obsolete($"HealthResult is obsolete and will be removed in a future release. Use {nameof(HealthCheckMapContext)} instead.")]
2627
public sealed class HealthResult
2728
{
2829
/// <summary>
Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -16,32 +16,35 @@
1616

1717
#endregion
1818

19-
using Grpc.AspNetCore.HealthChecks;
2019
using Grpc.Health.V1;
2120
using Microsoft.Extensions.Diagnostics.HealthChecks;
2221

2322
internal static class HealthChecksStatusHelpers
2423
{
25-
public static HealthCheckResponse.Types.ServingStatus GetStatus(HealthReport report, Func<HealthResult, bool> predicate)
24+
public static (HealthCheckResponse.Types.ServingStatus status, int resultCount) GetStatus(IEnumerable<KeyValuePair<string, HealthReportEntry>> results)
2625
{
27-
var filteredResults = report.Entries
28-
.Select(entry => new HealthResult(entry.Key, entry.Value.Tags, entry.Value.Status, entry.Value.Description, entry.Value.Duration, entry.Value.Exception, entry.Value.Data))
29-
.Where(predicate);
30-
26+
var resultCount = 0;
3127
var resolvedStatus = HealthCheckResponse.Types.ServingStatus.Unknown;
32-
foreach (var result in filteredResults)
28+
foreach (var result in results)
3329
{
34-
if (result.Status == HealthStatus.Unhealthy)
35-
{
36-
resolvedStatus = HealthCheckResponse.Types.ServingStatus.NotServing;
30+
resultCount++;
3731

38-
// No point continuing to check statuses.
39-
break;
32+
// NotServing is a final status but keep iterating to discover how many results are being evaluated.
33+
if (resolvedStatus == HealthCheckResponse.Types.ServingStatus.NotServing)
34+
{
35+
continue;
4036
}
4137

42-
resolvedStatus = HealthCheckResponse.Types.ServingStatus.Serving;
38+
if (result.Value.Status == HealthStatus.Unhealthy)
39+
{
40+
resolvedStatus = HealthCheckResponse.Types.ServingStatus.NotServing;
41+
}
42+
else
43+
{
44+
resolvedStatus = HealthCheckResponse.Types.ServingStatus.Serving;
45+
}
4346
}
4447

45-
return resolvedStatus;
48+
return (resolvedStatus, resultCount);
4649
}
4750
}

src/Grpc.AspNetCore.HealthChecks/Internal/HealthServiceIntegration.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -74,8 +74,35 @@ private async Task<HealthCheckResponse> GetHealthCheckResponseAsync(string servi
7474
HealthCheckResponse.Types.ServingStatus status;
7575
if (_grpcHealthCheckOptions.Services.TryGetServiceMapping(service, out var serviceMapping))
7676
{
77-
var result = await _healthCheckService.CheckHealthAsync(_healthCheckOptions.Predicate, cancellationToken);
78-
status = HealthChecksStatusHelpers.GetStatus(result, serviceMapping.Predicate);
77+
var result = await _healthCheckService.CheckHealthAsync((HealthCheckRegistration registration) =>
78+
{
79+
if (_healthCheckOptions.Predicate != null && !_healthCheckOptions.Predicate(registration))
80+
{
81+
return false;
82+
}
83+
84+
if (serviceMapping.HealthCheckPredicate != null && !serviceMapping.HealthCheckPredicate(new HealthCheckMapContext(registration.Name, registration.Tags)))
85+
{
86+
return false;
87+
}
88+
89+
return true;
90+
}, cancellationToken);
91+
92+
IEnumerable<KeyValuePair<string, HealthReportEntry>> serviceEntries = result.Entries;
93+
94+
#pragma warning disable CS0618 // Type or member is obsolete
95+
if (serviceMapping.Predicate != null)
96+
{
97+
serviceEntries = serviceEntries.Where(entry =>
98+
{
99+
var result = new HealthResult(entry.Key, entry.Value.Tags, entry.Value.Status, entry.Value.Description, entry.Value.Duration, entry.Value.Exception, entry.Value.Data);
100+
return serviceMapping.Predicate(result);
101+
});
102+
}
103+
#pragma warning restore CS0618 // Type or member is obsolete
104+
105+
(status, _) = HealthChecksStatusHelpers.GetStatus(serviceEntries);
79106
}
80107
else
81108
{
@@ -128,7 +155,7 @@ public async Task WriteAsync(HealthCheckResponse message)
128155
_receivedFirstWrite = true;
129156
message = await _service.GetHealthCheckResponseAsync(_request.Service, throwOnNotFound: false, _cancellationToken);
130157
}
131-
158+
132159
await _innerResponseStream.WriteAsync(message);
133160
}
134161
}

src/Grpc.AspNetCore.HealthChecks/ServiceMapping.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -28,19 +28,59 @@ public sealed class ServiceMapping
2828
/// </summary>
2929
/// <param name="name">The service name.</param>
3030
/// <param name="predicate">The predicate used to filter <see cref="HealthResult"/> instances. These results determine service health.</param>
31+
[Obsolete("This constructor is obsolete and will be removed in the future. Use ServiceMapping(string name, Func<HealthCheckRegistration, bool> predicate) to map service names to .NET health checks.")]
3132
public ServiceMapping(string name, Func<HealthResult, bool> predicate)
3233
{
3334
Name = name;
3435
Predicate = predicate;
3536
}
3637

38+
/// <summary>
39+
/// Creates a new instance of <see cref="ServiceMapping"/>.
40+
/// </summary>
41+
/// <param name="name">The service name.</param>
42+
/// <param name="predicate">
43+
/// The predicate used to filter health checks when the <c>Health</c> service <c>Check</c> and <c>Watch</c> methods are called.
44+
/// <para>
45+
/// The <c>Health</c> service methods have different behavior:
46+
/// </para>
47+
/// <list type="bullet">
48+
/// <item><description><c>Check</c> uses the predicate to determine which health checks are run for a service.</description></item>
49+
/// <item><description><c>Watch</c> periodically runs all health checks. The predicate filters the health results for a service.</description></item>
50+
/// </list>
51+
/// <para>
52+
/// The health result for the service is based on the health check results.
53+
/// </para>
54+
/// </param>
55+
public ServiceMapping(string name, Func<HealthCheckMapContext, bool> predicate)
56+
{
57+
Name = name;
58+
HealthCheckPredicate = predicate;
59+
}
60+
3761
/// <summary>
3862
/// Gets the service name.
3963
/// </summary>
4064
public string Name { get; }
4165

66+
/// <summary>
67+
/// Gets the predicate used to filter health checks when the <c>Health</c> service <c>Check</c> and <c>Watch</c> methods are called.
68+
/// <para>
69+
/// The <c>Health</c> service methods have different behavior:
70+
/// </para>
71+
/// <list type="bullet">
72+
/// <item><description><c>Check</c> uses the predicate to determine which health checks are run for a service.</description></item>
73+
/// <item><description><c>Watch</c> periodically runs all health checks. The predicate filters the health results for a service.</description></item>
74+
/// </list>
75+
/// <para>
76+
/// The health result for the service is based on the health check results.
77+
/// </para>
78+
/// </summary>
79+
public Func<HealthCheckMapContext, bool>? HealthCheckPredicate { get; }
80+
4281
/// <summary>
4382
/// Gets the predicate used to filter <see cref="HealthResult"/> instances. These results determine service health.
4483
/// </summary>
45-
public Func<HealthResult, bool> Predicate { get; }
84+
[Obsolete($"This member is obsolete and will be removed in the future. Use {nameof(HealthCheckPredicate)} to map service names to .NET health checks.")]
85+
public Func<HealthResult, bool>? Predicate { get; }
4686
}

0 commit comments

Comments
 (0)