Skip to content

Commit bb824d6

Browse files
authored
Add ScriptHost and WebHost IHealthCheck (#11161)
Health check service is not yet connected.
1 parent 8ef03dc commit bb824d6

File tree

4 files changed

+233
-0
lines changed

4 files changed

+233
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.Extensions.Diagnostics.HealthChecks;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
10+
{
11+
internal class ScriptHostHealthCheck(IScriptHostManager manager) : IHealthCheck
12+
{
13+
private readonly IScriptHostManager _manager = manager ?? throw new ArgumentNullException(nameof(manager));
14+
15+
private static readonly Task<HealthCheckResult> _healthy =
16+
Task.FromResult(HealthCheckResult.Healthy());
17+
18+
private static readonly Task<HealthCheckResult> _unhealthyNoScriptHost =
19+
Task.FromResult(HealthCheckResult.Unhealthy("No script host available"));
20+
21+
private static readonly Task<HealthCheckResult> _unhealthyNotStarted =
22+
Task.FromResult(HealthCheckResult.Unhealthy("Script host not started"));
23+
24+
private static readonly Task<HealthCheckResult> _unhealthyStopping =
25+
Task.FromResult(HealthCheckResult.Unhealthy("Script host stopping"));
26+
27+
private static readonly Task<HealthCheckResult> _unhealthyStopped =
28+
Task.FromResult(HealthCheckResult.Unhealthy("Script host stopped"));
29+
30+
private static readonly Task<HealthCheckResult> _unhealthyOffline =
31+
Task.FromResult(HealthCheckResult.Unhealthy("Script host offline"));
32+
33+
private static readonly Task<HealthCheckResult> _unhealthyUnknown =
34+
Task.FromResult(HealthCheckResult.Unhealthy("Script host in unknown state"));
35+
36+
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
37+
=> _manager.State switch
38+
{
39+
ScriptHostState.Default => _unhealthyNoScriptHost,
40+
ScriptHostState.Starting => _unhealthyNotStarted,
41+
ScriptHostState.Initialized or ScriptHostState.Running => _healthy,
42+
ScriptHostState.Error => UnhealthyError(_manager.LastError),
43+
ScriptHostState.Stopping => _unhealthyStopping,
44+
ScriptHostState.Stopped => _unhealthyStopped,
45+
ScriptHostState.Offline => _unhealthyOffline,
46+
_ => _unhealthyUnknown,
47+
};
48+
49+
private static Task<HealthCheckResult> UnhealthyError(Exception ex)
50+
=> Task.FromResult(HealthCheckResult.Unhealthy($"Script host in error state: {Environment.NewLine}{ex.Message}", ex));
51+
}
52+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Extensions.Diagnostics.HealthChecks;
7+
using Microsoft.Extensions.Hosting;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
10+
{
11+
internal class WebHostHealthCheck(IHostApplicationLifetime lifetime) : IHealthCheck
12+
{
13+
private static readonly Task<HealthCheckResult> _healthy = Task.FromResult(HealthCheckResult.Healthy());
14+
private static readonly Task<HealthCheckResult> _unhealthyNotStarted = Task.FromResult(HealthCheckResult.Unhealthy("Not Started"));
15+
private static readonly Task<HealthCheckResult> _unhealthyStopping = Task.FromResult(HealthCheckResult.Unhealthy("Stopping"));
16+
private static readonly Task<HealthCheckResult> _unhealthyStopped = Task.FromResult(HealthCheckResult.Unhealthy("Stopped"));
17+
18+
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
19+
{
20+
bool isStopped = lifetime.ApplicationStopped.IsCancellationRequested;
21+
if (isStopped)
22+
{
23+
return _unhealthyStopped;
24+
}
25+
26+
bool isStopping = lifetime.ApplicationStopping.IsCancellationRequested;
27+
if (isStopping)
28+
{
29+
return _unhealthyStopping;
30+
}
31+
32+
bool isStarted = lifetime.ApplicationStarted.IsCancellationRequested;
33+
if (!isStarted)
34+
{
35+
return _unhealthyNotStarted;
36+
}
37+
38+
return _healthy;
39+
}
40+
}
41+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
using System;
5+
using System.Threading.Tasks;
6+
using FluentAssertions;
7+
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
8+
using Microsoft.Extensions.Diagnostics.HealthChecks;
9+
using Moq;
10+
using Xunit;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics.HealthChecks
13+
{
14+
public class ScriptHostHealthCheckTests
15+
{
16+
[Theory]
17+
[InlineData(ScriptHostState.Default, "No script host available", HealthStatus.Unhealthy)]
18+
[InlineData(ScriptHostState.Starting, "Script host not started", HealthStatus.Unhealthy)]
19+
[InlineData(ScriptHostState.Initialized, null, HealthStatus.Healthy)]
20+
[InlineData(ScriptHostState.Running, null, HealthStatus.Healthy)]
21+
[InlineData(ScriptHostState.Stopping, "Script host stopping", HealthStatus.Unhealthy)]
22+
[InlineData(ScriptHostState.Stopped, "Script host stopped", HealthStatus.Unhealthy)]
23+
[InlineData(ScriptHostState.Offline, "Script host offline", HealthStatus.Unhealthy)]
24+
[InlineData((ScriptHostState)100, "Script host in unknown state", HealthStatus.Unhealthy)]
25+
public async Task CheckHealthAsync_MatchesScriptHostHealth(
26+
ScriptHostState state, string description, HealthStatus status)
27+
{
28+
// arrange
29+
Mock<IScriptHostManager> manager = new();
30+
manager.Setup(m => m.State).Returns(state);
31+
ScriptHostHealthCheck healthCheck = new(manager.Object);
32+
33+
// act
34+
HealthCheckResult result = await healthCheck.CheckHealthAsync(new(), default);
35+
36+
// assert
37+
result.Status.Should().Be(status);
38+
result.Description.Should().Be(description);
39+
result.Exception.Should().BeNull();
40+
}
41+
42+
[Fact]
43+
public async Task CheckHealthAsync_Error_IncludesException()
44+
{
45+
// arrange
46+
Exception error = new("Some exception message");
47+
Mock<IScriptHostManager> manager = new();
48+
manager.Setup(m => m.State).Returns(ScriptHostState.Error);
49+
manager.Setup(m => m.LastError).Returns(error);
50+
ScriptHostHealthCheck healthCheck = new(manager.Object);
51+
52+
// act
53+
HealthCheckResult result = await healthCheck.CheckHealthAsync(new(), default);
54+
55+
// assert
56+
result.Status.Should().Be(HealthStatus.Unhealthy);
57+
result.Description.Should().Be($"Script host in error state: {Environment.NewLine}{error.Message}");
58+
result.Exception.Should().Be(error);
59+
}
60+
}
61+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using FluentAssertions;
7+
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
8+
using Microsoft.Extensions.Diagnostics.HealthChecks;
9+
using Microsoft.Extensions.Hosting;
10+
using Moq;
11+
using Xunit;
12+
13+
namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics.HealthChecks
14+
{
15+
public class WebHostHealthCheckTests
16+
{
17+
public enum LifetimeState
18+
{
19+
NotStarted = 0,
20+
Running = 1,
21+
Stopping = 2,
22+
Stopped = 3,
23+
}
24+
25+
[Theory]
26+
[InlineData(LifetimeState.NotStarted)]
27+
[InlineData(LifetimeState.Running)]
28+
[InlineData(LifetimeState.Stopping)]
29+
[InlineData(LifetimeState.Stopped)]
30+
public async Task CheckHealthAsync_MatchesApplicationHealth(LifetimeState state)
31+
{
32+
// arrange
33+
IHostApplicationLifetime lifetime = CreateLifetime(state, out string description, out HealthStatus expected);
34+
WebHostHealthCheck healthCheck = new(lifetime);
35+
36+
// act
37+
HealthCheckResult result = await healthCheck.CheckHealthAsync(new(), default);
38+
39+
// assert
40+
result.Status.Should().Be(expected);
41+
result.Exception.Should().BeNull();
42+
result.Description.Should().Be(description);
43+
}
44+
45+
private static IHostApplicationLifetime CreateLifetime(
46+
LifetimeState state, out string description, out HealthStatus status)
47+
{
48+
Mock<IHostApplicationLifetime> lifetime = new();
49+
50+
int lifecycle = (int)state;
51+
52+
description = "Not Started";
53+
status = HealthStatus.Unhealthy;
54+
55+
if (lifecycle > 0)
56+
{
57+
description = null;
58+
lifetime.Setup(m => m.ApplicationStarted).Returns(new CancellationToken(true));
59+
status = HealthStatus.Healthy;
60+
}
61+
62+
if (lifecycle > 1)
63+
{
64+
description = "Stopping";
65+
lifetime.Setup(m => m.ApplicationStopping).Returns(new CancellationToken(true));
66+
status = HealthStatus.Unhealthy;
67+
}
68+
69+
if (lifecycle > 2)
70+
{
71+
description = "Stopped";
72+
lifetime.Setup(m => m.ApplicationStopped).Returns(new CancellationToken(true));
73+
status = HealthStatus.Unhealthy;
74+
}
75+
76+
return lifetime.Object;
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)