Skip to content

[HealthChecks] Register health check services & checks #11183

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

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
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
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Azure.WebJobs.Script.Config;
using Microsoft.Azure.WebJobs.Script.Configuration;
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
using Microsoft.Azure.WebJobs.Script.Grpc;
using Microsoft.Azure.WebJobs.Script.Metrics;
using Microsoft.Azure.WebJobs.Script.Middleware;
Expand Down Expand Up @@ -236,6 +237,9 @@ public static void AddWebJobsScriptHost(this IServiceCollection services, IConfi

// Add AzureBlobStorageProvider to WebHost (also needed for ScriptHost) and AzureTableStorageProvider
services.AddAzureStorageProviders();

// Add health checks
services.AddHealthChecks().AddWebJobsScriptHealthChecks();
}

internal static void AddHostingConfigOptions(this IServiceCollection services, IConfiguration configuration)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
/// <summary>
/// Health check related extension methods.
/// </summary>
internal static class HealthCheckExtensions
{
/// <summary>
/// Registers all health check services required for the functions host. Should be called
/// on the WebHost.
/// </summary>
/// <param name="builder">The builder to register health checks with.</param>
/// <returns>The original builder, for call chaining.</returns>
public static IHealthChecksBuilder AddWebJobsScriptHealthChecks(this IHealthChecksBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder
.AddWebHostHealthCheck()
.AddScriptHostHealthCheck();

return builder;
}

/// <summary>
/// Adds a health check for the web host lifecycle.
/// </summary>
/// <param name="builder">The builder to register health checks with.</param>
/// <returns>The original builder, for call chaining.</returns>
public static IHealthChecksBuilder AddWebHostHealthCheck(this IHealthChecksBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.AddCheck<WebHostHealthCheck>(
HealthCheckNames.WebHostLifeCycle, tags: [HealthCheckTags.Liveness]);
return builder;
}

/// <summary>
/// Adds a health check for the script host lifecycle.
/// </summary>
/// <param name="builder">The builder to register health checks with.</param>
/// <returns>The original builder, for call chaining.</returns>
public static IHealthChecksBuilder AddScriptHostHealthCheck(this IHealthChecksBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.AddCheck<ScriptHostHealthCheck>(
HealthCheckNames.ScriptHostLifeCycle, tags: [HealthCheckTags.Readiness]);
return builder;
}
}
}
23 changes: 23 additions & 0 deletions src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
/// <summary>
/// Contains names used for health checks in the Azure Functions host.
/// </summary>
internal static class HealthCheckNames
{
private const string Prefix = "az.functions.";

/// <summary>
/// The 'az.functions.web_host.lifecycle' check monitors the lifecycle of the web host.
/// </summary>
public const string WebHostLifeCycle = Prefix + "web_host.lifecycle";

/// <summary>
/// The 'az.functions.script_host.lifecycle' check monitors the lifecycle of the script host.
/// </summary>
public const string ScriptHostLifeCycle = Prefix + "script_host.lifecycle";
}
}
37 changes: 37 additions & 0 deletions src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckTags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
/// <summary>
/// Contains tags used for health checks in the Azure Functions host.
/// </summary>
internal static class HealthCheckTags
{
private const string Prefix = "az.functions.";

/// <summary>
/// The 'az.functions.liveness' tag is used for liveness checks in the Azure Functions host.
/// </summary>
/// <remarks>
/// Liveness checks are used to determine if the host is alive and responsive.
/// </remarks>
public const string Liveness = Prefix + "liveness";

/// <summary>
/// The 'az.functions.readiness' tag is used for readiness checks in the Azure Functions host.
/// </summary>
/// <remarks>
/// Readiness checks are used to determine if the host is ready to process requests.
/// </remarks>
public const string Readiness = Prefix + "readiness";

/// <summary>
/// The 'az.functions.configuration' tag is used for configuration-related health checks in the Azure Functions host.
/// </summary>
/// <remarks>
/// These are typically customer configuration related, such as configuring AzureWebJobsStorage access.
/// </remarks>
public const string Configuration = Prefix + "configuration";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
/// <summary>
/// A health check that reports the status of the script host lifecycle.
/// </summary>
/// <param name="manager">The script host manager.</param>
internal class ScriptHostHealthCheck(IScriptHostManager manager) : IHealthCheck
{
private readonly IScriptHostManager _manager = manager ?? throw new ArgumentNullException(nameof(manager));
Expand All @@ -33,6 +37,7 @@ internal class ScriptHostHealthCheck(IScriptHostManager manager) : IHealthCheck
private static readonly Task<HealthCheckResult> _unhealthyUnknown =
Task.FromResult(HealthCheckResult.Unhealthy("Script host in unknown state"));

/// <inheritdoc />
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
=> _manager.State switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
/// <summary>
/// A health check that reports the status of the web host lifecycle.
/// </summary>
/// <param name="lifetime">The application lifetime.</param>
internal class WebHostHealthCheck(IHostApplicationLifetime lifetime) : IHealthCheck
{
private static readonly Task<HealthCheckResult> _healthy = Task.FromResult(HealthCheckResult.Healthy());
private static readonly Task<HealthCheckResult> _unhealthyNotStarted = Task.FromResult(HealthCheckResult.Unhealthy("Not Started"));
private static readonly Task<HealthCheckResult> _unhealthyStopping = Task.FromResult(HealthCheckResult.Unhealthy("Stopping"));
private static readonly Task<HealthCheckResult> _unhealthyStopped = Task.FromResult(HealthCheckResult.Unhealthy("Stopped"));

/// <inheritdoc />
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
bool isStopped = lifetime.ApplicationStopped.IsCancellationRequested;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Reflection;
using AwesomeAssertions;
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moq;
using Xunit;

namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics.HealthChecks
{
public class HealthCheckExtensionsTests
{
[Fact]
public void AddWebJobsScriptHealthChecks_ThrowsOnNullBuilder()
{
IHealthChecksBuilder builder = null;
Action act = () => HealthCheckExtensions.AddWebJobsScriptHealthChecks(builder);
act.Should().Throw<ArgumentNullException>().WithParameterName("builder");
}

[Fact]
public void AddWebHostHealthCheck_ThrowsOnNullBuilder()
{
IHealthChecksBuilder builder = null;
Action act = () => HealthCheckExtensions.AddWebHostHealthCheck(builder);
act.Should().Throw<ArgumentNullException>().WithParameterName("builder");
}

[Fact]
public void AddScriptHostHealthCheck_ThrowsOnNullBuilder()
{
IHealthChecksBuilder builder = null;
Action act = () => HealthCheckExtensions.AddScriptHostHealthCheck(builder);
act.Should().Throw<ArgumentNullException>().WithParameterName("builder");
}

[Fact]
public void AddWebJobsScriptHealthChecks_RegistersBothHealthChecks()
{
// arrange
Mock<IHealthChecksBuilder> builder = new(MockBehavior.Strict);
builder.Setup(b => b.Add(It.IsAny<HealthCheckRegistration>())).Returns(builder.Object);

// act
IHealthChecksBuilder returned = builder.Object.AddWebJobsScriptHealthChecks();

// assert
returned.Should().BeSameAs(builder.Object);
builder.Verify(b => b.Add(IsRegistration<WebHostHealthCheck>(
HealthCheckNames.WebHostLifeCycle, HealthCheckTags.Liveness)),
Times.Once);
builder.Verify(b => b.Add(IsRegistration<ScriptHostHealthCheck>(
HealthCheckNames.ScriptHostLifeCycle, HealthCheckTags.Readiness)),
Times.Once);
builder.VerifyNoOtherCalls();
}

[Fact]
public void AddWebHostHealthCheck_RegistersWebHostHealthCheck()
{
// arrange
Mock<IHealthChecksBuilder> builder = new(MockBehavior.Strict);
builder.Setup(b => b.Add(It.IsAny<HealthCheckRegistration>())).Returns(builder.Object)
.Callback((HealthCheckRegistration registration) =>
{
Type r = registration.Factory.GetMethodInfo().ReturnType;
});

// act
IHealthChecksBuilder returned = builder.Object.AddWebHostHealthCheck();

// assert
returned.Should().BeSameAs(builder.Object);
builder.Verify(b => b.Add(IsRegistration<WebHostHealthCheck>(
HealthCheckNames.WebHostLifeCycle, HealthCheckTags.Liveness)),
Times.Once);
builder.VerifyNoOtherCalls();
}

[Fact]
public void AddScriptHostHealthCheck_RegistersScriptHostHealthCheck()
{
// arrange
Mock<IHealthChecksBuilder> builder = new(MockBehavior.Strict);
builder.Setup(b => b.Add(It.IsAny<HealthCheckRegistration>())).Returns(builder.Object);

// act
IHealthChecksBuilder returned = builder.Object.AddScriptHostHealthCheck();

// assert
returned.Should().BeSameAs(builder.Object);
builder.Verify(b => b.Add(IsRegistration<ScriptHostHealthCheck>(
HealthCheckNames.ScriptHostLifeCycle, HealthCheckTags.Readiness)),
Times.Once);
builder.VerifyNoOtherCalls();
}

private static HealthCheckRegistration IsRegistration<T>(string name, string tag)
where T : IHealthCheck
{
static bool IsType(HealthCheckRegistration registration)
{
if (registration.Factory is not { } factory)
{
return false;
}

return factory.GetMethodInfo().ReturnType == typeof(T);
}

return Match.Create<HealthCheckRegistration>(r =>
{
return r.Name == name && r.Tags.Contains(tag) && IsType(r);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System;
using System.Threading.Tasks;
using FluentAssertions;
using AwesomeAssertions;
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moq;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using AwesomeAssertions;
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using AwesomeAssertions;
using Azure.Identity;
using Azure.Monitor.OpenTelemetry.Exporter;
using FluentAssertions;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Azure.WebJobs.Script.Configuration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading.Tasks;
using FluentAssertions;
using AwesomeAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Script.WebHost.Middleware;
using Microsoft.Extensions.Primitives;
Expand Down
2 changes: 1 addition & 1 deletion test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="9.1.0" />
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.22.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.22.0" />
<PackageReference Include="Microsoft.ApplicationInsights.WindowsServer" Version="2.22.0" />
<PackageReference Include="Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel" Version="2.22.0" />
<PackageReference Include="appinsights.testlogger" Version="1.0.0" />
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.1.0" />
<PackageReference Include="Microsoft.Azure.Functions.PythonWorker" Version="4.38.0" />
Expand Down
Loading