Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

### Other Changes

* Updated Request Tracing

## 2.2.0 (2025-08-08)

### Features Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f
configuration_refresh_attempted = False
feature_flag_refresh_attempted = False
updated_watched_settings: Mapping[Tuple[str, str], Optional[str]] = {}
existing_feature_flag_usage = self._feature_filter_usage.copy()
existing_feature_flag_usage = self._tracing_context.feature_filter_usage.copy()
try:
if self._watched_settings and self._refresh_timer.needs_refresh():
configuration_refresh_attempted = True
Expand Down Expand Up @@ -317,7 +317,7 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f
logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint)
self._replica_client_manager.backoff(client)
# Restore feature flag usage on failure
self._feature_filter_usage = existing_feature_flag_usage
self._tracing_context.feature_filter_usage = existing_feature_flag_usage
raise e

def refresh(self, **kwargs) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
import base64
import hashlib
import json
import os
import time
import datetime
from importlib.metadata import version, PackageNotFoundError
from threading import Lock
import logging
from typing import (
Expand All @@ -33,20 +31,7 @@
)
from ._models import SettingSelector
from ._constants import (
REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE,
ServiceFabricEnvironmentVariable,
AzureFunctionEnvironmentVariable,
AzureWebAppEnvironmentVariable,
ContainerAppEnvironmentVariable,
KubernetesEnvironmentVariable,
NULL_CHAR,
CUSTOM_FILTER_KEY,
PERCENTAGE_FILTER_KEY,
TIME_WINDOW_FILTER_KEY,
TARGETING_FILTER_KEY,
PERCENTAGE_FILTER_NAMES,
TIME_WINDOW_FILTER_NAMES,
TARGETING_FILTER_NAMES,
TELEMETRY_KEY,
METADATA_KEY,
ETAG_KEY,
Expand All @@ -58,6 +43,8 @@
FEATURE_FLAG_KEY,
)
from ._refresh_timer import _RefreshTimer
from ._request_tracing_context import _RequestTracingContext


JSON = Mapping[str, Any]
_T = TypeVar("_T")
Expand Down Expand Up @@ -237,10 +224,7 @@ def __init__(self, **kwargs: Any) -> None:
self._watched_feature_flags: Dict[Tuple[str, str], Optional[str]] = {}
self._feature_flag_refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs)
self._feature_flag_refresh_enabled = kwargs.pop("feature_flag_refresh_enabled", False)
self._feature_filter_usage: Dict[str, bool] = {}
self._uses_load_balancing = kwargs.pop("load_balancing_enabled", False)
self._uses_ai_configuration = False
self._uses_aicc_configuration = False # AI Chat Completion
self._tracing_context = _RequestTracingContext(kwargs.pop("load_balancing_enabled", False))
self._update_lock = Lock()
self._refresh_lock = Lock()

Expand All @@ -266,39 +250,34 @@ def _update_ff_telemetry_metadata(
:param feature_flag_value: The feature flag value dictionary to update.
:type feature_flag_value: Dict[str, Any]
"""
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}"
if feature_flag_value[TELEMETRY_KEY].get("enabled"):
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference
allocation_id = self._generate_allocation_id(feature_flag_value)
if allocation_id:
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ALLOCATION_ID_KEY] = allocation_id

def _update_feature_filter_telemetry(self, feature_flag: FeatureFlagConfigurationSetting):
"""
Track feature filter usage for App Configuration telemetry.

:param feature_flag: The feature flag to analyze for filter usage.
:type feature_flag: FeatureFlagConfigurationSetting
"""
if feature_flag.filters:
for filter in feature_flag.filters:
if filter.get("name") in PERCENTAGE_FILTER_NAMES:
self._feature_filter_usage[PERCENTAGE_FILTER_KEY] = True
elif filter.get("name") in TIME_WINDOW_FILTER_NAMES:
self._feature_filter_usage[TIME_WINDOW_FILTER_KEY] = True
elif filter.get("name") in TARGETING_FILTER_NAMES:
self._feature_filter_usage[TARGETING_FILTER_KEY] = True
else:
self._feature_filter_usage[CUSTOM_FILTER_KEY] = True
if TELEMETRY_KEY not in feature_flag_value:
# Initialize telemetry dictionary if not present
feature_flag_value[TELEMETRY_KEY] = {}

# Update telemetry metadata for application insights/logging in feature management
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}"
allocation = feature_flag_value.get("allocation")
if allocation and allocation.get("seed"):
self._tracing_context.uses_seed = True
if feature_flag_value[TELEMETRY_KEY].get("enabled"):
self._tracing_context.uses_telemetry = True
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference
allocation_id = self._generate_allocation_id(feature_flag_value)
if allocation_id:
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ALLOCATION_ID_KEY] = allocation_id

variants = feature_flag_value.get("variants")
if variants:
# Update Usage Data for Telemetry
self._tracing_context.update_max_variants(len(variants))

@staticmethod
def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str]:
Expand Down Expand Up @@ -477,9 +456,9 @@ def _process_key_value_base(self, config: ConfigurationSetting) -> Union[str, Di
# Feature flags are of type json, but don't treat them as such
try:
if APP_CONFIG_AI_MIME_PROFILE in config.content_type:
self._uses_ai_configuration = True
self._tracing_context.uses_ai_configuration = True
if APP_CONFIG_AICC_MIME_PROFILE in config.content_type:
self._uses_aicc_configuration = True
self._tracing_context.uses_aicc_configuration = True
return json.loads(config.value)
except json.JSONDecodeError:
try:
Expand All @@ -500,7 +479,7 @@ def _process_feature_flags(
) -> Dict[str, Any]:
if feature_flags:
# Reset feature flag usage
self._feature_filter_usage = {}
self._tracing_context.reset_feature_filter_usage()
processed_feature_flags = [self._process_feature_flag(ff) for ff in feature_flags]
self._watched_feature_flags = self._update_watched_feature_flags(feature_flags)

Expand All @@ -512,7 +491,7 @@ def _process_feature_flags(
def _process_feature_flag(self, feature_flag: FeatureFlagConfigurationSetting) -> Dict[str, Any]:
feature_flag_value = json.loads(feature_flag.value)
self._update_ff_telemetry_metadata(self._origin_endpoint, feature_flag, feature_flag_value)
self._update_feature_filter_telemetry(feature_flag)
self._tracing_context.update_feature_filter_telemetry(feature_flag)
return feature_flag_value

def _update_watched_settings(
Expand Down Expand Up @@ -568,73 +547,14 @@ def _update_correlation_context_header(
:return: The updated headers dictionary.
:rtype: Dict[str, str]
"""
if os.environ.get(REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, default="").lower() == "true":
return headers
correlation_context = f"RequestType={request_type}"

if len(self._feature_filter_usage) > 0:
filters_used = ""
if CUSTOM_FILTER_KEY in self._feature_filter_usage:
filters_used = CUSTOM_FILTER_KEY
if PERCENTAGE_FILTER_KEY in self._feature_filter_usage:
filters_used += ("+" if filters_used else "") + PERCENTAGE_FILTER_KEY
if TIME_WINDOW_FILTER_KEY in self._feature_filter_usage:
filters_used += ("+" if filters_used else "") + TIME_WINDOW_FILTER_KEY
if TARGETING_FILTER_KEY in self._feature_filter_usage:
filters_used += ("+" if filters_used else "") + TARGETING_FILTER_KEY
correlation_context += f",Filters={filters_used}"

correlation_context += self._uses_feature_flags()

if uses_key_vault:
correlation_context += ",UsesKeyVault"
host_type = ""
if AzureFunctionEnvironmentVariable in os.environ:
host_type = "AzureFunction"
elif AzureWebAppEnvironmentVariable in os.environ:
host_type = "AzureWebApp"
elif ContainerAppEnvironmentVariable in os.environ:
host_type = "ContainerApp"
elif KubernetesEnvironmentVariable in os.environ:
host_type = "Kubernetes"
elif ServiceFabricEnvironmentVariable in os.environ:
host_type = "ServiceFabric"
if host_type:
correlation_context += f",Host={host_type}"

if replica_count > 0:
correlation_context += f",ReplicaCount={replica_count}"

if is_failover_request:
correlation_context += ",Failover"

features = ""

if self._uses_load_balancing:
features += "LB+"

if self._uses_ai_configuration:
features += "AI+"

if self._uses_aicc_configuration:
features += "AICC+"

if features:
correlation_context += f",Features={features[:-1]}"

headers["Correlation-Context"] = correlation_context
return headers

def _uses_feature_flags(self) -> str:
if not self._feature_flag_enabled:
return ""
package_name = "featuremanagement"
try:
feature_management_version = version(package_name)
return f",FMPyVer={feature_management_version}"
except PackageNotFoundError:
pass
return ""
return self._tracing_context.update_correlation_context_header(
headers=headers,
request_type=request_type,
replica_count=replica_count,
uses_key_vault=uses_key_vault,
feature_flag_enabled=self._feature_flag_enabled,
is_failover_request=is_failover_request,
)

def _deduplicate_settings(
self, configuration_settings: List[ConfigurationSetting]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@
ETAG_KEY = "ETag"
FEATURE_FLAG_REFERENCE_KEY = "FeatureFlagReference"

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"]

CUSTOM_FILTER_KEY = "CSTM" # cspell:disable-line
PERCENTAGE_FILTER_KEY = "PRCNT" # cspell:disable-line
TIME_WINDOW_FILTER_KEY = "TIME"
TARGETING_FILTER_KEY = "TRGT" # cspell:disable-line

# Mime profiles
APP_CONFIG_AI_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/"
APP_CONFIG_AICC_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/chat-completion"
Loading
Loading