Skip to content

Commit

Permalink
Merge pull request #607 from microsoft/maxdac/feature/add-endpoint-he…
Browse files Browse the repository at this point in the history
…alth-check-extensions

[Health Check] Added default Endpoint HTTP Health Check
  • Loading branch information
MaxDac authored Sep 7, 2023
2 parents 7eb744c + 3159906 commit 0005150
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Omex.Extensions.Abstractions;
using Microsoft.Omex.Extensions.Diagnostics.HealthChecks.Composables;

namespace Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore;

/// <summary>
/// This health check will perform a liveness call to the endpoint identified by the given parameters.
/// </summary>
internal class EndpointLivenessHealthCheck : IHealthCheck
{
private readonly EndpointLivenessHealthCheckParameters m_parameters;
private readonly IHealthCheck m_healthCheck;

/// <summary>
/// Constructor.
/// </summary>
public EndpointLivenessHealthCheck(
IHttpClientFactory httpClientFactory,
ActivitySource activitySource,
ILogger<EndpointLivenessHealthCheck> logger,
EndpointLivenessHealthCheckParameters parameters)
{
m_parameters = parameters;
m_healthCheck = HealthCheckComposablesExtensions.CreateLivenessHttpHealthCheck(
CreateHttpRequestMessage,
activitySource,
httpClientFactory,
httpClientName: parameters.HttpClientLogicalName)
.AsObservableHealthCheck(activitySource, logger, parameters: parameters);
}

/// <inheritdoc />
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) =>
m_healthCheck.CheckHealthAsync(context, cancellationToken);

private HttpRequestMessage CreateHttpRequestMessage()
{
int port = SfConfigurationProvider.GetEndpointPort(m_parameters.EndpointName);
UriBuilder uriBuilder = new(Uri.UriSchemeHttp, m_parameters.Host, port, m_parameters.EndpointRelativeUri);
return new HttpRequestMessage(HttpMethod.Get, uriBuilder.Uri);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Collections.Generic;
using System.Net.Http;

namespace Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore;

/// <summary>
/// Expands the <see cref="HealthCheckParameters"/> class wih additional parameters required by the
/// <see cref="EndpointLivenessHealthCheck"/> class.
/// </summary>
public class EndpointLivenessHealthCheckParameters : HealthCheckParameters
{
/// <summary>
/// The Service Fabric endpoint name, it will be used to fetch the service port.
/// </summary>
public string EndpointName { get; }

/// <summary>
/// The endpoint relative url at which the Service Fabric health check will be reachable.
/// </summary>
public string EndpointRelativeUri { get; }

/// <summary>
/// The name of the <seealso cref="HttpClient"/> that will be used to create the instance used
/// by the health check from <seealso cref="IHttpClientFactory"/>.
/// </summary>
public string HttpClientLogicalName { get; }

/// <summary>
/// The name of the service host that will be used to perform the health check HTTP call,
/// usually equal to `localhost`.
/// </summary>
public string Host { get; }

/// <summary>
/// Constructor.
/// </summary>
/// <param name="endpointName">The Service Fabric endpoint name.</param>
/// <param name="httpClientLogicalName">The name of the <seealso cref="HttpClient"/> defined in the consumer service DI.</param>
/// <param name="endpointRelativeUrl">
/// The endpoint relative url at which the Service Fabric health check will be reachable.
/// </param>
/// <param name="host">The host used to perform the health check HTTP call to the service.</param>
/// <param name="reportData">The report data.</param>
public EndpointLivenessHealthCheckParameters(
string endpointName,
string httpClientLogicalName,
string endpointRelativeUrl,
string host = "localhost",
params KeyValuePair<string, object>[] reportData)
: base(reportData)
{
EndpointName = endpointName;
HttpClientLogicalName = httpClientLogicalName;
EndpointRelativeUri = endpointRelativeUrl;
Host = host;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (C) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore;
Expand Down Expand Up @@ -34,4 +36,64 @@ public static IHealthCheck CreateLivenessHttpHealthCheck(
httpClientFactory,
httpClientName: httpClientName,
activityMarker: activity => activity?.MarkAsLivenessCheck());

/// <summary>
/// Adds the endpoint health check to the project.
/// </summary>
/// <param name="healthChecksBuilder">The health check builder.</param>
/// <param name="name">The health check name.</param>
/// <param name="failureStatus">
/// The health check failure status that will be reported if the health check fails.
/// </param>
/// <param name="parameters">
/// The health check parameters. Notice that these parameters will have to be an instance of the <see cref="EndpointLivenessHealthCheckParameters"/> class.
/// </param>
/// <returns>The health check builder.</returns>
public static IHealthChecksBuilder AddEndpointHttpHealthCheck(
this IHealthChecksBuilder healthChecksBuilder,
string name,
HealthStatus? failureStatus,
EndpointLivenessHealthCheckParameters parameters) =>
healthChecksBuilder
.AddTypeActivatedCheck<EndpointLivenessHealthCheck>(
name,
failureStatus,
parameters);

/// <summary>
/// Adds the endpoint helath check in the project.
/// This overload will build the custom Health Check parameters with the parameters provided in input.
/// The order of the input parameters is the same as the legacy extension method.
/// </summary>
/// <param name="healthChecksBuilder">The HealthChecks builder.</param>
/// <param name="name">The Health Check name.</param>
/// <param name="endpointName">The Service Fabric endpoint name.</param>
/// <param name="relativePath">The relative path of the endpoint to check.</param>
/// <param name="httpClientLogicalName">
/// The HTTP Client logical name. It must be the same as the one registered in the DI.
/// </param>
/// <param name="failureStatus">The status that will be reported if the Health Check fails.</param>
/// <param name="host">The service host.</param>
/// <param name="reportData">The report data parameters.</param>
/// <returns>The Health Check builder.</returns>
public static IHealthChecksBuilder AddEndpointHttpHealthCheck(
this IHealthChecksBuilder healthChecksBuilder,
string name,
string endpointName,
string relativePath,
string httpClientLogicalName,
HealthStatus failureStatus = HealthStatus.Unhealthy,
string host = "localhost",
params KeyValuePair<string, object>[] reportData)
{
EndpointLivenessHealthCheckParameters parameters = new(
endpointName,
httpClientLogicalName,
relativePath,
host,
reportData
);

return healthChecksBuilder.AddEndpointHttpHealthCheck(name, failureStatus, parameters);
}
}
24 changes: 24 additions & 0 deletions src/Diagnostics.HealthChecks.AspNetCore/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,27 @@ The liveness health check is a special type of health check that limits its prob
- The Health Check implementor will call the `CreateLivenessHttpHealthCheck` method in the [`HealthCheckComposablesExtensions`](./HealthCheckComposablesExtensions.cs) class. This factory method will create a default Http Health Check, but it will tweak the Activity by injecting the `activityMarker` function in the constructor, adding a key to the `Activity` baggage.
- The Activity will be sent to the endpoint.
- The endpoint will have to be marked with the [`LivenessCheckActionFilterAttribute`](./LivenessCheckActionFilterAttribute.cs). This filter will check whether there's an `Activity` injected in the request, and whether the `Activity` has the required key in its baggage: if that is the case, the endpoint execution will be short-circuited, returning an HTTP 200 status code, otherwise the method will be executed normally.

### Extension methods

The project offers extension methods to easily register liveness health checks on endpoints that can be specified with one line of configuration. The extension methods are located in the `HealthCheckComposableExtensions`, and they will extend the functionality on the `IHealthCheckBuilder` class available in the DI.

#### AddEndpointHttpHealthCheck

There are two overloads of the `HealthCheckComposableExtensions.AddEndpointHttpHealthCheck`. The first one allows the developer to specify directly the custom `HttpClientParameters` class called `EndpointLivenessHealthCheckParameters`: this class contains all the required information on how to build the HTTP Request to query the endpoint to check.

The second overload has been implemented to offer a similar functionality to the one available using the legacy health checks in the `HealthCheckComposablesExtensions` class: in this overload, all the information previously included in the `EndpointLivenessHealthCheckParameters` class can be defined manually.

For instance, in the following code it is shown how to define an HTTP Health Check on the `/healthz` endpoint using the legacy extension method:

```csharp
services.AddServiceFabricHealthChecks().AddHttpEndpointCheck("healthz", endpointName, "/healthz");
```

Using the new extension method, the same functionality can be achieved with the following call:

```csharp
services.AddServiceFabricHealthChecks().AddEndpointHttpHealthCheck("healthz", endpointName, "/healthz", "httpClientLogicalName");
```

The only difference is the specification of the `HttpClient` logical name, as the definition of it will now be a responsibility of the DI configuration.
4 changes: 2 additions & 2 deletions src/Diagnostics.HealthChecks/HealthChecksBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static class HealthChecksBuilderExtensions
/// <param name="failureStatus">status that should be reported when the health check reports a failure, if the provided value is null, Unhealthy will be reported Unhealthy</param>
/// <param name="additionalCheck">action that would be called after getting response, function should return new result object that would be reported</param>
/// <param name="reportData">additional properties that will be attached to health check result, for example escalation info</param>
[Obsolete("This method is deprecated and will be removed in a later release, please use HealthCheckComposablesExtensions class extension methods to compose health checks.")]
[Obsolete("This method is deprecated and will be removed in a later release, please use HealthCheckComposablesExtensions.AddEndpointHttpHealthCheck in the Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore package instead.")]
public static IHealthChecksBuilder AddHttpEndpointCheck(
this IHealthChecksBuilder builder,
string name,
Expand Down Expand Up @@ -98,7 +98,7 @@ public static IHealthChecksBuilder AddHttpEndpointCheck(
/// <param name="failureStatus">status that should be reported when the health check reports a failure, if the provided value is null, Unhealthy will be reported Unhealthy</param>
/// <param name="additionalCheck">action that would be called after getting response, function should return new result object that would be reported</param>
/// <param name="reportData">additional properties that will be attached to health check result, for example escalation info</param>
[Obsolete("This method is deprecated and will be removed in a later release, please use HealthCheckComposablesExtensions class extension methods to compose health checks.")]
[Obsolete("This method is deprecated and will be removed in a later release, please use HealthCheckComposablesExtensions.AddEndpointHttpHealthCheck in the Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore package instead.")]
public static IHealthChecksBuilder AddHttpEndpointCheck(
this IHealthChecksBuilder builder,
string name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore;
using Microsoft.Omex.Extensions.Diagnostics.HealthChecks.UnitTests.Composables;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Microsoft.OMEX.ClusterDiagnostics.Service.Tests.HealthChecks;

[TestClass]
public class HealthzEndpointHealthCheckTests
{
private static ILogger<T> GetLogger<T>() => new NullLogger<T>();

private const string RelativeUri = "/healthz";
private const string HttpClientLogicalName = "HealthzEndpointHttpHealthCheckHttpClient";

[TestInitialize]
public void Setup() => Environment.SetEnvironmentVariable($"Fabric_Endpoint_{nameof(EndpointLivenessHealthCheck)}", "1234");

[TestMethod]
[DataRow(HttpStatusCode.OK, HealthStatus.Healthy)]
[DataRow(HttpStatusCode.InternalServerError, HealthStatus.Unhealthy)]
[DataRow(HttpStatusCode.NotFound, HealthStatus.Unhealthy)]
[DataRow(HttpStatusCode.Unauthorized, HealthStatus.Unhealthy)]
[DataRow(HttpStatusCode.Forbidden, HealthStatus.Unhealthy)]
public async Task HealthzEndpointHttpHealthCheck_ReturnsExpectedStatus(HttpStatusCode returnedStatusCode, HealthStatus expectedHealthStatus)
{
EndpointLivenessHealthCheckParameters healthCheckParameters = new(
nameof(EndpointLivenessHealthCheck),
$"{nameof(EndpointLivenessHealthCheck)}_HttpClient",
"healthz");

HealthCheckTestHelpers.SetLocalServiceInfo();
Mock<IHttpClientFactory> clientFactory = HealthCheckTestHelpers.GetHttpClientFactoryMock(
HealthCheckTestHelpers.GetHttpResponseMessageMock(returnedStatusCode, message: string.Empty));

ActivitySource activitySourceMock = new(nameof(EndpointLivenessHealthCheck));

IHealthCheck healthCheck = new EndpointLivenessHealthCheck(
clientFactory.Object,
activitySourceMock,
GetLogger<EndpointLivenessHealthCheck>(),
healthCheckParameters);

CancellationTokenSource source = new();

HealthCheckResult healthCheckResult = await healthCheck.CheckHealthAsync(
HealthCheckTestHelpers.GetHealthCheckContext(healthCheck),
source.Token);

Assert.AreEqual(expectedHealthStatus, healthCheckResult.Status);
}

[TestMethod]
[DataRow(HttpStatusCode.OK, HealthStatus.Healthy)]
[DataRow(HttpStatusCode.InternalServerError, HealthStatus.Unhealthy)]
[DataRow(HttpStatusCode.NotFound, HealthStatus.Unhealthy)]
[DataRow(HttpStatusCode.Unauthorized, HealthStatus.Unhealthy)]
[DataRow(HttpStatusCode.Forbidden, HealthStatus.Unhealthy)]
public async Task HealthzEndpointHttpHealthCheck_ReturnsCorrectErrorMessageAndException(HttpStatusCode returnedStatusCode, HealthStatus expectedHealthStatus)
{
EndpointLivenessHealthCheckParameters healthCheckParameters = new(
nameof(EndpointLivenessHealthCheck),
$"{nameof(EndpointLivenessHealthCheck)}_HttpClient",
"healthz");

HealthCheckTestHelpers.SetLocalServiceInfo();
Mock<IHttpClientFactory> clientFactory = HealthCheckTestHelpers.GetHttpClientFactoryMock(
HealthCheckTestHelpers.GetHttpResponseMessageMock(returnedStatusCode, message: string.Empty),
shouldThrowException: true);

ActivitySource activitySourceMock = new(nameof(EndpointLivenessHealthCheck));

IHealthCheck healthCheck = new EndpointLivenessHealthCheck(
clientFactory.Object,
activitySourceMock,
GetLogger<EndpointLivenessHealthCheck>(),
healthCheckParameters);

CancellationTokenSource source = new();

HealthCheckResult healthCheckResult = await healthCheck.CheckHealthAsync(
HealthCheckTestHelpers.GetHealthCheckContext(healthCheck),
source.Token);

Assert.AreEqual(expectedHealthStatus, healthCheckResult.Status);

if (expectedHealthStatus != HealthStatus.Healthy)
{
Assert.IsTrue(healthCheckResult.Description?.Contains(returnedStatusCode.ToString(), StringComparison.InvariantCulture));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
/// <param name="numberOfFailuresBeforeOk">The number of failures before returning the response.</param>
/// <param name="shouldThrowException">Whether the HttpClient should throw an exception.</param>
/// <returns>The Http Client factory mock.</returns>
internal static Mock<IHttpClientFactory> GetHttpClientFactoryMock(
public static Mock<IHttpClientFactory> GetHttpClientFactoryMock(
HttpResponseMessage message,
int? numberOfFailuresBeforeOk = null,
bool? shouldThrowException = false)
Expand Down Expand Up @@ -124,7 +124,7 @@ internal static HttpRequestMessage GetHttpRequestMessageMock(string? uri = null)
/// <param name="statusCode">The response status code.</param>
/// <param name="message">The response raw body string.</param>
/// <returns>The response.</returns>
internal static HttpResponseMessage GetHttpResponseMessageMock(HttpStatusCode statusCode, string message) =>
public static HttpResponseMessage GetHttpResponseMessageMock(HttpStatusCode statusCode, string message) =>
new(statusCode)
{
Content = new StringContent(message)
Expand All @@ -137,7 +137,7 @@ internal static HttpResponseMessage GetHttpResponseMessageMock(HttpStatusCode st
/// <summary>
/// Creates the local SF service info mock.
/// </summary>
internal static void SetLocalServiceInfo()
public static void SetLocalServiceInfo()
{
const string PublishAddressEvnVariableName = "Fabric_NodeIPOrFQDN";
const string EndpointPortEvnVariableSuffix = "Fabric_Endpoint_";
Expand Down

0 comments on commit 0005150

Please sign in to comment.