Skip to content

Commit 3a7af26

Browse files
mrm9084avanigupta
andauthored
App Config Provider - Update telemetry (Azure#35556)
* Update async_key_vault_reference_provided_clients_sample.py * Update refresh_sample_feature_flags.py * UpdateTelemetry * Updating to constants * Fixing pylint * Update assets.json * Update assets.json * Update conftest.py * Update conftest.py * Update setup.py * formatting * Fixing order * Fixed formatting. Updated readme. * fixing len check * Update _azureappconfigurationproviderasync.py * Update sdk/appconfiguration/azure-appconfiguration-provider/README.md Co-authored-by: Avani Gupta <[email protected]> * review changes --------- Co-authored-by: Avani Gupta <[email protected]>
1 parent e02d1f1 commit 3a7af26

File tree

12 files changed

+190
-102
lines changed

12 files changed

+190
-102
lines changed

sdk/appconfiguration/azure-appconfiguration-provider/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Currently the Azure App Configuration Provider enables:
4242

4343
* Connecting to an App Configuration Store using a connection string or Azure Active Directory.
4444
* Selecting multiple sets of configurations using `SettingSelector`.
45+
* Loading Feature Flags
4546
* Dynamic Refresh
4647
* Trim prefixes off key names.
4748
* Resolving Key Vault References, requires AAD.
@@ -53,7 +54,6 @@ Currently the Azure App Configuration Provider enables:
5354
List of features we are going to add to the Python Provider in the future.
5455

5556
* Geo-Replication support
56-
* Feature Management
5757
* Configuration Placeholders
5858

5959
## Examples
@@ -178,7 +178,7 @@ alpha = config["feature_management"]["feature_flags"]["Alpha"]
178178
print(alpha["enabled"])
179179
```
180180

181-
By default all feature flags with no label are loaded. If you want to load feature flags with a specific label you can use `SettingSelector` to filter the feature flags.
181+
By default all feature flags with no label are loaded when `feature_flags_enabled` is set to `True`. . If you want to load feature flags with a specific label you can use `SettingSelector` to filter the feature flags.
182182

183183
```python
184184
from azure.appconfiguration.provider import load, SettingSelector
@@ -192,6 +192,9 @@ To enable refresh for feature flags you need to enable refresh. This will allow
192192

193193
```python
194194
config = load(endpoint=endpoint, credential=DefaultAzureCredential(), feature_flags_enabled=True, feature_flag_refresh_enabled=True)
195+
196+
...
197+
195198
config.refresh()
196199
```
197200

sdk/appconfiguration/azure-appconfiguration-provider/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/appconfiguration/azure-appconfiguration-provider",
5-
"Tag": "python/appconfiguration/azure-appconfiguration-provider_8a49e8ba1e"
5+
"Tag": "python/appconfiguration/azure-appconfiguration-provider_d412917208"
66
}

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py

Lines changed: 107 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import random
99
import time
1010
import datetime
11+
from importlib.metadata import version, PackageNotFoundError
1112
from threading import Lock
1213
import logging
1314
from typing import (
@@ -48,6 +49,13 @@
4849
ContainerAppEnvironmentVariable,
4950
KubernetesEnvironmentVariable,
5051
EMPTY_LABEL,
52+
PERCENTAGE_FILTER_NAMES,
53+
TIME_WINDOW_FILTER_NAMES,
54+
TARGETING_FILTER_NAMES,
55+
CUSTOM_FILTER_KEY,
56+
PERCENTAGE_FILTER_KEY,
57+
TIME_WINDOW_FILTER_KEY,
58+
TARGETING_FILTER_KEY,
5159
)
5260
from ._user_agent import USER_AGENT
5361

@@ -62,7 +70,7 @@
6270

6371

6472
@overload
65-
def load(
73+
def load( # pylint: disable=docstring-keyword-should-match-keyword-only
6674
endpoint: str,
6775
credential: "TokenCredential",
6876
*,
@@ -102,25 +110,25 @@ def load(
102110
:paramtype refresh_on: List[Tuple[str, str]]
103111
:keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a
104112
service call to update the settings. Default value is 30 seconds.
105-
:paramtype on_refresh_success: Optional[Callable]
106113
:keyword on_refresh_success: Optional callback to be invoked when a change is found and a successful refresh has
107114
happened.
108-
:paramtype on_refresh_error: Optional[Callable[[Exception], None]]
115+
:paramtype on_refresh_success: Optional[Callable]
109116
:keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not
110117
specified, errors will be raised.
111-
:paramtype feature_flag_enabled: bool
118+
:paramtype on_refresh_error: Optional[Callable[[Exception], None]]
112119
:keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False.
113-
:paramtype feature_flag_selectors: List[SettingSelector]
120+
:paramtype feature_flag_enabled: bool
114121
:keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all
115122
feature flags without a label.
116-
:paramtype feature_flag_refresh_enabled: bool
123+
:paramtype feature_flag_selectors: List[SettingSelector]
117124
:keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is
118125
False.
126+
:paramtype feature_flag_refresh_enabled: bool
119127
"""
120128

121129

122130
@overload
123-
def load(
131+
def load( # pylint: disable=docstring-keyword-should-match-keyword-only
124132
*,
125133
connection_string: str,
126134
selects: Optional[List[SettingSelector]] = None,
@@ -157,20 +165,20 @@ def load(
157165
:paramtype refresh_on: List[Tuple[str, str]]
158166
:keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a
159167
service call to update the settings. Default value is 30 seconds.
160-
:paramtype on_refresh_success: Optional[Callable]
161168
:keyword on_refresh_success: Optional callback to be invoked when a change is found and a successful refresh has
162169
happened.
163-
:paramtype on_refresh_error: Optional[Callable[[Exception], None]]
170+
:paramtype on_refresh_success: Optional[Callable]
164171
:keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not
165172
specified, errors will be raised.
166-
:paramtype feature_flag_enabled: bool
173+
:paramtype on_refresh_error: Optional[Callable[[Exception], None]]
167174
:keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False.
168-
:paramtype feature_flag_selectors: List[SettingSelector]
175+
:paramtype feature_flag_enabled: bool
169176
:keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all
170177
feature flags without a label.
171-
:paramtype feature_flag_refresh_enabled: bool
178+
:paramtype feature_flag_selectors: List[SettingSelector]
172179
:keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is
173180
False.
181+
:paramtype feature_flag_refresh_enabled: bool
174182
"""
175183

176184

@@ -260,32 +268,63 @@ def _delay_failure(start_time: datetime.datetime) -> None:
260268

261269
def _get_headers(request_type, **kwargs) -> str:
262270
headers = kwargs.pop("headers", {})
263-
if os.environ.get(REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, default="").lower() != "true":
264-
correlation_context = "RequestType=" + request_type
265-
if (
266-
"keyvault_credential" in kwargs
267-
or "keyvault_client_configs" in kwargs
268-
or "secret_resolver" in kwargs
269-
or kwargs.pop("uses_key_vault", False)
270-
):
271-
correlation_context += ",UsesKeyVault"
272-
host_type = ""
273-
if AzureFunctionEnvironmentVariable in os.environ:
274-
host_type = "AzureFunction"
275-
elif AzureWebAppEnvironmentVariable in os.environ:
276-
host_type = "AzureWebApp"
277-
elif ContainerAppEnvironmentVariable in os.environ:
278-
host_type = "ContainerApp"
279-
elif KubernetesEnvironmentVariable in os.environ:
280-
host_type = "Kubernetes"
281-
elif ServiceFabricEnvironmentVariable in os.environ:
282-
host_type = "ServiceFabric"
283-
if host_type:
284-
correlation_context += ",Host=" + host_type
285-
headers["Correlation-Context"] = correlation_context
271+
if os.environ.get(REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, default="").lower() == "true":
272+
return headers
273+
correlation_context = "RequestType=" + request_type
274+
275+
if "feature_filters_used" in kwargs:
276+
filters_used = ""
277+
feature_filters_used = kwargs.pop("feature_filters_used", {})
278+
if CUSTOM_FILTER_KEY in feature_filters_used:
279+
filters_used = CUSTOM_FILTER_KEY
280+
if PERCENTAGE_FILTER_KEY in feature_filters_used:
281+
filters_used += ("+" if filters_used else "") + PERCENTAGE_FILTER_KEY
282+
if TIME_WINDOW_FILTER_KEY in feature_filters_used:
283+
filters_used += ("+" if filters_used else "") + TIME_WINDOW_FILTER_KEY
284+
if TARGETING_FILTER_KEY in feature_filters_used:
285+
filters_used += ("+" if filters_used else "") + TARGETING_FILTER_KEY
286+
correlation_context += ",Filters=" + filters_used
287+
288+
correlation_context += _uses_feature_flags(**kwargs)
289+
290+
if (
291+
"keyvault_credential" in kwargs
292+
or "keyvault_client_configs" in kwargs
293+
or "secret_resolver" in kwargs
294+
or kwargs.pop("uses_key_vault", False)
295+
):
296+
correlation_context += ",UsesKeyVault"
297+
host_type = ""
298+
if AzureFunctionEnvironmentVariable in os.environ:
299+
host_type = "AzureFunction"
300+
elif AzureWebAppEnvironmentVariable in os.environ:
301+
host_type = "AzureWebApp"
302+
elif ContainerAppEnvironmentVariable in os.environ:
303+
host_type = "ContainerApp"
304+
elif KubernetesEnvironmentVariable in os.environ:
305+
host_type = "Kubernetes"
306+
elif ServiceFabricEnvironmentVariable in os.environ:
307+
host_type = "ServiceFabric"
308+
if host_type:
309+
correlation_context += ",Host=" + host_type
310+
311+
headers["Correlation-Context"] = correlation_context
286312
return headers
287313

288314

315+
def _uses_feature_flags(**kwargs):
316+
if not kwargs.pop("uses_feature_flags", False):
317+
return ""
318+
package_name = "featuremanagement"
319+
try:
320+
feature_management_version = version(package_name)
321+
if feature_management_version:
322+
return ",FMPyVer=" + feature_management_version
323+
except PackageNotFoundError:
324+
pass
325+
return ""
326+
327+
289328
def _buildprovider(
290329
connection_string: Optional[str], endpoint: Optional[str], credential: Optional["TokenCredential"], **kwargs
291330
) -> "AzureAppConfigurationProvider":
@@ -469,14 +508,15 @@ def __init__(self, **kwargs) -> None:
469508
self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {})
470509
self._uses_key_vault = (
471510
self._keyvault_credential is not None
472-
or self._keyvault_client_configs is not None
511+
or (self._keyvault_client_configs is not None and len(self._keyvault_client_configs) > 0)
473512
or self._secret_resolver is not None
474513
)
475514
self._feature_flag_enabled = kwargs.pop("feature_flag_enabled", False)
476515
self._feature_flag_selectors = kwargs.pop("feature_flag_selectors", [SettingSelector(key_filter="*")])
477516
self._refresh_on_feature_flags: Mapping[Tuple[str, str], Optional[str]] = {}
478517
self._feature_flag_refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs)
479518
self._feature_flag_refresh_enabled = kwargs.pop("feature_flag_refresh_enabled", False)
519+
self._feature_filter_usage: Mapping[str, bool] = {}
480520
self._update_lock = Lock()
481521
self._refresh_lock = Lock()
482522

@@ -516,7 +556,13 @@ def refresh(self, **kwargs) -> None:
516556
def _refresh_configuration_settings(self, **kwargs) -> bool:
517557
need_refresh = False
518558
updated_sentinel_keys = dict(self._refresh_on)
519-
headers = _get_headers("Watch", uses_key_vault=self._uses_key_vault, **kwargs)
559+
headers = _get_headers(
560+
"Watch",
561+
uses_key_vault=self._uses_key_vault,
562+
feature_filters_used=self._feature_filter_usage,
563+
uses_feature_flags=self._feature_flag_enabled,
564+
**kwargs
565+
)
520566
for (key, label), etag in updated_sentinel_keys.items():
521567
changed, updated_sentinel = self._check_configuration_setting(
522568
key=key, label=label, etag=etag, headers=headers, **kwargs
@@ -537,7 +583,13 @@ def _refresh_configuration_settings(self, **kwargs) -> bool:
537583

538584
def _refresh_feature_flags(self, **kwargs) -> bool:
539585
feature_flag_sentinel_keys = dict(self._refresh_on_feature_flags)
540-
headers = _get_headers("Watch", uses_key_vault=self._uses_key_vault, **kwargs)
586+
headers = _get_headers(
587+
"Watch",
588+
uses_key_vault=self._uses_key_vault,
589+
feature_filters_used=self._feature_filter_usage,
590+
uses_feature_flags=self._feature_flag_enabled,
591+
**kwargs
592+
)
541593
for (key, label), etag in feature_flag_sentinel_keys.items():
542594
changed = self._check_configuration_setting(key=key, label=label, etag=etag, headers=headers, **kwargs)
543595
if changed:
@@ -557,14 +609,10 @@ def _check_configuration_setting(
557609
"""
558610
Checks if the configuration setting have been updated since the last refresh.
559611
560-
:keyword key: key to check for chances
561-
:paramtype key: str
562-
:keyword label: label to check for changes
563-
:paramtype label: str
564-
:keyword etag: etag to check for changes
565-
:paramtype etag: str
566-
:keyword headers: headers to use for the request
567-
:paramtype headers: Mapping[str, str]
612+
:param str key: key to check for chances
613+
:param str label: label to check for changes
614+
:param str etag: etag to check for changes
615+
:param Mapping[str, str] headers: headers to use for the request
568616
:return: A tuple with the first item being true/false if a change is detected. The second item is the updated
569617
value if a change was detected.
570618
:rtype: Tuple[bool, Union[ConfigurationSetting, None]]
@@ -629,6 +677,7 @@ def _load_feature_flags(self, **kwargs):
629677
loaded_feature_flags = []
630678
# Needs to be removed unknown keyword argument for list_configuration_settings
631679
kwargs.pop("sentinel_keys", None)
680+
filters_used = {}
632681
for select in self._feature_flag_selectors:
633682
feature_flags = self._client.list_configuration_settings(
634683
key_filter=FEATURE_FLAG_PREFIX + select.key_filter, label_filter=select.label_filter, **kwargs
@@ -638,6 +687,18 @@ def _load_feature_flags(self, **kwargs):
638687

639688
if self._feature_flag_refresh_enabled:
640689
feature_flag_sentinel_keys[(feature_flag.key, feature_flag.label)] = feature_flag.etag
690+
if feature_flag.filters:
691+
for filter in feature_flag.filters:
692+
if filter.get("name") in PERCENTAGE_FILTER_NAMES:
693+
filters_used[PERCENTAGE_FILTER_KEY] = True
694+
elif filter.get("name") in TIME_WINDOW_FILTER_NAMES:
695+
filters_used[TIME_WINDOW_FILTER_KEY] = True
696+
elif filter.get("name") in TARGETING_FILTER_NAMES:
697+
filters_used[TARGETING_FILTER_KEY] = True
698+
else:
699+
filters_used[CUSTOM_FILTER_KEY] = True
700+
self._feature_filter_usage = filters_used
701+
641702
return loaded_feature_flags, feature_flag_sentinel_keys
642703

643704
def _process_key_name(self, config):

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,12 @@
1616
ContainerAppEnvironmentVariable = "CONTAINER_APP_NAME"
1717
KubernetesEnvironmentVariable = "KUBERNETES_PORT"
1818
ServiceFabricEnvironmentVariable = "Fabric_NodeName" # cspell:disable-line
19+
20+
PERCENTAGE_FILTER_NAMES = ["Percentage", "PercentageFilter", "Microsoft.Percentage", "Microsoft.PercentageFilter"]
21+
TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindow", "Microsoft.TimeWindowFilter"]
22+
TARGETING_FILTER_NAMES = ["Targeting", "TargetingFilter", "Microsoft.Targeting", "Microsoft.TargetingFilter"]
23+
24+
CUSTOM_FILTER_KEY = "CSTM" # cspell:disable-line
25+
PERCENTAGE_FILTER_KEY = "PRCNT" # cspell:disable-line
26+
TIME_WINDOW_FILTER_KEY = "TIME"
27+
TARGETING_FILTER_KEY = "TRGT" # cspell:disable-line

0 commit comments

Comments
 (0)