Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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 @@ -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 Down Expand Up @@ -276,30 +260,18 @@ def _update_ff_telemetry_metadata(
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[TELEMETRY_KEY].get("allocation")
if allocation and allocation.get("seed"):
self._tracing_context.uses_seed = True
if feature_flag_value[TELEMETRY_KEY].get("variant"):
self._tracing_context.update_max_variants(len(feature_flag_value[TELEMETRY_KEY].get("variant")))
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

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

@staticmethod
def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str]:
"""
Expand Down Expand Up @@ -477,9 +449,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 +472,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 +484,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 +540,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