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

Porting of composable Health Checks #591

Merged
merged 8 commits into from
Aug 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
9 changes: 6 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ ClientBin/
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

Expand Down Expand Up @@ -319,7 +319,7 @@ __pycache__/
# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
Expand All @@ -328,5 +328,8 @@ ASALocalRun/
# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Disabling VSCode files
.vscode/
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
<ItemGroup Label="Package Versions. AutoUpdate">
<PackageVersion Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageVersion Include="MSTest.TestFramework" Version="3.1.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc" Version="2.1.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Core" Version="2.1.16" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Csharp" Version="4.6.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.10" />
Expand Down
24 changes: 23 additions & 1 deletion Omex.sln
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
Directory.Packages.props = Directory.Packages.props
global.json = global.json
NuGet.Config = NuGet.Config
Packages.props = Packages.props
README.md = README.md
ship.snk = ship.snk
EndProjectSection
Expand Down Expand Up @@ -70,6 +70,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Omex.Extensions.A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Omex.Extensions.ServiceFabricGuest.Abstractions", "src\ServiceFabricGuest.Abstractions\Microsoft.Omex.Extensions.ServiceFabricGuest.Abstractions.csproj", "{A07BF64C-1B19-4972-83DB-B22BFF26E6B0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore", "src\Diagnostics.HealthChecks.AspNetCore\Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore.csproj", "{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore.UnitTests", "tests\Diagnostics.HealthChecks.AspNetCore.UnitTests\Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore.UnitTests.csproj", "{43CE835D-5A71-4689-9297-942EF1233175}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -246,6 +250,22 @@ Global
{A07BF64C-1B19-4972-83DB-B22BFF26E6B0}.Release|Any CPU.Build.0 = Release|Any CPU
{A07BF64C-1B19-4972-83DB-B22BFF26E6B0}.Release|x64.ActiveCfg = Release|Any CPU
{A07BF64C-1B19-4972-83DB-B22BFF26E6B0}.Release|x64.Build.0 = Release|Any CPU
{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD}.Debug|x64.ActiveCfg = Debug|Any CPU
{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD}.Debug|x64.Build.0 = Debug|Any CPU
{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD}.Release|Any CPU.Build.0 = Release|Any CPU
{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD}.Release|x64.ActiveCfg = Release|Any CPU
{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD}.Release|x64.Build.0 = Release|Any CPU
{43CE835D-5A71-4689-9297-942EF1233175}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43CE835D-5A71-4689-9297-942EF1233175}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43CE835D-5A71-4689-9297-942EF1233175}.Debug|x64.ActiveCfg = Debug|Any CPU
{43CE835D-5A71-4689-9297-942EF1233175}.Debug|x64.Build.0 = Debug|Any CPU
{43CE835D-5A71-4689-9297-942EF1233175}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43CE835D-5A71-4689-9297-942EF1233175}.Release|Any CPU.Build.0 = Release|Any CPU
{43CE835D-5A71-4689-9297-942EF1233175}.Release|x64.ActiveCfg = Release|Any CPU
{43CE835D-5A71-4689-9297-942EF1233175}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -272,6 +292,8 @@ Global
{8520AA74-FDA7-4EC0-B60D-C9A786574443} = {3249ADDB-50EC-4C21-A8F0-65EF444662EE}
{E9123DDF-3D31-48A5-8442-F1DAD516E1A6} = {551C93F8-6E89-4954-8905-7F5AC7173285}
{A07BF64C-1B19-4972-83DB-B22BFF26E6B0} = {3249ADDB-50EC-4C21-A8F0-65EF444662EE}
{7FD8C746-9DB1-4B33-B4A6-95AF5FEF2CCD} = {3249ADDB-50EC-4C21-A8F0-65EF444662EE}
{43CE835D-5A71-4689-9297-942EF1233175} = {551C93F8-6E89-4954-8905-7F5AC7173285}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E6FB7BCB-BF07-4F19-ACBA-457479D421BB}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (C) Microsoft Corporation. All rights reserved.

using System;
using System.Diagnostics;

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

/// <summary>
/// A set of extensions for Activity markers.
/// </summary>
public static class ActivityMarkersExtensions
{
/// <summary>
/// The key used to identify the Short Circuited Activity.
/// </summary>
public const string LivenessCheckActivityKey = "LivenessCheckMarker";

/// <summary>
/// The value used to identify the Short Circuited Activity.
/// </summary>
public const string LivenessCheckActivityValue = "true";

/// <summary>
/// Determines whether the activity has been marked as belonging to a health check whose call should be
/// short-circuited.
/// </summary>
/// <param name="activity">The Activity.</param>
/// <returns><c>True</c> if the activity's health check should be short-circuited, <c>False</c> otherwise.</returns>
public static bool IsLivenessCheck(this Activity activity) =>
string.Equals(activity.GetBaggageItem(LivenessCheckActivityKey), LivenessCheckActivityValue, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Marks the Activity as belonging to a liveness health check whose call should be short-circuited.
/// </summary>
/// <param name="activity">The activity.</param>
/// <returns>The marked activity.</returns>
public static Activity MarkAsLivenessCheck(this Activity activity)
{
if (!activity.IsLivenessCheck())
{
return activity.AddBaggage(LivenessCheckActivityKey, LivenessCheckActivityValue);
}

return activity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (C) Microsoft Corporation. All rights reserved.

using System;
using System.Diagnostics;
using System.Net.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;

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

/// <summary>
/// Extension methods for health check decorators configuration.
/// </summary>
public static class HealthCheckComposablesExtensions
{
/// <summary>
/// Creates a new liveness HTTP endpoint checker health check.
/// This health check will only verify the reachability of the endpoint, it will mark the activity as a liveness activity,
/// that can trigger the response short-circuiting.
/// </summary>
/// <param name="requestBuilder">The request builder.</param>
/// <param name="activitySource">The activity source.</param>
/// <param name="httpClientFactory">The http client factory.</param>
/// <param name="httpClientName">The HTTP Client name, if a client with that name was registered in particular.</param>
/// <returns>The HTTP endpoint checker health check.</returns>
public static IHealthCheck CreateLivenessHttpHealthCheck(
Func<HttpRequestMessage> requestBuilder,
ActivitySource activitySource,
IHttpClientFactory httpClientFactory,
string? httpClientName = "") =>
Composables.HealthCheckComposablesExtensions.CreateHttpHealthCheck(
requestBuilder,
async (context, response, _) => await Composables.HealthCheckComposablesExtensions.CheckResponseStatusCodeAsync(context, response),
activitySource,
httpClientFactory,
httpClientName: httpClientName,
activityMarker: activity => activity?.MarkAsLivenessCheck());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (C) Microsoft Corporation. All rights reserved.

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

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

/// <summary>
/// This endpoint filter tries to identify whether the current <seealso cref="Activity"/> has been marked
/// as a Liveness health check. If it is, it returns a 200 response.
/// </summary>
public class LivenessCheckActionFilterAttribute : ActionFilterAttribute
{
/// <inheritdoc />
public override void OnActionExecuting(ActionExecutingContext context)
{
if (Activity.Current?.IsLivenessCheck() == true)
{
context.Result = new OkObjectResult("Running test healthcheck transaction");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetCoreVersions)</TargetFrameworks>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Label="NuGet Properties">
<Title>Microsoft.Omex.Extensions.Diagnostics.HealthChecks.AspNetCore</Title>
<Summary>Microsoft Omex Extensions Diagnostics HealthChecks.AspNetCore</Summary>
<Description>This library adds support to Omex Health Checks to ASP.NET Core controllers.</Description>
<ReleaseNotes>Initial release.</ReleaseNotes>
<PackageProjectUrl>https://github.com/microsoft/Omex/tree/main/src/Extensions/Diagnostics.HealthChecks</PackageProjectUrl>
<PackageTags>Microsoft;Omex;Extensions;HealthChecks;AspNetCore</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Diagnostics.HealthChecks\Microsoft.Omex.Extensions.Diagnostics.HealthChecks.csproj" />
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion src/Diagnostics.HealthChecks/AbstractHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Microsoft.Omex.Extensions.Diagnostics.HealthChecks
/// <summary>
/// Base health check that extracts logic of exception handling and wraps it into activity
/// </summary>
[Obsolete("The usage of this class is deprecated and will be removed in a later release, please use composable classes in Microsoft.Omex.Extensions.Diagnostics.HealthChecks.Composables namespace to build health checks.")]
public abstract class AbstractHealthCheck<TParameters> : IHealthCheck
where TParameters : HealthCheckParameters
{
Expand Down Expand Up @@ -43,7 +44,7 @@ protected AbstractHealthCheck(TParameters parameters, ILogger logger, ActivitySo
/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken token = default)
{
string activityName = string.IsNullOrWhiteSpace(context.Registration.Name) ? "HealthCheckActivity" : context.Registration.Name;
string activityName = string.IsNullOrWhiteSpace(context.Registration.Name) ? "HealthCheckActivity" : context.Registration.Name;
using Activity? activity = m_activitySource.StartActivity(activityName)
?.MarkAsSystemError()
.MarkAsHealthCheck();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (C) Microsoft Corporation. All rights reserved.

using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;

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

/// <summary>
/// Extension methods for health check decorators configuration.
/// </summary>
public static class HealthCheckComposablesExtensions
{
/// <summary>
/// Makes the Health check executable only at startup time, adding also observability to it.
/// If the consumer health check of this extension method does not want automatic observability,
/// the developer is encouraged to wrap it directly around the <see cref="StartupHealthCheck"/> class.
/// </summary>
/// <param name="healthCheck">The health check.</param>
/// <param name="cache">The memory cache. It will be used to memorise the latest health check result.</param>
/// <param name="activitySource">The activity source.</param>
/// <param name="logger">The logger.</param>
/// <param name="parameters">The health check parameters, set at DI registration time.</param>
/// <returns>The health check executable only at deploy time.</returns>
public static IHealthCheck AsObservableStartupHealthCheck(
this IHealthCheck healthCheck,
IMemoryCache cache,
ActivitySource activitySource,
ILogger logger,
HealthCheckParameters? parameters = null)
{
IHealthCheck startupHealthCheck = new StartupHealthCheck(healthCheck, cache);
return new ObservableHealthCheck(parameters ?? new(), startupHealthCheck, activitySource, logger);
}

/// <summary>
/// Applies the default error handling and activity around the current health check.
/// </summary>
/// The instance of the Activity marker. By selecting the implementor, the health check will determine how
/// the related endpoint will process the health check request.
/// <param name="healthCheck">The health check.</param>
/// <param name="activitySource">The activity source.</param>
/// <param name="logger">The logger.</param>
/// <param name="parameters">The health check parameters, set at DI registration time.</param>
/// <returns>The health check with the default configuration and error handling.</returns>
public static IHealthCheck AsObservableHealthCheck(
this IHealthCheck healthCheck,
ActivitySource activitySource,
ILogger logger,
HealthCheckParameters? parameters = null) =>
new ObservableHealthCheck(parameters ?? new(), healthCheck, activitySource, logger);

/// <summary>
/// Creates a new HTTP endpoint checker health check.
/// </summary>
/// <param name="httpClientFactory">The http client factory.</param>
/// <param name="requestBuilder">The request builder.</param>
/// <param name="healthCheckResponseChecker">The response checker instance.</param>
/// <param name="activitySource">The activity source.</param>
/// <param name="httpClientName">The HTTP Client name, if a client with that name was registered in particular.</param>
/// <param name="activityMarker">Marks the activity with the required baggage items.</param>
/// <returns>The HTTP endpoint checker health check.</returns>
public static IHealthCheck CreateHttpHealthCheck(
Func<HttpRequestMessage> requestBuilder,
Func<HealthCheckContext, HttpResponseMessage, CancellationToken, Task<HealthCheckResult>> healthCheckResponseChecker,
ActivitySource activitySource,
IHttpClientFactory httpClientFactory,
string? httpClientName = "",
Action<Activity?>? activityMarker = null) =>
new HttpEndpointHealthCheck(
httpClientFactory,
requestBuilder,
healthCheckResponseChecker,
activitySource,
httpClientName: httpClientName,
activityMarker: activityMarker);

/// <summary>
/// Checks whether the response from the endpoint is successful or not by checking its HTTP Status Code.
/// </summary>
/// <param name="context">The Health Check Context.</param>
/// <param name="response">The endpoint response.</param>
/// <param name="allowedStatusCodes">The allowed status codes for the response..</param>
/// <returns>The health check result.</returns>
public static async Task<HealthCheckResult> CheckResponseStatusCodeAsync(
HealthCheckContext context,
HttpResponseMessage response,
HttpStatusCode[]? allowedStatusCodes = null)
{
if (allowedStatusCodes == null || allowedStatusCodes.Length == 0)
{
allowedStatusCodes = new[] { HttpStatusCode.OK };
}

if (allowedStatusCodes.Contains(response.StatusCode))
{
return await Task.FromResult(new HealthCheckResult(HealthStatus.Healthy, "The endpoint returned an allowed status code."));
}

return await Task.FromResult(
new HealthCheckResult(
context.Registration.FailureStatus,
$"The endpoint returned an unallowed status code. Status code returned: '{response.StatusCode}'. " +
$"Allowed status codes: '{string.Join(", ", allowedStatusCodes.Select(h => h.ToString()))}'"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (C) Microsoft Corporation. All rights reserved.

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

/// <summary>
/// A series of constants used by health checks.
/// </summary>
public static class HealthCheckConstants
{
/// <summary>
/// The local service default host.
/// </summary>
public const string LocalServiceDefaultHost = "localhost";
}
Loading