Skip to content

Commit 7831af7

Browse files
authored
[HealthChecks] Add health check middleware (#11173)
* Add health check middleware * Fix test nullref * Update deps.json and runtimeassemblies.json * Update ExistingRuntimeAssemblies.txt * Update deps.json * Extract JsonSerializerOptions to reusable static * Mark sealed * Address PR comments * Update deps.json * Add max wait time, respond to RequestAborted * Remove unused assembly
1 parent a05ad56 commit 7831af7

19 files changed

+587
-109
lines changed

src/WebJobs.Script.WebHost/Controllers/KeysController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public async Task<IActionResult> Get()
6969
// Extensions that are webhook providers create their default system keys
7070
// as part of host initialization (when those keys aren't already present).
7171
// So we must delay key retrieval until host initialization is complete.
72-
await _hostManager.DelayUntilHostReady();
72+
await _hostManager.DelayUntilHostReadyAsync();
7373
}
7474

7575
Dictionary<string, string> keys = await GetHostSecretsByScope(hostKeyScope);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.Text.Json;
6+
using System.Threading.Tasks;
7+
using HealthChecks.UI.Client;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.Extensions.Diagnostics.HealthChecks;
10+
using Microsoft.Extensions.Primitives;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.HealthChecks
13+
{
14+
public class HealthCheckResponseWriter
15+
{
16+
public static Task WriteResponseAsync(HttpContext httpContext, HealthReport report)
17+
{
18+
ArgumentNullException.ThrowIfNull(httpContext);
19+
ArgumentNullException.ThrowIfNull(report);
20+
21+
// We will write a detailed report if ?expand=true is present.
22+
if (httpContext.Request.Query.TryGetValue("expand", out StringValues value)
23+
&& bool.TryParse(value, out bool expand) && expand)
24+
{
25+
return UIResponseWriter.WriteHealthCheckUIResponse(httpContext, report);
26+
}
27+
28+
return WriteMinimalResponseAsync(httpContext, report);
29+
}
30+
31+
private static Task WriteMinimalResponseAsync(HttpContext httpContext, HealthReport report)
32+
{
33+
MinimalResponse body = new(report.Status);
34+
return JsonSerializer.SerializeAsync(
35+
httpContext.Response.Body, body, JsonSerializerOptionsProvider.Options, httpContext.RequestAborted);
36+
}
37+
38+
internal readonly struct MinimalResponse(HealthStatus status)
39+
{
40+
public HealthStatus Status { get; } = status;
41+
}
42+
}
43+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 Microsoft.AspNetCore.Http;
7+
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
8+
using Microsoft.Extensions.Primitives;
9+
10+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.HealthChecks
11+
{
12+
public sealed class HealthCheckWaitMiddleware(RequestDelegate next, IScriptHostManager manager)
13+
{
14+
private const int MaxWaitSeconds = 60;
15+
private readonly RequestDelegate _next = next ?? throw new ArgumentNullException(nameof(next));
16+
private readonly IScriptHostManager _manager = manager ?? throw new ArgumentNullException(nameof(manager));
17+
18+
public async Task InvokeAsync(HttpContext context)
19+
{
20+
ArgumentNullException.ThrowIfNull(context);
21+
22+
// If specified, the ?wait={seconds} query param will wait for an
23+
// active script host for that duration. This is to avoid excessive polling
24+
// when waiting for the initial readiness probe.
25+
if (context.Request.Query.TryGetValue("wait", out StringValues wait))
26+
{
27+
if (!int.TryParse(wait.ToString(), out int waitSeconds) || waitSeconds < 0)
28+
{
29+
context.Response.StatusCode = StatusCodes.Status400BadRequest;
30+
await context.Response.WriteAsJsonAsync(
31+
ErrorResponse.BadArgument("'wait' query param must be a positive integer", $"wait={wait}"));
32+
return;
33+
}
34+
35+
waitSeconds = Math.Min(waitSeconds, MaxWaitSeconds);
36+
await _manager.DelayUntilHostReadyAsync(waitSeconds).WaitAsync(context.RequestAborted);
37+
}
38+
39+
await _next(context);
40+
}
41+
}
42+
}

src/WebJobs.Script.WebHost/Extensions/HttpContextExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public static async Task WaitForRunningHostAsync(this HttpContext httpContext, I
2323
// If the host is not ready, we'll wait a bit for it to initialize.
2424
// This might happen if http requests come in while the host is starting
2525
// up for the first time, or if it is restarting.
26-
bool hostReady = await hostManager.DelayUntilHostReady(timeoutSeconds, pollingIntervalMilliseconds);
26+
bool hostReady = await hostManager.DelayUntilHostReadyAsync(timeoutSeconds, pollingIntervalMilliseconds);
2727

2828
if (!hostReady)
2929
{

src/WebJobs.Script.WebHost/Extensions/ScriptHostManagerExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.Azure.WebJobs.Script
77
{
88
public static class ScriptHostManagerExtensions
99
{
10-
public static async Task<bool> DelayUntilHostReady(this IScriptHostManager hostManager, int timeoutSeconds = ScriptConstants.HostTimeoutSeconds, int pollingIntervalMilliseconds = ScriptConstants.HostPollingIntervalMilliseconds)
10+
public static async Task<bool> DelayUntilHostReadyAsync(this IScriptHostManager hostManager, int timeoutSeconds = ScriptConstants.HostTimeoutSeconds, int pollingIntervalMilliseconds = ScriptConstants.HostPollingIntervalMilliseconds)
1111
{
1212
if (HostIsInitialized(hostManager))
1313
{

src/WebJobs.Script.WebHost/Middleware/HostAvailabilityCheckMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ private static async Task InvokeAwaitingHost(HttpContext context, RequestDelegat
5454
{
5555
Logger.InitiatingHostAvailabilityCheck(logger);
5656

57-
bool hostReady = await scriptHostManager.DelayUntilHostReady();
57+
bool hostReady = await scriptHostManager.DelayUntilHostReadyAsync();
5858
if (!hostReady)
5959
{
6060
Logger.HostUnavailableAfterCheck(logger);

src/WebJobs.Script.WebHost/Middleware/HostWarmupMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ public async Task HostWarmupAsync(HttpRequest request)
161161
await _hostManager.RestartHostAsync("Host warmup call requested a restart.", CancellationToken.None);
162162

163163
// This call is here for sanity, but we should be fully initialized.
164-
await _hostManager.DelayUntilHostReady();
164+
await _hostManager.DelayUntilHostReadyAsync();
165165
}
166166
}
167167

src/WebJobs.Script.WebHost/Models/ApiErrorModel.cs

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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.Collections.Generic;
5+
using System.Text.Json.Serialization;
6+
using Newtonsoft.Json;
7+
8+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Models
9+
{
10+
/// <summary>
11+
/// Represents an error response.
12+
/// See https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-details.md#error-response-content.
13+
/// </summary>
14+
/// <param name="Code">
15+
/// The error code. This is NOT the HTTP status code.
16+
/// Unlocalized string which can be used to programmatically identify the error.
17+
/// The code should be Pascal-cased, and should serve to uniquely identify a particular class of error,
18+
/// for example "BadArgument".
19+
/// </param>
20+
/// <param name="Message">
21+
/// The error message. Describes the error in detail and provides debugging information.
22+
/// If Accept-Language is set in the request, it must be localized to that language.
23+
/// </param>]
24+
public record ErrorResponse(
25+
[property: JsonProperty("code")][property: JsonPropertyName("code")] string Code,
26+
[property: JsonProperty("message")][property: JsonPropertyName("message")] string Message)
27+
{
28+
/// <summary>
29+
/// Gets the target of the particular error. For example, the name of the property in error.
30+
/// </summary>
31+
[JsonProperty("target")]
32+
[JsonPropertyName("target")]
33+
public string Target { get; init; }
34+
35+
/// <summary>
36+
/// Gets the details of this error.
37+
/// </summary>
38+
[JsonProperty("details")]
39+
[JsonPropertyName("details")]
40+
public IEnumerable<ErrorResponse> Details { get; init; } = [];
41+
42+
/// <summary>
43+
/// Gets the additional information for this error.
44+
/// </summary>
45+
[JsonProperty("additionalInfo")]
46+
[JsonPropertyName("additionalInfo")]
47+
public IEnumerable<ErrorAdditionalInfo> AdditionalInfo { get; init; } = [];
48+
49+
public static ErrorResponse BadArgument(string message, string target = null)
50+
{
51+
return new("BadArgument", message) { Target = target };
52+
}
53+
}
54+
55+
/// <summary>
56+
/// Represents additional information for an error.
57+
/// </summary>
58+
/// <param name="Type">The type of additional information.</param>
59+
/// <param name="Info">The additional error information.</param>
60+
public record ErrorAdditionalInfo(
61+
[property: JsonProperty("type")][property: JsonPropertyName("type")] string Type,
62+
[property: JsonProperty("info")][property: JsonPropertyName("info")] object Info);
63+
}

src/WebJobs.Script.WebHost/Standby/StandbyManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public async Task SpecializeHostCoreAsync()
124124

125125
using (_metricsLogger.LatencyEvent(MetricEventNames.SpecializationDelayUntilHostReady))
126126
{
127-
await _scriptHostManager.DelayUntilHostReady();
127+
await _scriptHostManager.DelayUntilHostReadyAsync();
128128
}
129129
}
130130

0 commit comments

Comments
 (0)