Skip to content

Commit a992b59

Browse files
Merge pull request #670 from Azure/merge-main-to-preview
Merge main to preview
2 parents 17ed39c + b721c9e commit a992b59

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3585
-303
lines changed

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# If there are abnormal line endings in any file, run "git add --renormalize <file_name>",
2+
# review the changes, and commit them to fix the line endings.
3+
* text=auto

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ on:
1414

1515
permissions:
1616
security-events: write
17+
id-token: write
1718

1819
jobs:
1920
build:
@@ -40,8 +41,17 @@ jobs:
4041
- name: Dotnet Pack
4142
run: pwsh pack.ps1
4243

44+
- name: Azure Login with OIDC
45+
uses: azure/login@v1
46+
with:
47+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
48+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
49+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
50+
4351
- name: Dotnet Test
4452
run: pwsh test.ps1
53+
env:
54+
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
4555

4656
- name: Publish Test Results
4757
uses: actions/upload-artifact@v4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
# Azure Functions localsettings file
77
local.settings.json
88

9+
# Integration test secrets
10+
appsettings.Secrets.json
11+
912
# User-specific files
1013
*.suo
1114
*.user

NOTICE

Lines changed: 226 additions & 226 deletions
Large diffs are not rendered by default.

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationClientFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public ConfigurationClient CreateClient(string endpoint)
5959
string connectionString = _connectionStrings.FirstOrDefault(cs => ConnectionStringUtils.Parse(cs, ConnectionStringUtils.EndpointSection) == endpoint);
6060

6161
//
62-
// falback to the first connection string
62+
// fallback to the first connection string
6363
if (connectionString == null)
6464
{
6565
string id = ConnectionStringUtils.Parse(_connectionStrings.First(), ConnectionStringUtils.IdSection);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Microsoft.Extensions.Diagnostics.HealthChecks;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Reflection;
8+
using System.Threading.Tasks;
9+
using System.Threading;
10+
using System.Linq;
11+
12+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
13+
{
14+
internal class AzureAppConfigurationHealthCheck : IHealthCheck
15+
{
16+
private static readonly PropertyInfo _propertyInfo = typeof(ChainedConfigurationProvider).GetProperty("Configuration", BindingFlags.Public | BindingFlags.Instance);
17+
private readonly IEnumerable<IHealthCheck> _healthChecks;
18+
19+
public AzureAppConfigurationHealthCheck(IConfiguration configuration)
20+
{
21+
if (configuration == null)
22+
{
23+
throw new ArgumentNullException(nameof(configuration));
24+
}
25+
26+
var healthChecks = new List<IHealthCheck>();
27+
var configurationRoot = configuration as IConfigurationRoot;
28+
FindHealthChecks(configurationRoot, healthChecks);
29+
30+
_healthChecks = healthChecks;
31+
}
32+
33+
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
34+
{
35+
if (!_healthChecks.Any())
36+
{
37+
return HealthCheckResult.Unhealthy(HealthCheckConstants.NoProviderFoundMessage);
38+
}
39+
40+
foreach (IHealthCheck healthCheck in _healthChecks)
41+
{
42+
var result = await healthCheck.CheckHealthAsync(context, cancellationToken).ConfigureAwait(false);
43+
44+
if (result.Status == HealthStatus.Unhealthy)
45+
{
46+
return result;
47+
}
48+
}
49+
50+
return HealthCheckResult.Healthy();
51+
}
52+
53+
private void FindHealthChecks(IConfigurationRoot configurationRoot, List<IHealthCheck> healthChecks)
54+
{
55+
if (configurationRoot != null)
56+
{
57+
foreach (IConfigurationProvider provider in configurationRoot.Providers)
58+
{
59+
if (provider is AzureAppConfigurationProvider appConfigurationProvider)
60+
{
61+
healthChecks.Add(appConfigurationProvider);
62+
}
63+
else if (provider is ChainedConfigurationProvider chainedProvider)
64+
{
65+
if (_propertyInfo != null)
66+
{
67+
var chainedProviderConfigurationRoot = _propertyInfo.GetValue(chainedProvider) as IConfigurationRoot;
68+
FindHealthChecks(chainedProviderConfigurationRoot, healthChecks);
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
75+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
6+
using Microsoft.Extensions.Diagnostics.HealthChecks;
7+
using System;
8+
using System.Collections.Generic;
9+
10+
namespace Microsoft.Extensions.DependencyInjection
11+
{
12+
/// <summary>
13+
/// Extension methods to configure <see cref="AzureAppConfigurationHealthCheck"/>.
14+
/// </summary>
15+
public static class AzureAppConfigurationHealthChecksBuilderExtensions
16+
{
17+
/// <summary>
18+
/// Add a health check for Azure App Configuration to given <paramref name="builder"/>.
19+
/// </summary>
20+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add <see cref="HealthCheckRegistration"/> to.</param>
21+
/// <param name="factory"> A factory to obtain <see cref="IConfiguration"/> instance.</param>
22+
/// <param name="name">The health check name.</param>
23+
/// <param name="failureStatus">The <see cref="HealthStatus"/> that should be reported when the health check fails.</param>
24+
/// <param name="tags">A list of tags that can be used to filter sets of health checks.</param>
25+
/// <param name="timeout">A <see cref="TimeSpan"/> representing the timeout of the check.</param>
26+
/// <returns>The provided health checks builder.</returns>
27+
public static IHealthChecksBuilder AddAzureAppConfiguration(
28+
this IHealthChecksBuilder builder,
29+
Func<IServiceProvider, IConfiguration> factory = default,
30+
string name = HealthCheckConstants.HealthCheckRegistrationName,
31+
HealthStatus failureStatus = default,
32+
IEnumerable<string> tags = default,
33+
TimeSpan? timeout = default)
34+
{
35+
return builder.Add(new HealthCheckRegistration(
36+
name ?? HealthCheckConstants.HealthCheckRegistrationName,
37+
sp => new AzureAppConfigurationHealthCheck(
38+
factory?.Invoke(sp) ?? sp.GetRequiredService<IConfiguration>()),
39+
failureStatus,
40+
tags,
41+
timeout));
42+
}
43+
}
44+
}
45+

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT license.
33
//
44
using Azure.Core;
5-
using Azure.Core.Pipeline;
65
using Azure.Data.AppConfiguration;
76
using Microsoft.Extensions.Azure;
87
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
@@ -12,7 +11,6 @@
1211
using System;
1312
using System.Collections.Generic;
1413
using System.Linq;
15-
using System.Net.Http;
1614
using System.Threading.Tasks;
1715

1816
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
@@ -205,7 +203,14 @@ public AzureAppConfigurationOptions SetClientFactory(IAzureClientFactory<Configu
205203
/// The label filter to apply when querying Azure App Configuration for key-values. By default the null label will be used. Built-in label filter options: <see cref="LabelFilter"/>
206204
/// The characters asterisk (*) and comma (,) are not supported. Backslash (\) character is reserved and must be escaped using another backslash (\).
207205
/// </param>
208-
public AzureAppConfigurationOptions Select(string keyFilter, string labelFilter = LabelFilter.Null)
206+
/// <param name="tagFilters">
207+
/// In addition to key and label filters, key-values from Azure App Configuration can be filtered based on their tag names and values.
208+
/// Each tag filter must follow the format "tagName=tagValue". Only those key-values will be loaded whose tags match all the tags provided here.
209+
/// Built in tag filter values: <see cref="TagValue"/>. For example, $"tagName={<see cref="TagValue.Null"/>}".
210+
/// The characters asterisk (*), comma (,) and backslash (\) are reserved and must be escaped using a backslash (\).
211+
/// Up to 5 tag filters can be provided. If no tag filters are provided, key-values will not be filtered based on tags.
212+
/// </param>
213+
public AzureAppConfigurationOptions Select(string keyFilter, string labelFilter = LabelFilter.Null, IEnumerable<string> tagFilters = null)
209214
{
210215
if (string.IsNullOrEmpty(keyFilter))
211216
{
@@ -223,6 +228,17 @@ public AzureAppConfigurationOptions Select(string keyFilter, string labelFilter
223228
labelFilter = LabelFilter.Null;
224229
}
225230

231+
if (tagFilters != null)
232+
{
233+
foreach (string tag in tagFilters)
234+
{
235+
if (string.IsNullOrEmpty(tag) || !tag.Contains('=') || tag.IndexOf('=') == 0)
236+
{
237+
throw new ArgumentException($"Tag filter '{tag}' does not follow the format \"tagName=tagValue\".", nameof(tagFilters));
238+
}
239+
}
240+
}
241+
226242
if (!_selectCalled)
227243
{
228244
_selectors.Remove(DefaultQuery);
@@ -233,7 +249,8 @@ public AzureAppConfigurationOptions Select(string keyFilter, string labelFilter
233249
_selectors.AppendUnique(new KeyValueSelector
234250
{
235251
KeyFilter = keyFilter,
236-
LabelFilter = labelFilter
252+
LabelFilter = labelFilter,
253+
TagFilters = tagFilters
237254
});
238255

239256
return this;
@@ -307,6 +324,7 @@ public AzureAppConfigurationOptions UseFeatureFlags(Action<FeatureFlagOptions> c
307324
{
308325
Key = featureFlagSelector.KeyFilter,
309326
Label = featureFlagSelector.LabelFilter,
327+
Tags = featureFlagSelector.TagFilters,
310328
// If UseFeatureFlags is called multiple times for the same key and label filters, last refresh interval wins
311329
RefreshInterval = options.RefreshInterval
312330
});
@@ -528,15 +546,12 @@ public AzureAppConfigurationOptions ConfigureStartupOptions(Action<StartupOption
528546

529547
private static ConfigurationClientOptions GetDefaultClientOptions()
530548
{
531-
var clientOptions = new ConfigurationClientOptions(ConfigurationClientOptions.ServiceVersion.V2023_10_01);
549+
var clientOptions = new ConfigurationClientOptions(ConfigurationClientOptions.ServiceVersion.V2023_11_01);
532550
clientOptions.Retry.MaxRetries = MaxRetries;
533551
clientOptions.Retry.MaxDelay = MaxRetryDelay;
534552
clientOptions.Retry.Mode = RetryMode.Exponential;
553+
clientOptions.Retry.NetworkTimeout = NetworkTimeout;
535554
clientOptions.AddPolicy(new UserAgentHeaderPolicy(), HttpPipelinePosition.PerCall);
536-
clientOptions.Transport = new HttpClientTransport(new HttpClient()
537-
{
538-
Timeout = NetworkTimeout
539-
});
540555

541556
return clientOptions;
542557
}

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Azure.Data.AppConfiguration;
66
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
77
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models;
8+
using Microsoft.Extensions.Diagnostics.HealthChecks;
89
using Microsoft.Extensions.Logging;
910
using System;
1011
using System.Collections.Generic;
@@ -21,8 +22,9 @@
2122

2223
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
2324
{
24-
internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigurationRefresher, IDisposable
25+
internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigurationRefresher, IHealthCheck, IDisposable
2526
{
27+
private readonly ActivitySource _activitySource = new ActivitySource(ActivityNames.AzureAppConfigurationActivitySource);
2628
private bool _optional;
2729
private bool _isInitialLoadComplete = false;
2830
private bool _isAssemblyInspected;
@@ -52,6 +54,10 @@ internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigura
5254
private Logger _logger = new Logger();
5355
private ILoggerFactory _loggerFactory;
5456

57+
// For health check
58+
private DateTimeOffset? _lastSuccessfulAttempt = null;
59+
private DateTimeOffset? _lastFailedAttempt = null;
60+
5561
private class ConfigurationClientBackoffStatus
5662
{
5763
public int FailedAttempts { get; set; }
@@ -158,7 +164,7 @@ public AzureAppConfigurationProvider(IConfigurationClientManager configClientMan
158164
public override void Load()
159165
{
160166
var watch = Stopwatch.StartNew();
161-
167+
using Activity activity = _activitySource.StartActivity(ActivityNames.Load);
162168
try
163169
{
164170
using var startupCancellationTokenSource = new CancellationTokenSource(_options.Startup.Timeout);
@@ -255,9 +261,12 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
255261

256262
_logger.LogDebug(LogHelper.BuildRefreshSkippedNoClientAvailableMessage());
257263

264+
_lastFailedAttempt = DateTime.UtcNow;
265+
258266
return;
259267
}
260268

269+
using Activity activity = _activitySource.StartActivity(ActivityNames.Refresh);
261270
// Check if initial configuration load had failed
262271
if (_mappedData == null)
263272
{
@@ -344,6 +353,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
344353
{
345354
KeyFilter = watcher.Key,
346355
LabelFilter = watcher.Label,
356+
TagFilters = watcher.Tags,
347357
IsFeatureFlagSelector = true
348358
}),
349359
_ffEtags,
@@ -568,6 +578,22 @@ public void ProcessPushNotification(PushNotification pushNotification, TimeSpan?
568578
}
569579
}
570580

581+
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
582+
{
583+
if (!_lastSuccessfulAttempt.HasValue)
584+
{
585+
return HealthCheckResult.Unhealthy(HealthCheckConstants.LoadNotCompletedMessage);
586+
}
587+
588+
if (_lastFailedAttempt.HasValue &&
589+
_lastSuccessfulAttempt.Value < _lastFailedAttempt.Value)
590+
{
591+
return HealthCheckResult.Unhealthy(HealthCheckConstants.RefreshFailedMessage);
592+
}
593+
594+
return HealthCheckResult.Healthy();
595+
}
596+
571597
private void SetDirty(TimeSpan? maxDelay)
572598
{
573599
DateTimeOffset nextRefreshTime = AddRandomDelay(DateTimeOffset.UtcNow, maxDelay ?? DefaultMaxSetDirtyDelay);
@@ -826,6 +852,14 @@ private async Task<Dictionary<string, ConfigurationSetting>> LoadSelected(
826852
LabelFilter = loadOption.LabelFilter
827853
};
828854

855+
if (loadOption.TagFilters != null)
856+
{
857+
foreach (string tagFilter in loadOption.TagFilters)
858+
{
859+
selector.TagsFilter.Add(tagFilter);
860+
}
861+
}
862+
829863
var matchConditions = new List<MatchConditions>();
830864

831865
await CallWithRequestTracing(async () =>
@@ -1147,6 +1181,7 @@ private async Task<T> ExecuteWithFailOverPolicyAsync<T>(
11471181
success = true;
11481182

11491183
_lastSuccessfulEndpoint = _configClientManager.GetEndpointForClient(currentClient);
1184+
_lastSuccessfulAttempt = DateTime.UtcNow;
11501185

11511186
return result;
11521187
}
@@ -1172,6 +1207,7 @@ private async Task<T> ExecuteWithFailOverPolicyAsync<T>(
11721207
{
11731208
if (!success && backoffAllClients)
11741209
{
1210+
_lastFailedAttempt = DateTime.UtcNow;
11751211
_logger.LogWarning(LogHelper.BuildLastEndpointFailedMessage(previousEndpoint?.ToString()));
11761212

11771213
do
@@ -1221,9 +1257,7 @@ await ExecuteWithFailOverPolicyAsync<object>(clients, async (client) =>
12211257

12221258
private bool IsFailOverable(AggregateException ex)
12231259
{
1224-
TaskCanceledException tce = ex.InnerExceptions?.LastOrDefault(e => e is TaskCanceledException) as TaskCanceledException;
1225-
1226-
if (tce != null && tce.InnerException is TimeoutException)
1260+
if (ex.InnerExceptions?.Any(e => e is TaskCanceledException) == true)
12271261
{
12281262
return true;
12291263
}
@@ -1413,6 +1447,7 @@ private async Task ProcessKeyValueChangesAsync(
14131447
public void Dispose()
14141448
{
14151449
(_configClientManager as ConfigurationClientManager)?.Dispose();
1450+
_activitySource.Dispose();
14161451
}
14171452
}
14181453
}

0 commit comments

Comments
 (0)