Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6db8216
Adding Telemetry
mrm9084 Apr 5, 2024
acc62d9
Telemetry Support
mrm9084 Apr 15, 2024
c333311
fixing formatting
mrm9084 Apr 17, 2024
2893693
Update _azureappconfigurationprovider.py
mrm9084 Apr 24, 2024
6ade779
Update _azureappconfigurationproviderasync.py
mrm9084 Apr 24, 2024
3ec51ec
formatting
mrm9084 Apr 24, 2024
d1c5a17
changing doc style due to pylint-next
mrm9084 Apr 24, 2024
5595657
fixing kwargs docs
mrm9084 Apr 24, 2024
246d259
Formatting
mrm9084 Apr 25, 2024
07c771a
Review comments
mrm9084 Apr 29, 2024
26f5042
Changed label checking.
mrm9084 Apr 29, 2024
5d485ab
black format changes
mrm9084 Apr 29, 2024
c09247b
pylint
mrm9084 Apr 29, 2024
958761e
Update sdk/appconfiguration/azure-appconfiguration-provider/azure/app…
mrm9084 Apr 30, 2024
fac284f
added space checks
mrm9084 Apr 30, 2024
50755b0
Update conftest.py
mrm9084 May 10, 2024
ce64aca
Merge remote-tracking branch 'upstream/main' into TelemetrySupport
mrm9084 Jul 2, 2024
c727b1c
Merge branch 'main' into TelemetrySupport
mrm9084 Aug 19, 2024
33ae94e
moved telemetry to client wrapper
mrm9084 Aug 19, 2024
c8f3b40
fixing format
mrm9084 Aug 19, 2024
79c9053
Merge branch 'main' into TelemetrySupport
mrm9084 Aug 28, 2024
d9cd086
Merge remote-tracking branch 'upstream/main' into TelemetrySupport
mrm9084 Sep 6, 2024
0456c6d
updating after merge
mrm9084 Sep 6, 2024
411dd2e
Merge remote-tracking branch 'upstream/main' into TelemetrySupport
mrm9084 Sep 9, 2024
7e33ecb
fixing black issue
mrm9084 Sep 9, 2024
126937a
removing unused imports
mrm9084 Sep 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -163,21 +163,27 @@ def load_feature_flags(
loaded_feature_flags = []
# Needs to be removed unknown keyword argument for list_configuration_settings
kwargs.pop("sentinel_keys", None)
endpoint = self._client._impl._config.endpoint # pylint: disable=protected-access
filters_used: Dict[str, bool] = {}
for select in feature_flag_selectors:
feature_flags = self._client.list_configuration_settings(
key_filter=FEATURE_FLAG_PREFIX + select.key_filter, label_filter=select.label_filter, **kwargs
)
for feature_flag in feature_flags:
loaded_feature_flags.append(json.loads(feature_flag.value))
if not isinstance(feature_flag, FeatureFlagConfigurationSetting):
# If the feature flag is not a FeatureFlagConfigurationSetting, it means it was selected by
# mistake, so we should ignore it.
continue

feature_flag_value = json.loads(feature_flag.value)

self._feature_flag_telemetry(endpoint, feature_flag, feature_flag_value)
self._feature_flag_appconfig_telemetry(feature_flag, filters_used)

loaded_feature_flags.append(feature_flag_value)

if feature_flag_refresh_enabled:
feature_flag_sentinel_keys[(feature_flag.key, feature_flag.label)] = feature_flag.etag
self._feature_flag_telemetry(feature_flag, filters_used)
return loaded_feature_flags, feature_flag_sentinel_keys, filters_used

@distributed_trace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# -------------------------------------------------------------------------
import time
import random
import hashlib
import base64
from dataclasses import dataclass
from typing import Dict, List
from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module
Expand All @@ -18,6 +20,11 @@
PERCENTAGE_FILTER_KEY,
TIME_WINDOW_FILTER_KEY,
TARGETING_FILTER_KEY,
TELEMETRY_KEY,
METADATA_KEY,
ETAG_KEY,
FEATURE_FLAG_REFERENCE_KEY,
FEATURE_FLAG_ID_KEY,
)

FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL = 3600 # 1 hour in seconds
Expand All @@ -28,7 +35,37 @@
class _ConfigurationClientWrapperBase:
endpoint: str

def _feature_flag_telemetry(self, feature_flag: FeatureFlagConfigurationSetting, filters_used: Dict[str, bool]):
@staticmethod
def _calculate_feature_id(key, label):
basic_value = f"{key}\n"
if label and not label.isspace():
basic_value += f"{label}"
feature_flag_id_hash_bytes = hashlib.sha256(basic_value.encode()).digest()
encoded_flag = base64.b64encode(feature_flag_id_hash_bytes)
encoded_flag = encoded_flag.replace(b"+", b"-").replace(b"/", b"_")
return encoded_flag[: encoded_flag.find(b"=")]

def _feature_flag_telemetry(
self, endpoint: str, feature_flag: FeatureFlagConfigurationSetting, feature_flag_value: Dict
):
if TELEMETRY_KEY in feature_flag_value:
if METADATA_KEY not in feature_flag_value[TELEMETRY_KEY]:
feature_flag_value[TELEMETRY_KEY][METADATA_KEY] = {}
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ETAG_KEY] = feature_flag.etag

if not endpoint.endswith("/"):
endpoint += "/"
feature_flag_reference = f"{endpoint}kv/{feature_flag.key}"
if feature_flag.label and not feature_flag.label.isspace():
feature_flag_reference += f"?label={feature_flag.label}"
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_ID_KEY] = self._calculate_feature_id(
feature_flag.key, feature_flag.label
)

def _feature_flag_appconfig_telemetry(
self, feature_flag: FeatureFlagConfigurationSetting, filters_used: Dict[str, bool]
):
if feature_flag.filters:
for filter in feature_flag.filters:
if filter.get("name") in PERCENTAGE_FILTER_NAMES:
Expand All @@ -51,7 +88,7 @@ def __init__(
replica_discovery_enabled,
min_backoff_sec,
max_backoff_sec,
**kwargs
**kwargs,
):
self._replica_clients: List[_ConfigurationClientWrapperBase] = []
self._original_endpoint = endpoint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
KubernetesEnvironmentVariable = "KUBERNETES_PORT"
ServiceFabricEnvironmentVariable = "Fabric_NodeName" # cspell:disable-line

TELEMETRY_KEY = "telemetry"
METADATA_KEY = "metadata"
ETAG_KEY = "etag"
FEATURE_FLAG_REFERENCE_KEY = "feature_flag_reference"
FEATURE_FLAG_ID_KEY = "feature_flag_id"
PERCENTAGE_FILTER_NAMES = ["Percentage", "PercentageFilter", "Microsoft.Percentage", "Microsoft.PercentageFilter"]
TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindow", "Microsoft.TimeWindowFilter"]
TARGETING_FILTER_NAMES = ["Targeting", "TargetingFilter", "Microsoft.Targeting", "Microsoft.TargetingFilter"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,21 +165,27 @@ async def load_feature_flags(
loaded_feature_flags = []
# Needs to be removed unknown keyword argument for list_configuration_settings
kwargs.pop("sentinel_keys", None)
endpoint = self._client._impl._config.endpoint # pylint: disable=protected-access
filters_used: Dict[str, bool] = {}
for select in feature_flag_selectors:
feature_flags = self._client.list_configuration_settings(
key_filter=FEATURE_FLAG_PREFIX + select.key_filter, label_filter=select.label_filter, **kwargs
)
async for feature_flag in feature_flags:
loaded_feature_flags.append(json.loads(feature_flag.value))
if not isinstance(feature_flag, FeatureFlagConfigurationSetting):
# If the feature flag is not a FeatureFlagConfigurationSetting, it means it was selected by
# mistake, so we should ignore it.
continue

feature_flag_value = json.loads(feature_flag.value)

self._feature_flag_telemetry(endpoint, feature_flag, feature_flag_value)
self._feature_flag_appconfig_telemetry(feature_flag, filters_used)

loaded_feature_flags.append(feature_flag_value)

if feature_flag_refresh_enabled:
feature_flag_sentinel_keys[(feature_flag.key, feature_flag.label)] = feature_flag.etag
self._feature_flag_telemetry(feature_flag, filters_used)
return loaded_feature_flags, feature_flag_sentinel_keys, filters_used

@distributed_trace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only
refresh_interval: int = 30,
on_refresh_success: Optional[Callable] = None,
on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] = None,
**kwargs
feature_flag_enabled: bool = False,
feature_flag_selectors: Optional[List[SettingSelector]] = None,
feature_flag_refresh_enabled: bool = False,
**kwargs,
) -> "AzureAppConfigurationProvider":
"""
Loads configuration settings from Azure App Configuration into a Python application.
Expand Down Expand Up @@ -129,7 +132,7 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only
feature_flag_enabled: bool = False,
feature_flag_selectors: Optional[List[SettingSelector]] = None,
feature_flag_refresh_enabled: bool = False,
**kwargs
**kwargs,
) -> "AzureAppConfigurationProvider":
"""
Loads configuration settings from Azure App Configuration into a Python application.
Expand Down Expand Up @@ -334,7 +337,7 @@ def __init__(self, **kwargs) -> None:
replica_discovery_enabled=kwargs.pop("replica_discovery_enabled", True),
min_backoff_sec=min_backoff,
max_backoff_sec=max_backoff,
**kwargs
**kwargs,
)
self._dict: Dict[str, Any] = {}
self._secret_clients: Dict[str, SecretClient] = {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-e ../../core/azure-core
../../appconfiguration/azure-appconfiguration
-e ../azure-appconfiguration
-e ../../identity/azure-identity
-e ../../keyvault/azure-keyvault-secrets
aiohttp>=3.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from azure.appconfiguration.provider.aio import load
from azure.appconfiguration.provider import SettingSelector
import os
from sample_utilities import get_authority, get_audience, get_credential, get_client_modifications
from sample_utilities import get_authority, get_credential, get_client_modifications


async def main():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from azure.appconfiguration.provider.aio import load
from azure.appconfiguration.provider import SettingSelector
import os
from sample_utilities import get_authority, get_audience, get_credential, get_client_modifications
from sample_utilities import get_authority, get_credential, get_client_modifications


async def main():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
- get_credential(): get credential of the ConfigurationClient
It is not a file expected to run independently.
"""

import os
from azure.identity import AzureAuthorityHosts, DefaultAzureCredential
from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
from test_constants import FEATURE_MANAGEMENT_KEY


from azure.appconfiguration.provider._azureappconfigurationprovider import _delay_failure


class TestAppConfigurationProviderFeatureManagement(AppConfigTestCase):
# method: load
@app_config_decorator_async
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
_find_origin,
find_auto_failover_endpoints,
)
from dns.resolver import NXDOMAIN, YXDOMAIN, LifetimeTimeout, NoNameservers, Answer # cspell:disable-line
from dns.resolver import NXDOMAIN, YXDOMAIN, LifetimeTimeout, NoNameservers # cspell:disable-line

AZCONFIG_IO = ".azconfig.io" # cspell:disable-line
APPCONFIG_IO = ".appconfig.io" # cspell:disable-line
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from azure.appconfiguration.provider import load, SettingSelector
from devtools_testutils import AzureRecordedTestCase, recorded_by_proxy
from devtools_testutils import recorded_by_proxy
from preparers import app_config_decorator
from testcase import AppConfigTestCase

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
from azure.appconfiguration import AzureAppConfigurationClient
from devtools_testutils import recorded_by_proxy
from preparers import app_config_decorator
from testcase import AppConfigTestCase, setup_configs, has_feature_flag
from unittest.mock import patch
from testcase import AppConfigTestCase, setup_configs, has_feature_flag, get_feature_flag
from test_constants import FEATURE_MANAGEMENT_KEY


Expand All @@ -25,6 +24,7 @@ def test_load_only_feature_flags(self, appconfiguration_connection_string):
assert len(client.keys()) == 1
assert FEATURE_MANAGEMENT_KEY in client
assert has_feature_flag(client, "Alpha")
assert "telemetry" not in get_feature_flag(client, "Alpha")

# method: load
@recorded_by_proxy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
# --------------------------------------------------------------------------
import time
import unittest
import pytest
from unittest.mock import Mock
from azure.appconfiguration.provider import WatchKey
from devtools_testutils import recorded_by_proxy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,15 @@ def create_feature_flag_config_setting(key, label, enabled):
)


def has_feature_flag(client, feature_id, enabled=False):
def get_feature_flag(client, feature_id):
for feature_flag in client[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY]:
if feature_flag["id"] == feature_id:
return feature_flag["enabled"] == enabled
return feature_flag
return None


def has_feature_flag(client, feature_id, enabled=False):
feature_flag = get_feature_flag(client, feature_id)
if feature_flag:
return feature_flag["enabled"] == enabled
return False