Skip to content

Commit efbc6b1

Browse files
authored
[HealthChecks] Register health check services & checks (#11183)
* Register health check services & checks * Fix using orderings * Verify health check registered types * Move health check names to constants * FluentAssertions -> AwesomeAssertions * az.functions -> azure.functions * Remove unused usings
1 parent 21c8bea commit efbc6b1

File tree

8 files changed

+213
-5
lines changed

8 files changed

+213
-5
lines changed

src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.Azure.WebJobs.Script.Config;
1414
using Microsoft.Azure.WebJobs.Script.Configuration;
1515
using Microsoft.Azure.WebJobs.Script.Diagnostics;
16+
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
1617
using Microsoft.Azure.WebJobs.Script.Grpc;
1718
using Microsoft.Azure.WebJobs.Script.Metrics;
1819
using Microsoft.Azure.WebJobs.Script.Middleware;
@@ -240,6 +241,9 @@ public static void AddWebJobsScriptHost(this IServiceCollection services, IConfi
240241

241242
// Add AzureBlobStorageProvider to WebHost (also needed for ScriptHost) and AzureTableStorageProvider
242243
services.AddAzureStorageProviders();
244+
245+
// Add health checks
246+
services.AddHealthChecks().AddWebJobsScriptHealthChecks();
243247
}
244248

245249
internal static void AddHostingConfigOptions(this IServiceCollection services, IConfiguration configuration)

src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckExtensions.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,26 @@
99

1010
namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
1111
{
12+
/// <summary>
13+
/// Health check related extension methods.
14+
/// </summary>
1215
internal static class HealthCheckExtensions
1316
{
17+
/// <summary>
18+
/// Registers all health check services required for the functions host. Should be called
19+
/// on the WebHost.
20+
/// </summary>
21+
/// <param name="builder">The builder to register health checks with.</param>
22+
/// <returns>The original builder, for call chaining.</returns>
23+
public static IHealthChecksBuilder AddWebJobsScriptHealthChecks(this IHealthChecksBuilder builder)
24+
{
25+
ArgumentNullException.ThrowIfNull(builder);
26+
builder
27+
.AddWebHostHealthCheck()
28+
.AddScriptHostHealthCheck();
29+
return builder;
30+
}
31+
1432
/// <summary>
1533
/// Registers the telemetry health check publisher with the specified additional tags.
1634
/// NOTE: this is currently not safe to call multiple times.
@@ -49,6 +67,32 @@ static void RegisterPublisher(IServiceCollection services, string tag)
4967
return builder;
5068
}
5169

70+
/// <summary>
71+
/// Adds a health check for the web host lifecycle.
72+
/// </summary>
73+
/// <param name="builder">The builder to register health checks with.</param>
74+
/// <returns>The original builder, for call chaining.</returns>
75+
public static IHealthChecksBuilder AddWebHostHealthCheck(this IHealthChecksBuilder builder)
76+
{
77+
ArgumentNullException.ThrowIfNull(builder);
78+
builder.AddCheck<WebHostHealthCheck>(
79+
HealthCheckNames.WebHostLifeCycle, tags: [HealthCheckTags.Liveness]);
80+
return builder;
81+
}
82+
83+
/// <summary>
84+
/// Adds a health check for the script host lifecycle.
85+
/// </summary>
86+
/// <param name="builder">The builder to register health checks with.</param>
87+
/// <returns>The original builder, for call chaining.</returns>
88+
public static IHealthChecksBuilder AddScriptHostHealthCheck(this IHealthChecksBuilder builder)
89+
{
90+
ArgumentNullException.ThrowIfNull(builder);
91+
builder.AddCheck<ScriptHostHealthCheck>(
92+
HealthCheckNames.ScriptHostLifeCycle, tags: [HealthCheckTags.Readiness]);
93+
return builder;
94+
}
95+
5296
/// <summary>
5397
/// Filters a health report to include only specified entries.
5498
/// </summary>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
5+
{
6+
/// <summary>
7+
/// Contains names used for health checks in the Azure Functions host.
8+
/// </summary>
9+
internal static class HealthCheckNames
10+
{
11+
private const string Prefix = "azure.functions.";
12+
13+
/// <summary>
14+
/// The 'azure.functions.web_host.lifecycle' check monitors the lifecycle of the web host.
15+
/// </summary>
16+
public const string WebHostLifeCycle = Prefix + "web_host.lifecycle";
17+
18+
/// <summary>
19+
/// The 'azure.functions.script_host.lifecycle' check monitors the lifecycle of the script host.
20+
/// </summary>
21+
public const string ScriptHostLifeCycle = Prefix + "script_host.lifecycle";
22+
}
23+
}

src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckTags.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,35 @@
33

44
namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
55
{
6+
/// <summary>
7+
/// Contains tags used for health checks in the Azure Functions host.
8+
/// </summary>
69
internal static class HealthCheckTags
710
{
8-
private const string Prefix = "azure.functions";
11+
private const string Prefix = "azure.functions.";
912

10-
public const string Liveness = Prefix + ".liveness";
13+
/// <summary>
14+
/// The 'azure.functions.liveness' tag is used for liveness checks in the Azure Functions host.
15+
/// </summary>
16+
/// <remarks>
17+
/// Liveness checks are used to determine if the host is alive and responsive.
18+
/// </remarks>
19+
public const string Liveness = Prefix + "liveness";
1120

12-
public const string Readiness = Prefix + ".readiness";
21+
/// <summary>
22+
/// The 'azure.functions.readiness' tag is used for readiness checks in the Azure Functions host.
23+
/// </summary>
24+
/// <remarks>
25+
/// Readiness checks are used to determine if the host is ready to process requests.
26+
/// </remarks>
27+
public const string Readiness = Prefix + "readiness";
1328

14-
public const string Configuration = Prefix + ".configuration";
29+
/// <summary>
30+
/// The 'azure.functions.configuration' tag is used for configuration-related health checks in the Azure Functions host.
31+
/// </summary>
32+
/// <remarks>
33+
/// These are typically customer configuration related, such as configuring AzureWebJobsStorage access.
34+
/// </remarks>
35+
public const string Configuration = Prefix + "configuration";
1536
}
1637
}

src/WebJobs.Script/Diagnostics/HealthChecks/ScriptHostHealthCheck.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

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

40+
/// <inheritdoc />
3641
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
3742
=> _manager.State switch
3843
{

src/WebJobs.Script/Diagnostics/HealthChecks/WebHostHealthCheck.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@
88

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

22+
/// <inheritdoc />
1823
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
1924
{
2025
bool isStopped = lifetime.ApplicationStopped.IsCancellationRequested;

test/WebJobs.Script.Tests/Diagnostics/HealthChecks/HealthCheckExtensionsTests.cs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Reflection;
78
using AwesomeAssertions;
89
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
910
using Microsoft.Extensions.DependencyInjection;
1011
using Microsoft.Extensions.Diagnostics.HealthChecks;
12+
using Moq;
1113
using Xunit;
1214

1315
namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics.HealthChecks
@@ -23,6 +25,91 @@ public class HealthCheckExtensionsTests
2325
{ ["tag1", "tag2"], [null, "tag1", "tag2"] },
2426
};
2527

28+
[Fact]
29+
public void AddWebJobsScriptHealthChecks_ThrowsOnNullBuilder()
30+
{
31+
IHealthChecksBuilder builder = null;
32+
Action act = () => HealthCheckExtensions.AddWebJobsScriptHealthChecks(builder);
33+
act.Should().Throw<ArgumentNullException>().WithParameterName("builder");
34+
}
35+
36+
[Fact]
37+
public void AddWebHostHealthCheck_ThrowsOnNullBuilder()
38+
{
39+
IHealthChecksBuilder builder = null;
40+
Action act = () => HealthCheckExtensions.AddWebHostHealthCheck(builder);
41+
act.Should().Throw<ArgumentNullException>().WithParameterName("builder");
42+
}
43+
44+
[Fact]
45+
public void AddScriptHostHealthCheck_ThrowsOnNullBuilder()
46+
{
47+
IHealthChecksBuilder builder = null;
48+
Action act = () => HealthCheckExtensions.AddScriptHostHealthCheck(builder);
49+
act.Should().Throw<ArgumentNullException>().WithParameterName("builder");
50+
}
51+
52+
[Fact]
53+
public void AddWebJobsScriptHealthChecks_RegistersBothHealthChecks()
54+
{
55+
// arrange
56+
Mock<IHealthChecksBuilder> builder = new(MockBehavior.Strict);
57+
builder.Setup(b => b.Add(It.IsAny<HealthCheckRegistration>())).Returns(builder.Object);
58+
59+
// act
60+
IHealthChecksBuilder returned = builder.Object.AddWebJobsScriptHealthChecks();
61+
62+
// assert
63+
returned.Should().BeSameAs(builder.Object);
64+
builder.Verify(b => b.Add(IsRegistration<WebHostHealthCheck>(
65+
HealthCheckNames.WebHostLifeCycle, HealthCheckTags.Liveness)),
66+
Times.Once);
67+
builder.Verify(b => b.Add(IsRegistration<ScriptHostHealthCheck>(
68+
HealthCheckNames.ScriptHostLifeCycle, HealthCheckTags.Readiness)),
69+
Times.Once);
70+
builder.VerifyNoOtherCalls();
71+
}
72+
73+
[Fact]
74+
public void AddWebHostHealthCheck_RegistersWebHostHealthCheck()
75+
{
76+
// arrange
77+
Mock<IHealthChecksBuilder> builder = new(MockBehavior.Strict);
78+
builder.Setup(b => b.Add(It.IsAny<HealthCheckRegistration>())).Returns(builder.Object)
79+
.Callback((HealthCheckRegistration registration) =>
80+
{
81+
Type r = registration.Factory.GetMethodInfo().ReturnType;
82+
});
83+
84+
// act
85+
IHealthChecksBuilder returned = builder.Object.AddWebHostHealthCheck();
86+
87+
// assert
88+
returned.Should().BeSameAs(builder.Object);
89+
builder.Verify(b => b.Add(IsRegistration<WebHostHealthCheck>(
90+
HealthCheckNames.WebHostLifeCycle, HealthCheckTags.Liveness)),
91+
Times.Once);
92+
builder.VerifyNoOtherCalls();
93+
}
94+
95+
[Fact]
96+
public void AddScriptHostHealthCheck_RegistersScriptHostHealthCheck()
97+
{
98+
// arrange
99+
Mock<IHealthChecksBuilder> builder = new(MockBehavior.Strict);
100+
builder.Setup(b => b.Add(It.IsAny<HealthCheckRegistration>())).Returns(builder.Object);
101+
102+
// act
103+
IHealthChecksBuilder returned = builder.Object.AddScriptHostHealthCheck();
104+
105+
// assert
106+
returned.Should().BeSameAs(builder.Object);
107+
builder.Verify(b => b.Add(IsRegistration<ScriptHostHealthCheck>(
108+
HealthCheckNames.ScriptHostLifeCycle, HealthCheckTags.Readiness)),
109+
Times.Once);
110+
builder.VerifyNoOtherCalls();
111+
}
112+
26113
[Fact]
27114
public void Filter_ReturnsFilteredHealthReport()
28115
{
@@ -142,6 +229,25 @@ public void AddTelemetryPublisher_RegistersExpected(string[] tags, string[] expe
142229
}
143230
}
144231

232+
private static HealthCheckRegistration IsRegistration<T>(string name, string tag)
233+
where T : IHealthCheck
234+
{
235+
static bool IsType(HealthCheckRegistration registration)
236+
{
237+
if (registration.Factory is not { } factory)
238+
{
239+
return false;
240+
}
241+
242+
return factory.GetMethodInfo().ReturnType == typeof(T);
243+
}
244+
245+
return Match.Create<HealthCheckRegistration>(r =>
246+
{
247+
return r.Name == name && r.Tags.Contains(tag) && IsType(r);
248+
});
249+
}
250+
145251
private static bool VerifyPublisher(IHealthCheckPublisher publisher, string tag)
146252
{
147253
return publisher is TelemetryHealthCheckPublisher telemetryPublisher

test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<ItemGroup>
1919
<PackageReference Include="appinsights.testlogger" Version="1.0.0" />
2020
<PackageReference Include="AwesomeAssertions" Version="9.1.0" />
21-
<PackageReference Include="AwesomeAssertions.Analyzers" Version="9.0.3" PrivateAssets="all" />
21+
<PackageReference Include="AwesomeAssertions.Analyzers" Version="9.0.4" PrivateAssets="all" />
2222
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.22.0" />
2323
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
2424
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.22.0" />

0 commit comments

Comments
 (0)