Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
internal class ScriptHostHealthCheck(IScriptHostManager manager) : IHealthCheck
{
private readonly IScriptHostManager _manager = manager ?? throw new ArgumentNullException(nameof(manager));

private static readonly Task<HealthCheckResult> _healthy =
Task.FromResult(HealthCheckResult.Healthy());

private static readonly Task<HealthCheckResult> _unhealthyNoScriptHost =
Task.FromResult(HealthCheckResult.Unhealthy("No script host available"));

private static readonly Task<HealthCheckResult> _unhealthyNotStarted =
Task.FromResult(HealthCheckResult.Unhealthy("Script host not started"));

private static readonly Task<HealthCheckResult> _unhealthyStopping =
Task.FromResult(HealthCheckResult.Unhealthy("Script host stopping"));

private static readonly Task<HealthCheckResult> _unhealthyStopped =
Task.FromResult(HealthCheckResult.Unhealthy("Script host stopped"));

private static readonly Task<HealthCheckResult> _unhealthyOffline =
Task.FromResult(HealthCheckResult.Unhealthy("Script host offline"));

private static readonly Task<HealthCheckResult> _unhealthyUnknown =
Task.FromResult(HealthCheckResult.Unhealthy("Script host in unknown state"));

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
=> _manager.State switch
{
ScriptHostState.Default => _unhealthyNoScriptHost,
ScriptHostState.Starting => _unhealthyNotStarted,
ScriptHostState.Initialized or ScriptHostState.Running => _healthy,
ScriptHostState.Error => UnhealthyError(_manager.LastError),
ScriptHostState.Stopping => _unhealthyStopping,
ScriptHostState.Stopped => _unhealthyStopped,
ScriptHostState.Offline => _unhealthyOffline,
_ => _unhealthyUnknown,
};

private static Task<HealthCheckResult> UnhealthyError(Exception ex)
=> Task.FromResult(HealthCheckResult.Unhealthy($"Script host in error state: {Environment.NewLine}{ex.Message}", ex));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
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"));

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
bool isStopped = lifetime.ApplicationStopped.IsCancellationRequested;
if (isStopped)
{
return _unhealthyStopped;
}

bool isStopping = lifetime.ApplicationStopping.IsCancellationRequested;
if (isStopping)
{
return _unhealthyStopping;
}

bool isStarted = lifetime.ApplicationStarted.IsCancellationRequested;
if (!isStarted)
{
return _unhealthyNotStarted;
}

return _healthy;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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.Threading.Tasks;
using FluentAssertions;
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moq;
using Xunit;

namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics.HealthChecks
{
public class ScriptHostHealthCheckTests
{
[Theory]
[InlineData(ScriptHostState.Default, "No script host available", HealthStatus.Unhealthy)]
[InlineData(ScriptHostState.Starting, "Script host not started", HealthStatus.Unhealthy)]
[InlineData(ScriptHostState.Initialized, null, HealthStatus.Healthy)]
[InlineData(ScriptHostState.Running, null, HealthStatus.Healthy)]
[InlineData(ScriptHostState.Stopping, "Script host stopping", HealthStatus.Unhealthy)]
[InlineData(ScriptHostState.Stopped, "Script host stopped", HealthStatus.Unhealthy)]
[InlineData(ScriptHostState.Offline, "Script host offline", HealthStatus.Unhealthy)]
[InlineData((ScriptHostState)100, "Script host in unknown state", HealthStatus.Unhealthy)]
public async Task CheckHealthAsync_MatchesScriptHostHealth(
ScriptHostState state, string description, HealthStatus status)
{
// arrange
Mock<IScriptHostManager> manager = new();
manager.Setup(m => m.State).Returns(state);
ScriptHostHealthCheck healthCheck = new(manager.Object);

// act
HealthCheckResult result = await healthCheck.CheckHealthAsync(new(), default);

// assert
result.Status.Should().Be(status);
result.Description.Should().Be(description);
result.Exception.Should().BeNull();
}

[Fact]
public async Task CheckHealthAsync_Error_IncludesException()
{
// arrange
Exception error = new("Some exception message");
Mock<IScriptHostManager> manager = new();
manager.Setup(m => m.State).Returns(ScriptHostState.Error);
manager.Setup(m => m.LastError).Returns(error);
ScriptHostHealthCheck healthCheck = new(manager.Object);

// act
HealthCheckResult result = await healthCheck.CheckHealthAsync(new(), default);

// assert
result.Status.Should().Be(HealthStatus.Unhealthy);
result.Description.Should().Be($"Script host in error state: {Environment.NewLine}{error.Message}");
result.Exception.Should().Be(error);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Moq;
using Xunit;

namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics.HealthChecks
{
public class WebHostHealthCheckTests
{
public enum LifetimeState
{
NotStarted = 0,
Running = 1,
Stopping = 2,
Stopped = 3,
}

[Theory]
[InlineData(LifetimeState.NotStarted)]
[InlineData(LifetimeState.Running)]
[InlineData(LifetimeState.Stopping)]
[InlineData(LifetimeState.Stopped)]
public async Task CheckHealthAsync_MatchesApplicationHealth(LifetimeState state)
{
// arrange
IHostApplicationLifetime lifetime = CreateLifetime(state, out string description, out HealthStatus expected);
WebHostHealthCheck healthCheck = new(lifetime);

// act
HealthCheckResult result = await healthCheck.CheckHealthAsync(new(), default);

// assert
result.Status.Should().Be(expected);
result.Exception.Should().BeNull();
result.Description.Should().Be(description);
}

private static IHostApplicationLifetime CreateLifetime(
LifetimeState state, out string description, out HealthStatus status)
{
Mock<IHostApplicationLifetime> lifetime = new();

int lifecycle = (int)state;

description = "Not Started";
status = HealthStatus.Unhealthy;

if (lifecycle > 0)
{
description = null;
lifetime.Setup(m => m.ApplicationStarted).Returns(new CancellationToken(true));
status = HealthStatus.Healthy;
}

if (lifecycle > 1)
{
description = "Stopping";
lifetime.Setup(m => m.ApplicationStopping).Returns(new CancellationToken(true));
status = HealthStatus.Unhealthy;
}

if (lifecycle > 2)
{
description = "Stopped";
lifetime.Setup(m => m.ApplicationStopped).Returns(new CancellationToken(true));
status = HealthStatus.Unhealthy;
}

return lifetime.Object;
}
}
}
Loading