Skip to content

Commit ae13bea

Browse files
committed
Add health check middleware
1 parent c7192ec commit ae13bea

File tree

16 files changed

+428
-53
lines changed

16 files changed

+428
-53
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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.Text.Json.Serialization;
7+
using System.Threading.Tasks;
8+
using HealthChecks.UI.Client;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.Extensions.Diagnostics.HealthChecks;
11+
using Microsoft.Extensions.Primitives;
12+
13+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.HealthChecks
14+
{
15+
public class HealthCheckResponseWriter
16+
{
17+
private static readonly JsonSerializerOptions _options = CreateJsonOptions();
18+
19+
public static Task WriteResponseAsync(HttpContext httpContext, HealthReport report)
20+
{
21+
ArgumentNullException.ThrowIfNull(httpContext);
22+
ArgumentNullException.ThrowIfNull(report);
23+
24+
// We will write a detailed report if ?expand=true is present.
25+
if (httpContext.Request.Query.TryGetValue("expand", out StringValues value)
26+
&& bool.TryParse(value, out bool expand) && expand)
27+
{
28+
return UIResponseWriter.WriteHealthCheckUIResponse(httpContext, report);
29+
}
30+
31+
return WriteMinimalResponseAsync(httpContext, report);
32+
}
33+
34+
private static Task WriteMinimalResponseAsync(HttpContext httpContext, HealthReport report)
35+
{
36+
MinimalResponse body = new(report.Status);
37+
return JsonSerializer.SerializeAsync(
38+
httpContext.Response.Body, body, _options, httpContext.RequestAborted);
39+
}
40+
41+
private static JsonSerializerOptions CreateJsonOptions()
42+
{
43+
var options = new JsonSerializerOptions
44+
{
45+
AllowTrailingCommas = true,
46+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
47+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
48+
};
49+
50+
options.Converters.Add(new JsonStringEnumConverter());
51+
52+
return options;
53+
}
54+
55+
internal readonly struct MinimalResponse(HealthStatus status)
56+
{
57+
public HealthStatus Status { get; } = status;
58+
}
59+
}
60+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 class HealthCheckWaitMiddleware(RequestDelegate next, IScriptHostManager manager)
13+
{
14+
private readonly RequestDelegate _next = next ?? throw new ArgumentNullException(nameof(next));
15+
private readonly IScriptHostManager _manager = manager ?? throw new ArgumentNullException(nameof(manager));
16+
17+
public async Task InvokeAsync(HttpContext context)
18+
{
19+
ArgumentNullException.ThrowIfNull(next);
20+
21+
// If specified, the ?wait={seconds} query param will wait for an
22+
// active script host for that duration. This is to avoid excessive polling
23+
// when waiting for the initial readiness probe.
24+
if (context.Request.Query.TryGetValue("wait", out StringValues wait))
25+
{
26+
if (!int.TryParse(wait.ToString(), out int waitSeconds) || waitSeconds < 0)
27+
{
28+
context.Response.StatusCode = StatusCodes.Status400BadRequest;
29+
await context.Response.WriteAsJsonAsync(
30+
ErrorResponse.BadArgument("'wait' query param must be a positive integer", $"wait={wait}"));
31+
return;
32+
}
33+
34+
await _manager.DelayUntilHostReadyAsync(waitSeconds);
35+
}
36+
37+
await _next(context);
38+
}
39+
}
40+
}

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(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)