Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor healthchecks service mapping to support filtering on check #2142

Merged
merged 5 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down
59 changes: 54 additions & 5 deletions src/Grpc.AspNetCore.HealthChecks/GrpcHealthChecksPublisher.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand All @@ -16,32 +16,81 @@

#endregion

using System.Linq;
using Grpc.Health.V1;
using Grpc.HealthCheck;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Grpc.AspNetCore.HealthChecks;

internal sealed class GrpcHealthChecksPublisher : IHealthCheckPublisher
{
private readonly HealthServiceImpl _healthService;
private readonly ILogger _logger;
private readonly GrpcHealthChecksOptions _options;

public GrpcHealthChecksPublisher(HealthServiceImpl healthService, IOptions<GrpcHealthChecksOptions> options)
public GrpcHealthChecksPublisher(HealthServiceImpl healthService, IOptions<GrpcHealthChecksOptions> options, ILoggerFactory loggerFactory)
{
_healthService = healthService;
_options = options.Value;
_logger = loggerFactory.CreateLogger<GrpcHealthChecksPublisher>();
}

public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
{
foreach (var registration in _options.Services)
Log.EvaluatingPublishedHealthReport(_logger, report.Entries.Count, _options.Services.Count);

foreach (var serviceMapping in _options.Services)
{
var resolvedStatus = HealthChecksStatusHelpers.GetStatus(report, registration.Predicate);
IEnumerable<KeyValuePair<string, HealthReportEntry>> serviceEntries = report.Entries;

if (serviceMapping.HealthCheckPredicate != null)
{
serviceEntries = serviceEntries.Where(entry =>
{
var context = new HealthCheckMapContext(entry.Key, entry.Value.Tags);
return serviceMapping.HealthCheckPredicate(context);
});
}

#pragma warning disable CS0618 // Type or member is obsolete
if (serviceMapping.Predicate != null)
{
serviceEntries = serviceEntries.Where(entry =>
{
var result = new HealthResult(entry.Key, entry.Value.Tags, entry.Value.Status, entry.Value.Description, entry.Value.Duration, entry.Value.Exception, entry.Value.Data);
return serviceMapping.Predicate(result);
});
}
#pragma warning restore CS0618 // Type or member is obsolete

var (resolvedStatus, resultCount) = HealthChecksStatusHelpers.GetStatus(serviceEntries);

_healthService.SetStatus(registration.Name, resolvedStatus);
Log.ServiceMappingStatusUpdated(_logger, serviceMapping.Name, resolvedStatus, resultCount);
_healthService.SetStatus(serviceMapping.Name, resolvedStatus);
}

return Task.CompletedTask;
}

private static class Log
{
private static readonly Action<ILogger, int, int, Exception?> _evaluatingPublishedHealthReport =
LoggerMessage.Define<int, int>(LogLevel.Trace, new EventId(1, "EvaluatingPublishedHealthReport"), "Evaluating {HealthReportEntryCount} published health report entries against {ServiceMappingCount} service mappings.");

private static readonly Action<ILogger, string, HealthCheckResponse.Types.ServingStatus, int, Exception?> _serviceMappingStatusUpdated =
LoggerMessage.Define<string, HealthCheckResponse.Types.ServingStatus, int>(LogLevel.Debug, new EventId(2, "ServiceMappingStatusUpdated"), "Service '{ServiceName}' status updated to {Status}. {EntriesCount} health report entries evaluated.");

public static void EvaluatingPublishedHealthReport(ILogger logger, int healthReportEntryCount, int serviceMappingCount)
{
_evaluatingPublishedHealthReport(logger, healthReportEntryCount, serviceMappingCount, null);
}

public static void ServiceMappingStatusUpdated(ILogger logger, string serviceName, HealthCheckResponse.Types.ServingStatus status, int entriesCount)
{
_serviceMappingStatusUpdated(logger, serviceName, status, entriesCount, null);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

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

return services.AddHealthChecks();
Expand Down
46 changes: 46 additions & 0 deletions src/Grpc.AspNetCore.HealthChecks/HealthCheckMapContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

namespace Grpc.AspNetCore.HealthChecks;

/// <summary>
/// Context used to map health check registrations to a service.
/// </summary>
public sealed class HealthCheckMapContext
{
/// <summary>
/// Creates a new instance of <see cref="HealthCheckMapContext"/>.
/// </summary>
/// <param name="name">The health check name.</param>
/// <param name="tags">Tags associated with the health check.</param>
public HealthCheckMapContext(string name, IEnumerable<string> tags)
{
Name = name;
Tags = tags;
}

/// <summary>
/// Gets the health check name.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the tags associated with the health check.
/// </summary>
public IEnumerable<string> Tags { get; }
}
3 changes: 2 additions & 1 deletion src/Grpc.AspNetCore.HealthChecks/HealthResult.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand All @@ -23,6 +23,7 @@ namespace Grpc.AspNetCore.HealthChecks;
/// <summary>
/// Represents the result of a single <see cref="IHealthCheck"/>.
/// </summary>
[Obsolete($"HealthResult is obsolete and will be removed in a future release. Use {nameof(HealthCheckMapContext)} instead.")]
public sealed class HealthResult
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand All @@ -16,32 +16,35 @@

#endregion

using Grpc.AspNetCore.HealthChecks;
using Grpc.Health.V1;
using Microsoft.Extensions.Diagnostics.HealthChecks;

internal static class HealthChecksStatusHelpers
{
public static HealthCheckResponse.Types.ServingStatus GetStatus(HealthReport report, Func<HealthResult, bool> predicate)
public static (HealthCheckResponse.Types.ServingStatus status, int resultCount) GetStatus(IEnumerable<KeyValuePair<string, HealthReportEntry>> results)
{
var filteredResults = report.Entries
.Select(entry => new HealthResult(entry.Key, entry.Value.Tags, entry.Value.Status, entry.Value.Description, entry.Value.Duration, entry.Value.Exception, entry.Value.Data))
.Where(predicate);

var resultCount = 0;
var resolvedStatus = HealthCheckResponse.Types.ServingStatus.Unknown;
foreach (var result in filteredResults)
foreach (var result in results)
{
if (result.Status == HealthStatus.Unhealthy)
{
resolvedStatus = HealthCheckResponse.Types.ServingStatus.NotServing;
resultCount++;

// No point continuing to check statuses.
break;
// NotServing is a final status but keep iterating to discover how many results are being evaluated.
if (resolvedStatus == HealthCheckResponse.Types.ServingStatus.NotServing)
{
continue;
}

resolvedStatus = HealthCheckResponse.Types.ServingStatus.Serving;
if (result.Value.Status == HealthStatus.Unhealthy)
{
resolvedStatus = HealthCheckResponse.Types.ServingStatus.NotServing;
}
else
{
resolvedStatus = HealthCheckResponse.Types.ServingStatus.Serving;
}
}

return resolvedStatus;
return (resolvedStatus, resultCount);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -74,8 +74,35 @@ private async Task<HealthCheckResponse> GetHealthCheckResponseAsync(string servi
HealthCheckResponse.Types.ServingStatus status;
if (_grpcHealthCheckOptions.Services.TryGetServiceMapping(service, out var serviceMapping))
{
var result = await _healthCheckService.CheckHealthAsync(_healthCheckOptions.Predicate, cancellationToken);
status = HealthChecksStatusHelpers.GetStatus(result, serviceMapping.Predicate);
var result = await _healthCheckService.CheckHealthAsync((HealthCheckRegistration registration) =>
{
if (_healthCheckOptions.Predicate != null && !_healthCheckOptions.Predicate(registration))
{
return false;
}

if (serviceMapping.HealthCheckPredicate != null && !serviceMapping.HealthCheckPredicate(new HealthCheckMapContext(registration.Name, registration.Tags)))
{
return false;
}

return true;
}, cancellationToken);

IEnumerable<KeyValuePair<string, HealthReportEntry>> serviceEntries = result.Entries;

#pragma warning disable CS0618 // Type or member is obsolete
if (serviceMapping.Predicate != null)
{
serviceEntries = serviceEntries.Where(entry =>
{
var result = new HealthResult(entry.Key, entry.Value.Tags, entry.Value.Status, entry.Value.Description, entry.Value.Duration, entry.Value.Exception, entry.Value.Data);
return serviceMapping.Predicate(result);
});
}
#pragma warning restore CS0618 // Type or member is obsolete

(status, _) = HealthChecksStatusHelpers.GetStatus(serviceEntries);
}
else
{
Expand Down Expand Up @@ -128,7 +155,7 @@ public async Task WriteAsync(HealthCheckResponse message)
_receivedFirstWrite = true;
message = await _service.GetHealthCheckResponseAsync(_request.Service, throwOnNotFound: false, _cancellationToken);
}

await _innerResponseStream.WriteAsync(message);
}
}
Expand Down
44 changes: 42 additions & 2 deletions src/Grpc.AspNetCore.HealthChecks/ServiceMapping.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -28,19 +28,59 @@ public sealed class ServiceMapping
/// </summary>
/// <param name="name">The service name.</param>
/// <param name="predicate">The predicate used to filter <see cref="HealthResult"/> instances. These results determine service health.</param>
[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.")]
public ServiceMapping(string name, Func<HealthResult, bool> predicate)
{
Name = name;
Predicate = predicate;
}

/// <summary>
/// Creates a new instance of <see cref="ServiceMapping"/>.
/// </summary>
/// <param name="name">The service name.</param>
/// <param name="predicate">
/// The predicate used to filter health checks when the <c>Health</c> service <c>Check</c> and <c>Watch</c> methods are called.
/// <para>
/// The <c>Health</c> service methods have different behavior:
/// </para>
/// <list type="bullet">
/// <item><description><c>Check</c> uses the predicate to determine which health checks are run for a service.</description></item>
/// <item><description><c>Watch</c> periodically runs all health checks. The predicate filters the health results for a service.</description></item>
/// </list>
/// <para>
/// The health result for the service is based on the health check results.
/// </para>
/// </param>
public ServiceMapping(string name, Func<HealthCheckMapContext, bool> predicate)
{
Name = name;
HealthCheckPredicate = predicate;
}

/// <summary>
/// Gets the service name.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the predicate used to filter health checks when the <c>Health</c> service <c>Check</c> and <c>Watch</c> methods are called.
/// <para>
/// The <c>Health</c> service methods have different behavior:
/// </para>
/// <list type="bullet">
/// <item><description><c>Check</c> uses the predicate to determine which health checks are run for a service.</description></item>
/// <item><description><c>Watch</c> periodically runs all health checks. The predicate filters the health results for a service.</description></item>
/// </list>
/// <para>
/// The health result for the service is based on the health check results.
/// </para>
/// </summary>
public Func<HealthCheckMapContext, bool>? HealthCheckPredicate { get; }

/// <summary>
/// Gets the predicate used to filter <see cref="HealthResult"/> instances. These results determine service health.
/// </summary>
public Func<HealthResult, bool> Predicate { get; }
[Obsolete($"This member is obsolete and will be removed in the future. Use {nameof(HealthCheckPredicate)} to map service names to .NET health checks.")]
public Func<HealthResult, bool>? Predicate { get; }
}
Loading