Skip to content

Commit 69c7dda

Browse files
authored
[HealthChecks] Add AzureWebJobsStorage health check (#11471)
* Add AzureWebJobsStorage health check * Address copilot review * Add comments to HealthCheckData * Fix unit tests * Add Connectivity tag * Update WebJobs tag * Rename web_jobs -> webjobs
1 parent e6b7158 commit 69c7dda

File tree

8 files changed

+613
-12
lines changed

8 files changed

+613
-12
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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.Collections;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Runtime.CompilerServices;
9+
using Azure;
10+
11+
namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
12+
{
13+
/// <summary>
14+
/// A helper for providing data with a health check result.
15+
/// </summary>
16+
internal partial class HealthCheckData
17+
{
18+
// exposed to the HealthCheckResult through IReadOnlyDictionary.
19+
private readonly Dictionary<string, object> _data = [];
20+
21+
/// <summary>
22+
/// Gets or sets the area of the health check data failure.
23+
/// </summary>
24+
/// <remarks>
25+
/// This is the area that has failed. Such as "configuration", "connectivity", etc.
26+
/// </remarks>
27+
public string Area
28+
{
29+
get => GetOrDefault<string>();
30+
set => Set(value);
31+
}
32+
33+
/// <summary>
34+
/// Gets or sets the configuration section related to the health check data.
35+
/// </summary>
36+
/// <remarks>
37+
/// Useful for when the component being checked is related to a specific configuration section.
38+
/// </remarks>
39+
public string ConfigurationSection
40+
{
41+
get => GetOrDefault<string>();
42+
set => Set(value);
43+
}
44+
45+
/// <summary>
46+
/// Gets or sets the status code related to the health check data.
47+
/// For HTTP related related checks, this is the HTTP status code.
48+
/// </summary>
49+
public int StatusCode
50+
{
51+
get => GetOrDefault<int>();
52+
set => Set(value);
53+
}
54+
55+
/// <summary>
56+
/// Gets or sets the error code related to the health check data.
57+
/// </summary>
58+
/// <remarks>
59+
/// For Azure SDK related checks, this is typically the RequestFailedException.ErrorCode value.
60+
/// </remarks>
61+
public string ErrorCode
62+
{
63+
get => GetOrDefault<string>();
64+
set => Set(value);
65+
}
66+
67+
/// <summary>
68+
/// Sets exception details into the health check data.
69+
/// </summary>
70+
/// <param name="ex">The exception to set details from.</param>
71+
/// <remarks>
72+
/// This will set various properties based on the type of exception.
73+
/// </remarks>
74+
public void SetExceptionDetails(Exception ex)
75+
{
76+
ArgumentNullException.ThrowIfNull(ex);
77+
if (ex is AggregateException aggregate)
78+
{
79+
// Azure SDK will retry a few times in some cases, leading to multiple inner exceptions.
80+
// We only care about the last one.
81+
ex = aggregate.InnerExceptions.Last();
82+
}
83+
84+
if (ex is TimeoutException)
85+
{
86+
ErrorCode = "Timeout";
87+
}
88+
else if (ex is OperationCanceledException)
89+
{
90+
ErrorCode = "OperationCanceled";
91+
}
92+
else if (ex is RequestFailedException rfe)
93+
{
94+
StatusCode = rfe.Status;
95+
ErrorCode = rfe.ErrorCode;
96+
}
97+
}
98+
99+
private void Set<T>(T value, [CallerMemberName] string key = null)
100+
{
101+
_data[key] = value;
102+
}
103+
104+
private T GetOrDefault<T>([CallerMemberName] string key = null, T defaultValue = default)
105+
{
106+
if (_data.TryGetValue(key, out var value) && value is T typedValue)
107+
{
108+
return typedValue;
109+
}
110+
111+
return defaultValue;
112+
}
113+
}
114+
115+
// Partial class down here to separate IReadOnlyDictionary implementation details.
116+
internal partial class HealthCheckData : IReadOnlyDictionary<string, object>
117+
{
118+
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys
119+
=> _data.Keys;
120+
121+
IEnumerable<object> IReadOnlyDictionary<string, object>.Values
122+
=> _data.Values;
123+
124+
int IReadOnlyCollection<KeyValuePair<string, object>>.Count
125+
=> _data.Count;
126+
127+
object IReadOnlyDictionary<string, object>.this[string key]
128+
=> _data[key];
129+
130+
bool IReadOnlyDictionary<string, object>.ContainsKey(string key)
131+
=> _data.ContainsKey(key);
132+
133+
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
134+
=> _data.GetEnumerator();
135+
136+
IEnumerator IEnumerable.GetEnumerator()
137+
=> _data.GetEnumerator();
138+
139+
bool IReadOnlyDictionary<string, object>.TryGetValue(string key, out object value)
140+
=> _data.TryGetValue(key, out value);
141+
}
142+
}

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
77
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.DependencyInjection.Extensions;
89
using Microsoft.Extensions.Diagnostics.HealthChecks;
910
using Microsoft.Extensions.Logging;
1011

@@ -27,6 +28,7 @@ public static IHealthChecksBuilder AddWebJobsScriptHealthChecks(this IHealthChec
2728
builder
2829
.AddWebHostHealthCheck()
2930
.AddScriptHostHealthCheck()
31+
.AddWebJobsStorageHealthCheck()
3032
.AddTelemetryPublisher(HealthCheckTags.Liveness, HealthCheckTags.Readiness)
3133
.UseDynamicHealthCheckService();
3234
return builder;
@@ -126,6 +128,24 @@ public static IHealthChecksBuilder AddScriptHostHealthCheck(this IHealthChecksBu
126128
return builder;
127129
}
128130

131+
/// <summary>
132+
/// Adds a health check for the WebJobs storage account.
133+
/// </summary>
134+
/// <param name="builder">The builder to register health checks with.</param>
135+
/// <returns>The original builder, for call chaining.</returns>
136+
public static IHealthChecksBuilder AddWebJobsStorageHealthCheck(this IHealthChecksBuilder builder)
137+
{
138+
ArgumentNullException.ThrowIfNull(builder);
139+
140+
// Ensure singleton as this health check refreshes in the background.
141+
builder.Services.TryAddSingleton<WebJobsStorageHealthCheck>();
142+
builder.AddCheck<WebJobsStorageHealthCheck>(
143+
HealthCheckNames.WebJobsStorage,
144+
tags: [HealthCheckTags.Configuration, HealthCheckTags.Connectivity, HealthCheckTags.WebJobsStorage],
145+
timeout: TimeSpan.FromSeconds(10));
146+
return builder;
147+
}
148+
129149
/// <summary>
130150
/// Filters a health report to include only specified entries.
131151
/// </summary>

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,10 @@ internal static class HealthCheckNames
1919
/// The 'azure.functions.script_host.lifecycle' check monitors the lifecycle of the script host.
2020
/// </summary>
2121
public const string ScriptHostLifeCycle = Prefix + "script_host.lifecycle";
22+
23+
/// <summary>
24+
/// The 'azure.functions.webjobs.storage' check monitors connectivity to the WebJobs storage account.
25+
/// </summary>
26+
public const string WebJobsStorage = Prefix + "webjobs.storage";
2227
}
2328
}
Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
@@ -8,30 +8,44 @@ namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
88
/// </summary>
99
internal static class HealthCheckTags
1010
{
11-
private const string Prefix = "azure.functions.";
11+
private const string FuncPrefix = "azure.functions.";
12+
private const string WebJobsPrefix = FuncPrefix + "webjobs.";
1213

1314
/// <summary>
1415
/// The 'azure.functions.liveness' tag is used for liveness checks in the Azure Functions host.
1516
/// </summary>
1617
/// <remarks>
1718
/// Liveness checks are used to determine if the host is alive and responsive.
1819
/// </remarks>
19-
public const string Liveness = Prefix + "liveness";
20+
public const string Liveness = FuncPrefix + "liveness";
2021

2122
/// <summary>
2223
/// The 'azure.functions.readiness' tag is used for readiness checks in the Azure Functions host.
2324
/// </summary>
2425
/// <remarks>
2526
/// Readiness checks are used to determine if the host is ready to process requests.
2627
/// </remarks>
27-
public const string Readiness = Prefix + "readiness";
28+
public const string Readiness = FuncPrefix + "readiness";
2829

2930
/// <summary>
3031
/// The 'azure.functions.configuration' tag is used for configuration-related health checks in the Azure Functions host.
3132
/// </summary>
3233
/// <remarks>
3334
/// These are typically customer configuration related, such as configuring AzureWebJobsStorage access.
3435
/// </remarks>
35-
public const string Configuration = Prefix + "configuration";
36+
public const string Configuration = FuncPrefix + "configuration";
37+
38+
/// <summary>
39+
/// The 'azure.functions.connectivity' tag is used for connectivity-related health checks in the Azure Functions host.
40+
/// </summary>
41+
/// <remarks>
42+
/// These are typically related to connectivity to external services, such as Azure Storage.
43+
/// </remarks>
44+
public const string Connectivity = FuncPrefix + "connectivity";
45+
46+
/// <summary>
47+
/// The "azure.functions.webjobs.storage" tag is used for health checks related to the WebJobs storage account.
48+
/// </summary>
49+
public const string WebJobsStorage = WebJobsPrefix + "storage";
3650
}
3751
}

0 commit comments

Comments
 (0)