Skip to content

Commit 04e0fda

Browse files
mrm9084avanigupta
andauthored
Telemetry support for App Configuration (Azure#35254)
* Adding Telemetry * Telemetry Support * fixing formatting * Update _azureappconfigurationprovider.py * Update _azureappconfigurationproviderasync.py * formatting * changing doc style due to pylint-next * fixing kwargs docs * Formatting * Review comments * Changed label checking. * black format changes * pylint * Update sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py Co-authored-by: Avani Gupta <[email protected]> * added space checks * Update conftest.py * moved telemetry to client wrapper * fixing format * updating after merge * fixing black issue * removing unused imports --------- Co-authored-by: Avani Gupta <[email protected]>
1 parent f1d725c commit 04e0fda

15 files changed

+82
-25
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,21 +163,27 @@ def load_feature_flags(
163163
loaded_feature_flags = []
164164
# Needs to be removed unknown keyword argument for list_configuration_settings
165165
kwargs.pop("sentinel_keys", None)
166+
endpoint = self._client._impl._config.endpoint # pylint: disable=protected-access
166167
filters_used: Dict[str, bool] = {}
167168
for select in feature_flag_selectors:
168169
feature_flags = self._client.list_configuration_settings(
169170
key_filter=FEATURE_FLAG_PREFIX + select.key_filter, label_filter=select.label_filter, **kwargs
170171
)
171172
for feature_flag in feature_flags:
172-
loaded_feature_flags.append(json.loads(feature_flag.value))
173173
if not isinstance(feature_flag, FeatureFlagConfigurationSetting):
174174
# If the feature flag is not a FeatureFlagConfigurationSetting, it means it was selected by
175175
# mistake, so we should ignore it.
176176
continue
177177

178+
feature_flag_value = json.loads(feature_flag.value)
179+
180+
self._feature_flag_telemetry(endpoint, feature_flag, feature_flag_value)
181+
self._feature_flag_appconfig_telemetry(feature_flag, filters_used)
182+
183+
loaded_feature_flags.append(feature_flag_value)
184+
178185
if feature_flag_refresh_enabled:
179186
feature_flag_sentinel_keys[(feature_flag.key, feature_flag.label)] = feature_flag.etag
180-
self._feature_flag_telemetry(feature_flag, filters_used)
181187
return loaded_feature_flags, feature_flag_sentinel_keys, filters_used
182188

183189
@distributed_trace

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
# -------------------------------------------------------------------------
66
import time
77
import random
8+
import hashlib
9+
import base64
810
from dataclasses import dataclass
911
from typing import Dict, List
1012
from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module
@@ -18,6 +20,11 @@
1820
PERCENTAGE_FILTER_KEY,
1921
TIME_WINDOW_FILTER_KEY,
2022
TARGETING_FILTER_KEY,
23+
TELEMETRY_KEY,
24+
METADATA_KEY,
25+
ETAG_KEY,
26+
FEATURE_FLAG_REFERENCE_KEY,
27+
FEATURE_FLAG_ID_KEY,
2128
)
2229

2330
FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL = 3600 # 1 hour in seconds
@@ -28,7 +35,37 @@
2835
class _ConfigurationClientWrapperBase:
2936
endpoint: str
3037

31-
def _feature_flag_telemetry(self, feature_flag: FeatureFlagConfigurationSetting, filters_used: Dict[str, bool]):
38+
@staticmethod
39+
def _calculate_feature_id(key, label):
40+
basic_value = f"{key}\n"
41+
if label and not label.isspace():
42+
basic_value += f"{label}"
43+
feature_flag_id_hash_bytes = hashlib.sha256(basic_value.encode()).digest()
44+
encoded_flag = base64.b64encode(feature_flag_id_hash_bytes)
45+
encoded_flag = encoded_flag.replace(b"+", b"-").replace(b"/", b"_")
46+
return encoded_flag[: encoded_flag.find(b"=")]
47+
48+
def _feature_flag_telemetry(
49+
self, endpoint: str, feature_flag: FeatureFlagConfigurationSetting, feature_flag_value: Dict
50+
):
51+
if TELEMETRY_KEY in feature_flag_value:
52+
if METADATA_KEY not in feature_flag_value[TELEMETRY_KEY]:
53+
feature_flag_value[TELEMETRY_KEY][METADATA_KEY] = {}
54+
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ETAG_KEY] = feature_flag.etag
55+
56+
if not endpoint.endswith("/"):
57+
endpoint += "/"
58+
feature_flag_reference = f"{endpoint}kv/{feature_flag.key}"
59+
if feature_flag.label and not feature_flag.label.isspace():
60+
feature_flag_reference += f"?label={feature_flag.label}"
61+
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference
62+
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_ID_KEY] = self._calculate_feature_id(
63+
feature_flag.key, feature_flag.label
64+
)
65+
66+
def _feature_flag_appconfig_telemetry(
67+
self, feature_flag: FeatureFlagConfigurationSetting, filters_used: Dict[str, bool]
68+
):
3269
if feature_flag.filters:
3370
for filter in feature_flag.filters:
3471
if filter.get("name") in PERCENTAGE_FILTER_NAMES:
@@ -51,7 +88,7 @@ def __init__(
5188
replica_discovery_enabled,
5289
min_backoff_sec,
5390
max_backoff_sec,
54-
**kwargs
91+
**kwargs,
5592
):
5693
self._replica_clients: List[_ConfigurationClientWrapperBase] = []
5794
self._original_endpoint = endpoint

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
KubernetesEnvironmentVariable = "KUBERNETES_PORT"
1818
ServiceFabricEnvironmentVariable = "Fabric_NodeName" # cspell:disable-line
1919

20+
TELEMETRY_KEY = "telemetry"
21+
METADATA_KEY = "metadata"
22+
ETAG_KEY = "etag"
23+
FEATURE_FLAG_REFERENCE_KEY = "feature_flag_reference"
24+
FEATURE_FLAG_ID_KEY = "feature_flag_id"
2025
PERCENTAGE_FILTER_NAMES = ["Percentage", "PercentageFilter", "Microsoft.Percentage", "Microsoft.PercentageFilter"]
2126
TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindow", "Microsoft.TimeWindowFilter"]
2227
TARGETING_FILTER_NAMES = ["Targeting", "TargetingFilter", "Microsoft.Targeting", "Microsoft.TargetingFilter"]

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,21 +165,27 @@ async def load_feature_flags(
165165
loaded_feature_flags = []
166166
# Needs to be removed unknown keyword argument for list_configuration_settings
167167
kwargs.pop("sentinel_keys", None)
168+
endpoint = self._client._impl._config.endpoint # pylint: disable=protected-access
168169
filters_used: Dict[str, bool] = {}
169170
for select in feature_flag_selectors:
170171
feature_flags = self._client.list_configuration_settings(
171172
key_filter=FEATURE_FLAG_PREFIX + select.key_filter, label_filter=select.label_filter, **kwargs
172173
)
173174
async for feature_flag in feature_flags:
174-
loaded_feature_flags.append(json.loads(feature_flag.value))
175175
if not isinstance(feature_flag, FeatureFlagConfigurationSetting):
176176
# If the feature flag is not a FeatureFlagConfigurationSetting, it means it was selected by
177177
# mistake, so we should ignore it.
178178
continue
179179

180+
feature_flag_value = json.loads(feature_flag.value)
181+
182+
self._feature_flag_telemetry(endpoint, feature_flag, feature_flag_value)
183+
self._feature_flag_appconfig_telemetry(feature_flag, filters_used)
184+
185+
loaded_feature_flags.append(feature_flag_value)
186+
180187
if feature_flag_refresh_enabled:
181188
feature_flag_sentinel_keys[(feature_flag.key, feature_flag.label)] = feature_flag.etag
182-
self._feature_flag_telemetry(feature_flag, filters_used)
183189
return loaded_feature_flags, feature_flag_sentinel_keys, filters_used
184190

185191
@distributed_trace

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only
7171
refresh_interval: int = 30,
7272
on_refresh_success: Optional[Callable] = None,
7373
on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] = None,
74-
**kwargs
74+
feature_flag_enabled: bool = False,
75+
feature_flag_selectors: Optional[List[SettingSelector]] = None,
76+
feature_flag_refresh_enabled: bool = False,
77+
**kwargs,
7578
) -> "AzureAppConfigurationProvider":
7679
"""
7780
Loads configuration settings from Azure App Configuration into a Python application.
@@ -129,7 +132,7 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only
129132
feature_flag_enabled: bool = False,
130133
feature_flag_selectors: Optional[List[SettingSelector]] = None,
131134
feature_flag_refresh_enabled: bool = False,
132-
**kwargs
135+
**kwargs,
133136
) -> "AzureAppConfigurationProvider":
134137
"""
135138
Loads configuration settings from Azure App Configuration into a Python application.
@@ -334,7 +337,7 @@ def __init__(self, **kwargs) -> None:
334337
replica_discovery_enabled=kwargs.pop("replica_discovery_enabled", True),
335338
min_backoff_sec=min_backoff,
336339
max_backoff_sec=max_backoff,
337-
**kwargs
340+
**kwargs,
338341
)
339342
self._dict: Dict[str, Any] = {}
340343
self._secret_clients: Dict[str, SecretClient] = {}

sdk/appconfiguration/azure-appconfiguration-provider/dev_requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-e ../../core/azure-core
2-
../../appconfiguration/azure-appconfiguration
2+
-e ../azure-appconfiguration
33
-e ../../identity/azure-identity
44
-e ../../keyvault/azure-keyvault-secrets
55
aiohttp>=3.0

sdk/appconfiguration/azure-appconfiguration-provider/samples/async_aad_sample.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from azure.appconfiguration.provider.aio import load
99
from azure.appconfiguration.provider import SettingSelector
1010
import os
11-
from sample_utilities import get_authority, get_audience, get_credential, get_client_modifications
11+
from sample_utilities import get_authority, get_credential, get_client_modifications
1212

1313

1414
async def main():

sdk/appconfiguration/azure-appconfiguration-provider/samples/async_key_vault_reference_sample.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from azure.appconfiguration.provider.aio import load
99
from azure.appconfiguration.provider import SettingSelector
1010
import os
11-
from sample_utilities import get_authority, get_audience, get_credential, get_client_modifications
11+
from sample_utilities import get_authority, get_credential, get_client_modifications
1212

1313

1414
async def main():

sdk/appconfiguration/azure-appconfiguration-provider/samples/sample_utilities.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
- get_credential(): get credential of the ConfigurationClient
1414
It is not a file expected to run independently.
1515
"""
16-
17-
import os
1816
from azure.identity import AzureAuthorityHosts, DefaultAzureCredential
1917
from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential
2018

sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_feature_management.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212
from test_constants import FEATURE_MANAGEMENT_KEY
1313

1414

15-
from azure.appconfiguration.provider._azureappconfigurationprovider import _delay_failure
16-
17-
1815
class TestAppConfigurationProviderFeatureManagement(AppConfigTestCase):
1916
# method: load
2017
@app_config_decorator_async

0 commit comments

Comments
 (0)