88import random
99import time
1010import datetime
11+ from importlib .metadata import version , PackageNotFoundError
1112from threading import Lock
1213import logging
1314from typing import (
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)
5260from ._user_agent import USER_AGENT
5361
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
261269def _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+
289328def _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 ):
0 commit comments