-
Notifications
You must be signed in to change notification settings - Fork 38
Support health check #644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Support health check #644
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ff455b1
support health check
zhiyuanliang-ms 791bd21
update
zhiyuanliang-ms b8149ed
reord last successful time in ExecuteWithFailoverPolicy
zhiyuanliang-ms 5b3ff0b
make health check compatible with DI
zhiyuanliang-ms fddffdd
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Dotn…
zhiyuanliang-ms 09e5c17
add health check for each provider instance
zhiyuanliang-ms 7101c7c
update
zhiyuanliang-ms b81f0d3
Merge branch 'main' into zhiyuanliang/health-check
zhiyuanliang-ms c6d181a
merge main
zhiyuanliang-ms 825cb39
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Dotn…
zhiyuanliang-ms 58cc831
update comment
zhiyuanliang-ms 57ce70e
Merge branch 'main' into zhiyuanliang/health-check
zhiyuanliang-ms 7f6da05
Merge branch 'main' into zhiyuanliang/health-check
zhiyuanliang-ms File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
75 changes: 75 additions & 0 deletions
75
...rosoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationHealthCheck.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
// | ||
using Microsoft.Extensions.Diagnostics.HealthChecks; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Reflection; | ||
using System.Threading.Tasks; | ||
using System.Threading; | ||
using System.Linq; | ||
|
||
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration | ||
{ | ||
internal class AzureAppConfigurationHealthCheck : IHealthCheck | ||
{ | ||
private static readonly PropertyInfo _propertyInfo = typeof(ChainedConfigurationProvider).GetProperty("Configuration", BindingFlags.Public | BindingFlags.Instance); | ||
private readonly IEnumerable<IHealthCheck> _healthChecks; | ||
|
||
public AzureAppConfigurationHealthCheck(IConfiguration configuration) | ||
{ | ||
if (configuration == null) | ||
{ | ||
throw new ArgumentNullException(nameof(configuration)); | ||
} | ||
|
||
var healthChecks = new List<IHealthCheck>(); | ||
var configurationRoot = configuration as IConfigurationRoot; | ||
FindHealthChecks(configurationRoot, healthChecks); | ||
|
||
_healthChecks = healthChecks; | ||
} | ||
|
||
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) | ||
{ | ||
if (!_healthChecks.Any()) | ||
{ | ||
return HealthCheckResult.Unhealthy(HealthCheckConstants.NoProviderFoundMessage); | ||
} | ||
|
||
foreach (IHealthCheck healthCheck in _healthChecks) | ||
{ | ||
var result = await healthCheck.CheckHealthAsync(context, cancellationToken).ConfigureAwait(false); | ||
|
||
if (result.Status == HealthStatus.Unhealthy) | ||
{ | ||
return result; | ||
} | ||
} | ||
|
||
return HealthCheckResult.Healthy(); | ||
} | ||
|
||
private void FindHealthChecks(IConfigurationRoot configurationRoot, List<IHealthCheck> healthChecks) | ||
{ | ||
if (configurationRoot != null) | ||
{ | ||
foreach (IConfigurationProvider provider in configurationRoot.Providers) | ||
{ | ||
if (provider is AzureAppConfigurationProvider appConfigurationProvider) | ||
{ | ||
healthChecks.Add(appConfigurationProvider); | ||
} | ||
else if (provider is ChainedConfigurationProvider chainedProvider) | ||
{ | ||
if (_propertyInfo != null) | ||
{ | ||
var chainedProviderConfigurationRoot = _propertyInfo.GetValue(chainedProvider) as IConfigurationRoot; | ||
FindHealthChecks(chainedProviderConfigurationRoot, healthChecks); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
...Configuration.AzureAppConfiguration/AzureAppConfigurationHealthChecksBuilderExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
// | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Configuration.AzureAppConfiguration; | ||
using Microsoft.Extensions.Diagnostics.HealthChecks; | ||
using System; | ||
using System.Collections.Generic; | ||
|
||
namespace Microsoft.Extensions.DependencyInjection | ||
zhiyuanliang-ms marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
/// <summary> | ||
/// Extension methods to configure <see cref="AzureAppConfigurationHealthCheck"/>. | ||
/// </summary> | ||
public static class AzureAppConfigurationHealthChecksBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Add a health check for Azure App Configuration to given <paramref name="builder"/>. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add <see cref="HealthCheckRegistration"/> to.</param> | ||
/// <param name="factory"> A factory to obtain <see cref="IConfiguration"/> instance.</param> | ||
/// <param name="name">The health check name.</param> | ||
/// <param name="failureStatus">The <see cref="HealthStatus"/> that should be reported when the health check fails.</param> | ||
/// <param name="tags">A list of tags that can be used to filter sets of health checks.</param> | ||
/// <param name="timeout">A <see cref="TimeSpan"/> representing the timeout of the check.</param> | ||
/// <returns>The provided health checks builder.</returns> | ||
public static IHealthChecksBuilder AddAzureAppConfiguration( | ||
this IHealthChecksBuilder builder, | ||
Func<IServiceProvider, IConfiguration> factory = default, | ||
string name = HealthCheckConstants.HealthCheckRegistrationName, | ||
HealthStatus failureStatus = default, | ||
IEnumerable<string> tags = default, | ||
TimeSpan? timeout = default) | ||
{ | ||
return builder.Add(new HealthCheckRegistration( | ||
name ?? HealthCheckConstants.HealthCheckRegistrationName, | ||
sp => new AzureAppConfigurationHealthCheck( | ||
factory?.Invoke(sp) ?? sp.GetRequiredService<IConfiguration>()), | ||
failureStatus, | ||
tags, | ||
timeout)); | ||
} | ||
} | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
...icrosoft.Extensions.Configuration.AzureAppConfiguration/Constants/HealthCheckConstants.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
// | ||
|
||
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration | ||
{ | ||
internal class HealthCheckConstants | ||
{ | ||
public const string HealthCheckRegistrationName = "Microsoft.Extensions.Configuration.AzureAppConfiguration"; | ||
public const string NoProviderFoundMessage = "No configuration provider is found."; | ||
public const string LoadNotCompletedMessage = "The initial load is not completed."; | ||
public const string RefreshFailedMessage = "The last refresh attempt failed."; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
// | ||
using Azure; | ||
using Azure.Data.AppConfiguration; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Configuration.AzureAppConfiguration; | ||
using Microsoft.Extensions.Diagnostics.HealthChecks; | ||
using Moq; | ||
using System.Threading; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
using System; | ||
using System.Linq; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Tests.AzureAppConfiguration | ||
{ | ||
public class HealthCheckTest | ||
{ | ||
readonly List<ConfigurationSetting> kvCollection = new List<ConfigurationSetting> | ||
{ | ||
ConfigurationModelFactory.ConfigurationSetting("TestKey1", "TestValue1", "label", | ||
eTag: new ETag("0a76e3d7-7ec1-4e37-883c-9ea6d0d89e63"), | ||
contentType:"text"), | ||
ConfigurationModelFactory.ConfigurationSetting("TestKey2", "TestValue2", "label", | ||
eTag: new ETag("31c38369-831f-4bf1-b9ad-79db56c8b989"), | ||
contentType: "text"), | ||
ConfigurationModelFactory.ConfigurationSetting("TestKey3", "TestValue3", "label", | ||
|
||
eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"), | ||
contentType: "text"), | ||
ConfigurationModelFactory.ConfigurationSetting("TestKey4", "TestValue4", "label", | ||
eTag: new ETag("3ca43b3e-d544-4b0c-b3a2-e7a7284217a2"), | ||
contentType: "text"), | ||
}; | ||
|
||
[Fact] | ||
public async Task HealthCheckTests_ReturnsHealthyWhenInitialLoadIsCompleted() | ||
{ | ||
var mockResponse = new Mock<Response>(); | ||
var mockClient = new Mock<ConfigurationClient>(MockBehavior.Strict); | ||
|
||
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>())) | ||
.Returns(new MockAsyncPageable(kvCollection)); | ||
|
||
var config = new ConfigurationBuilder() | ||
.AddAzureAppConfiguration(options => | ||
{ | ||
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); | ||
}) | ||
.Build(); | ||
|
||
IHealthCheck healthCheck = new AzureAppConfigurationHealthCheck(config); | ||
|
||
Assert.True(config["TestKey1"] == "TestValue1"); | ||
var result = await healthCheck.CheckHealthAsync(new HealthCheckContext()); | ||
Assert.Equal(HealthStatus.Healthy, result.Status); | ||
} | ||
|
||
[Fact] | ||
public async Task HealthCheckTests_ReturnsUnhealthyWhenRefreshFailed() | ||
{ | ||
IConfigurationRefresher refresher = null; | ||
var mockResponse = new Mock<Response>(); | ||
var mockClient = new Mock<ConfigurationClient>(MockBehavior.Strict); | ||
|
||
mockClient.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>())) | ||
.Returns(new MockAsyncPageable(kvCollection)) | ||
.Throws(new RequestFailedException(503, "Request failed.")) | ||
.Returns(new MockAsyncPageable(Enumerable.Empty<ConfigurationSetting>().ToList())) | ||
.Returns(new MockAsyncPageable(Enumerable.Empty<ConfigurationSetting>().ToList())); | ||
|
||
var config = new ConfigurationBuilder() | ||
.AddAzureAppConfiguration(options => | ||
{ | ||
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); | ||
options.MinBackoffDuration = TimeSpan.FromSeconds(2); | ||
options.ConfigurationSettingPageIterator = new MockConfigurationSettingPageIterator(); | ||
options.ConfigureRefresh(refreshOptions => | ||
{ | ||
refreshOptions.RegisterAll() | ||
.SetRefreshInterval(TimeSpan.FromSeconds(1)); | ||
}); | ||
refresher = options.GetRefresher(); | ||
}) | ||
.Build(); | ||
|
||
IHealthCheck healthCheck = new AzureAppConfigurationHealthCheck(config); | ||
|
||
var result = await healthCheck.CheckHealthAsync(new HealthCheckContext()); | ||
Assert.Equal(HealthStatus.Healthy, result.Status); | ||
|
||
// Wait for the refresh interval to expire | ||
Thread.Sleep(1000); | ||
|
||
await refresher.TryRefreshAsync(); | ||
result = await healthCheck.CheckHealthAsync(new HealthCheckContext()); | ||
Assert.Equal(HealthStatus.Unhealthy, result.Status); | ||
|
||
// Wait for client backoff to end | ||
Thread.Sleep(3000); | ||
|
||
await refresher.RefreshAsync(); | ||
result = await healthCheck.CheckHealthAsync(new HealthCheckContext()); | ||
Assert.Equal(HealthStatus.Healthy, result.Status); | ||
} | ||
|
||
[Fact] | ||
public async Task HealthCheckTests_RegisterAzureAppConfigurationHealthCheck() | ||
{ | ||
var mockResponse = new Mock<Response>(); | ||
var mockClient = new Mock<ConfigurationClient>(MockBehavior.Strict); | ||
|
||
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>())) | ||
.Returns(new MockAsyncPageable(kvCollection)); | ||
|
||
var config = new ConfigurationBuilder() | ||
.AddAzureAppConfiguration(options => | ||
{ | ||
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); | ||
}) | ||
.Build(); | ||
|
||
var services = new ServiceCollection(); | ||
services.AddSingleton<IConfiguration>(config); | ||
services.AddLogging(); // add logging for health check service | ||
services.AddHealthChecks() | ||
.AddAzureAppConfiguration(); | ||
var provider = services.BuildServiceProvider(); | ||
var healthCheckService = provider.GetRequiredService<HealthCheckService>(); | ||
|
||
var result = await healthCheckService.CheckHealthAsync(); | ||
Assert.Equal(HealthStatus.Healthy, result.Status); | ||
Assert.Contains(HealthCheckConstants.HealthCheckRegistrationName, result.Entries.Keys); | ||
Assert.Equal(HealthStatus.Healthy, result.Entries[HealthCheckConstants.HealthCheckRegistrationName].Status); | ||
} | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.