diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java index 4fcc931cb93e..cc0de8eedd97 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java @@ -2,8 +2,6 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; - import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -24,6 +22,7 @@ import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; /** * Azure App Configuration PropertySource unique per Store Label(Profile) combo. @@ -68,6 +67,8 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP @Override public void initProperties(List keyPrefixTrimValues, Context context) throws InvalidConfigurationPropertyValueException { + replicaClient.getTracingInfo().resetAiConfigurationTracing(); + List labels = Arrays.asList(labelFilters); // Reverse labels so they have the right priority order. Collections.reverse(labels); @@ -89,6 +90,7 @@ protected void processConfigurationSettings(List settings, List keyPrefixTrimValues) throws InvalidConfigurationPropertyValueException { for (ConfigurationSetting setting : settings) { + replicaClient.getTracingInfo().updateAiConfigurationTracing(setting.getContentType()); if (keyPrefixTrimValues == null && StringUtils.hasText(keyFilter)) { keyPrefixTrimValues = new ArrayList<>(); keyPrefixTrimValues.add(keyFilter.substring(0, keyFilter.length() - 1)); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java index b480b72eb922..cd6e0ddd5793 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java @@ -68,4 +68,34 @@ public class AppConfigurationConstants { public static final String FEATURE_FLAG_REFERENCE = "FeatureFlagReference"; public static final String E_TAG = "ETag"; + + /** + * AI mime profile identifier in content type. + */ + public static final String APP_CONFIG_AI_MIME_PROFILE = "azconfig.io/mime-profiles/ai"; + + /** + * AI Chat Completion mime profile identifier in content type. + */ + public static final String APP_CONFIG_AICC_MIME_PROFILE = "azconfig.io/mime-profiles/ai-chat-completion"; + + /** + * Constant for tracing snapshot reference usage. + */ + public static final String SNAPSHOT_REFERENCE_TAG = "SnapshotRef"; + + /** + * Constant for tracing load balancing usage. + */ + public static final String LOAD_BALANCING_FEATURE = "LB"; + + /** + * Constant for tracing AI configuration usage. + */ + public static final String AI_CONFIGURATION_FEATURE = "AI"; + + /** + * Constant for tracing AI Chat Completion configuration usage. + */ + public static final String AI_CHAT_COMPLETION_FEATURE = "AICC"; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java index edb761e0e737..f62384ae12ba 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java @@ -22,6 +22,7 @@ import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.data.appconfiguration.models.SnapshotComposition; import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings; +import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; import io.netty.handler.codec.http.HttpResponseStatus; @@ -43,6 +44,8 @@ class AppConfigurationReplicaClient { private final ConfigurationClient client; + private final TracingInfo tracingInfo; + private Instant backoffEndTime; private int failedAttempts; @@ -52,11 +55,14 @@ class AppConfigurationReplicaClient { * @param endpoint client endpoint * @param originClient origin client identifier * @param client Configuration Client to App Configuration store + * @param tracingInfo tracing info for this client */ - AppConfigurationReplicaClient(String endpoint, String originClient, ConfigurationClient client) { + AppConfigurationReplicaClient(String endpoint, String originClient, ConfigurationClient client, + TracingInfo tracingInfo) { this.endpoint = endpoint; this.originClient = originClient; this.client = client; + this.tracingInfo = tracingInfo; this.backoffEndTime = Instant.now().minusMillis(INITIAL_BACKOFF_OFFSET_MS); this.failedAttempts = 0; } @@ -98,6 +104,13 @@ String getOriginClient() { return originClient; } + /** + * @return tracingInfo for this client + */ + TracingInfo getTracingInfo() { + return tracingInfo; + } + /** * Gets the Configuration Setting for the given config store that match the Setting Selector criteria. Follows * retry-after-ms header. diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java index 8832196d5dc2..bd3eab9cb345 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java @@ -176,6 +176,9 @@ List buildClients(ConfigStore configStore) { if (configStore.isLoadBalancingEnabled()) { Collections.shuffle(clients); + for (AppConfigurationReplicaClient client : clients) { + client.getTracingInfo().setUsesLoadBalancing(true); + } } return clients; @@ -213,7 +216,7 @@ private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBu if (clientCustomizer != null) { clientCustomizer.customize(builder, endpoint); } - return new AppConfigurationReplicaClient(endpoint, originEndpoint, builder.buildClient()); + return new AppConfigurationReplicaClient(endpoint, originEndpoint, builder.buildClient(), tracingInfo); } private ConfigurationClientBuilder createBuilderInstance() { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java index 47c39c3fd920..7edb06d2e128 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java @@ -47,6 +47,7 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli * @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type */ public void initProperties(List trim, Context context) throws InvalidConfigurationPropertyValueException { + replicaClient.getTracingInfo().resetAiConfigurationTracing(); processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName, context), null, trim); WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, featureFlagsList); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java index eb1401d404c9..355b1da73f5e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java @@ -325,6 +325,10 @@ private AppConfigurationReplicaClient handleReplicaFailure(AppConfigurationRepli AppConfigurationReplicaClient nextClient = replicaClientFactory.getNextActiveClient(resource.getEndpoint(), false); + if (nextClient != null) { + nextClient.getTracingInfo().setFailoverRequest(true); + } + String scenario = resource.isRefresh() ? "refresh" : "startup"; String nextAction = nextClient != null ? "Trying next replica." : "No more replicas available."; logger.warn("Azure App Configuration replica " + client.getEndpoint() diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java index 18049c5200af..0c6e4bbd9e60 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java @@ -203,6 +203,34 @@ private void updateTelemetry(FeatureFlagConfigurationSetting featureFlag) { for (FeatureFlagFilter filter : featureFlag.getClientFilters()) { tracing.updateFeatureFilterTelemetry(filter.getName()); } + + // Track telemetry and seed usage from the feature flag value + try { + JsonNode node = CASE_INSENSITIVE_MAPPER.readTree(featureFlag.getValue()); + + // Check for telemetry enabled + JsonNode telemetryNode = node.get(TELEMETRY); + if (telemetryNode != null && !telemetryNode.isEmpty()) { + JsonNode enabledNode = telemetryNode.get("enabled"); + if (enabledNode != null && enabledNode.asBoolean()) { + tracing.setUsesTelemetry(true); + } + } + + // Check for allocation seed + JsonNode allocationNode = node.get("allocation"); + if (allocationNode != null && allocationNode.has("seed")) { + tracing.setUsesSeed(true); + } + + // Track max variants + JsonNode variantsNode = node.get("variants"); + if (variantsNode != null && variantsNode.isArray()) { + tracing.updateMaxVariants(variantsNode.size()); + } + } catch (JsonProcessingException e) { + LOGGER.warn("Error parsing feature flag telemetry for key: {}", featureFlag.getKey(), e); + } } /** diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java index a7d1a00a0748..8ccb807f8188 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java @@ -16,11 +16,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; -final class JsonConfigurationParser { +public final class JsonConfigurationParser { private static final ObjectMapper MAPPER = JsonMapper.builder().enable(JsonReadFeature.ALLOW_JAVA_COMMENTS).build(); - static boolean isJsonContentType(String contentType) { + public static boolean isJsonContentType(String contentType) { if (!StringUtils.hasText(contentType)) { return false; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/RequestTracingConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/RequestTracingConstants.java index e5884a0f4753..c91167acff60 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/RequestTracingConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/RequestTracingConstants.java @@ -55,7 +55,37 @@ public enum RequestTracingConstants { /** * Constant for number of replicas, not including origin configured. */ - REPLICA_COUNT("ReplicaCount"); + REPLICA_COUNT("ReplicaCount"), + + /** + * Constant for tracing the max variants count. + */ + MAX_VARIANTS_KEY("MaxVariants"), + + /** + * Constant for tracing feature flag features. + */ + FF_FEATURES_KEY("FFFeatures"), + + /** + * Constant for tracing general features. + */ + FEATURES_KEY("Features"), + + /** + * Constant for tracing the feature management version. + */ + FM_SPRING_VER_KEY("FMSpVer"), + + /** + * Constant for tracing if this is a failover request. + */ + FAILOVER_TAG("Failover"), + + /** + * Constant for tracing the filter key. + */ + FILTER_KEY("Filter"); private final String text; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java index fd35c83c1387..80eedf19384d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java @@ -17,6 +17,10 @@ public class FeatureFlagTracing { private static final String FILTER_TYPE_DELIMITER = "+"; + private static final String FEATURE_FLAG_USES_TELEMETRY_TAG = "Telemetry"; + + private static final String FEATURE_FLAG_USES_SEED_TAG = "Seed"; + private static final List PERCENTAGE_FILTER_NAMES = Arrays.asList("Percentage", "Microsoft.Percentage", "PercentageFilter", "Microsoft.PercentageFilter"); @@ -34,6 +38,12 @@ public class FeatureFlagTracing { private Boolean usesTargetingFilter = false; + private Boolean usesTelemetry = false; + + private Boolean usesSeed = false; + + private Integer maxVariants = null; + boolean usesAnyFilter() { return usesCustomFilter || usesPercentageFilter || usesTimeWindowFilter || usesTargetingFilter; } @@ -43,6 +53,9 @@ public void resetFeatureFilterTelemetry() { usesPercentageFilter = false; usesTimeWindowFilter = false; usesTargetingFilter = false; + usesTelemetry = false; + usesSeed = false; + maxVariants = null; } public void updateFeatureFilterTelemetry(String filterName) { @@ -57,6 +70,58 @@ public void updateFeatureFilterTelemetry(String filterName) { } } + /** + * Sets whether telemetry is used. + * @param usesTelemetry whether telemetry is used + */ + public void setUsesTelemetry(Boolean usesTelemetry) { + this.usesTelemetry = usesTelemetry; + } + + /** + * Sets whether seed is used. + * @param usesSeed whether seed is used + */ + public void setUsesSeed(Boolean usesSeed) { + this.usesSeed = usesSeed; + } + + /** + * Updates max variants if the new size is larger. + * @param size size of variants + */ + public void updateMaxVariants(int size) { + if (maxVariants == null || size > maxVariants) { + maxVariants = size; + } + } + + /** + * Gets the max variants value. + * @return max variants or null if not set + */ + public Integer getMaxVariants() { + return maxVariants; + } + + /** + * Creates the FFFeatures string for correlation context. + * @return FFFeatures string with telemetry and seed tags + */ + public String createFFFeaturesString() { + StringBuilder sb = new StringBuilder(); + if (Boolean.TRUE.equals(usesSeed)) { + sb.append(FEATURE_FLAG_USES_SEED_TAG); + } + if (Boolean.TRUE.equals(usesTelemetry)) { + if (sb.length() > 0) { + sb.append(FILTER_TYPE_DELIMITER); + } + sb.append(FEATURE_FLAG_USES_TELEMETRY_TAG); + } + return sb.toString(); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java index b1b7f4479e7b..0be74e256373 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java @@ -2,18 +2,26 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation.http.policy; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; - import org.springframework.util.StringUtils; import com.azure.core.util.Configuration; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.AI_CHAT_COMPLETION_FEATURE; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.AI_CONFIGURATION_FEATURE; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.APP_CONFIG_AICC_MIME_PROFILE; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.APP_CONFIG_AI_MIME_PROFILE; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.LOAD_BALANCING_FEATURE; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.SNAPSHOT_REFERENCE_TAG; import com.azure.spring.cloud.appconfiguration.config.implementation.HostType; +import com.azure.spring.cloud.appconfiguration.config.implementation.JsonConfigurationParser; import com.azure.spring.cloud.appconfiguration.config.implementation.RequestTracingConstants; import com.azure.spring.cloud.appconfiguration.config.implementation.RequestType; public class TracingInfo { + private static final String DELIMITER = "+"; + private boolean isKeyVaultConfigured = false; private int replicaCount; @@ -22,6 +30,16 @@ public class TracingInfo { private final Configuration configuration; + private boolean usesLoadBalancing = false; + + private boolean usesAiConfiguration = false; + + private boolean usesAiccConfiguration = false; + + private boolean usesSnapshotReference = false; + + private boolean isFailoverRequest = false; + public TracingInfo(boolean isKeyVaultConfigured, int replicaCount, Configuration configuration) { this.isKeyVaultConfigured = isKeyVaultConfigured; this.replicaCount = replicaCount; @@ -29,6 +47,85 @@ public TracingInfo(boolean isKeyVaultConfigured, int replicaCount, Configuration this.configuration = configuration; } + /** + * Sets whether load balancing is enabled. + * @param usesLoadBalancing whether load balancing is enabled + */ + public void setUsesLoadBalancing(boolean usesLoadBalancing) { + this.usesLoadBalancing = usesLoadBalancing; + } + + /** + * Sets whether snapshot references are used. + * @param usesSnapshotReference whether snapshot references are used + */ + public void setUsesSnapshotReference(boolean usesSnapshotReference) { + this.usesSnapshotReference = usesSnapshotReference; + } + + /** + * Sets whether this is a failover request. + * @param isFailoverRequest whether this is a failover request + */ + public void setFailoverRequest(boolean isFailoverRequest) { + this.isFailoverRequest = isFailoverRequest; + } + + /** + * Resets AI configuration tracing flags. + */ + public void resetAiConfigurationTracing() { + this.usesAiConfiguration = false; + this.usesAiccConfiguration = false; + } + + /** + * Updates AI configuration tracing based on content type. + * @param contentType the content type to analyze + */ + public void updateAiConfigurationTracing(String contentType) { + if (contentType == null || usesAiccConfiguration) { + return; + } + + if (!JsonConfigurationParser.isJsonContentType(contentType)) { + return; + } + + String profileValue = extractProfileParameter(contentType); + if (profileValue == null) { + return; + } + + if (profileValue.contains(APP_CONFIG_AI_MIME_PROFILE)) { + usesAiConfiguration = true; + if (profileValue.contains(APP_CONFIG_AICC_MIME_PROFILE)) { + usesAiccConfiguration = true; + } + } + } + + /** + * Extracts the value of the "profile" parameter from a content type string. + * @param contentType the full content type string (e.g. "application/json; profile=\"https://...\"") + * @return the profile parameter value, or null if not present + */ + private static String extractProfileParameter(String contentType) { + String[] parts = contentType.split(";"); + for (int i = 1; i < parts.length; i++) { + String param = parts[i].strip(); + if (param.toLowerCase().startsWith("profile=")) { + String value = param.substring("profile=".length()).strip(); + // Remove surrounding quotes if present + if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + value = value.substring(1, value.length() - 1); + } + return value; + } + } + return null; + } + String getValue(boolean watchRequests, boolean pushRefresh, FeatureFlagTracing featureFlagTracing) { if (featureFlagTracing != null) { this.featureFlagTracing = featureFlagTracing; @@ -41,33 +138,89 @@ String getValue(boolean watchRequests, boolean pushRefresh, FeatureFlagTracing f RequestType requestTypeValue = watchRequests ? RequestType.WATCH : RequestType.STARTUP; StringBuilder sb = new StringBuilder(); - sb.append(RequestTracingConstants.REQUEST_TYPE_KEY).append("=" + requestTypeValue); + // Key-value pairs + sb.append(RequestTracingConstants.REQUEST_TYPE_KEY).append("=").append(requestTypeValue); - if (this.featureFlagTracing.usesAnyFilter()) { - sb.append(",Filter=").append(this.featureFlagTracing.toString()); + if (replicaCount > 0) { + sb.append(",").append(RequestTracingConstants.REPLICA_COUNT).append("=").append(replicaCount); } String hostType = getHostType(); if (!hostType.isEmpty()) { sb.append(",").append(RequestTracingConstants.HOST_TYPE_KEY).append("=").append(hostType); } + + if (this.featureFlagTracing.usesAnyFilter()) { + sb.append(",").append(RequestTracingConstants.FILTER_KEY).append("=").append(this.featureFlagTracing.toString()); + } + + // Max variants + if (this.featureFlagTracing.getMaxVariants() != null && this.featureFlagTracing.getMaxVariants() > 0) { + sb.append(",").append(RequestTracingConstants.MAX_VARIANTS_KEY).append("=").append(this.featureFlagTracing.getMaxVariants()); + } + + // FFFeatures + String ffFeatures = this.featureFlagTracing.createFFFeaturesString(); + if (StringUtils.hasText(ffFeatures)) { + sb.append(",").append(RequestTracingConstants.FF_FEATURES_KEY).append("=").append(ffFeatures); + } + + // Features + String features = createFeaturesString(); + if (StringUtils.hasText(features)) { + sb.append(",").append(RequestTracingConstants.FEATURES_KEY).append("=").append(features); + } + + // Feature management version + sb = getFeatureManagementUsage(sb); + + // Tags if (isKeyVaultConfigured) { sb.append(",").append(KEY_VAULT_CONFIGURED_TRACING); } - + if (pushRefresh) { sb.append(",").append(PUSH_REFRESH); } - if (replicaCount > 0) { - sb.append(",").append(RequestTracingConstants.REPLICA_COUNT).append("=").append(replicaCount); + if (isFailoverRequest) { + sb.append(",").append(RequestTracingConstants.FAILOVER_TAG); + isFailoverRequest = false; } - - sb = getFeatureManagementUsage(sb); return sb.toString(); } + /** + * Creates the Features string for correlation context. + * @return Features string with load balancing, AI, and snapshot reference tags + */ + private String createFeaturesString() { + StringBuilder sb = new StringBuilder(); + if (usesLoadBalancing) { + sb.append(LOAD_BALANCING_FEATURE); + } + if (usesAiConfiguration) { + if (sb.length() > 0) { + sb.append(DELIMITER); + } + sb.append(AI_CONFIGURATION_FEATURE); + } + if (usesAiccConfiguration) { + if (sb.length() > 0) { + sb.append(DELIMITER); + } + sb.append(AI_CHAT_COMPLETION_FEATURE); + } + if (usesSnapshotReference) { + if (sb.length() > 0) { + sb.append(DELIMITER); + } + sb.append(SNAPSHOT_REFERENCE_TAG); + } + return sb.toString(); + } + /** * Gets the current host machines type; Azure Function, Azure Web App, Kubernetes, or Empty. * @@ -90,12 +243,12 @@ private static String getHostType() { return hostType.toString(); } - + private static StringBuilder getFeatureManagementUsage(StringBuilder sb) { ClassLoader loader = ClassLoader.getSystemClassLoader(); Package ff = loader.getDefinedPackage("com.azure.spring.cloud.feature.management.models"); if (ff != null && StringUtils.hasText(ff.getImplementationVersion())) { - sb.append(",FMSpVer=").append(ff.getImplementationVersion()); + sb.append(",").append(RequestTracingConstants.FM_SPRING_VER_KEY).append("=").append(ff.getImplementationVersion()); } return sb; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java index 1bad01307099..f79e90727f24 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java @@ -39,9 +39,11 @@ import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +import com.azure.core.util.Configuration; import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; +import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; @@ -111,6 +113,9 @@ public void init() { MockitoAnnotations.openMocks(this); + when(clientMock.getTracingInfo()) + .thenReturn(new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); + testItems = new ArrayList<>(); testItems.add(ITEM_1); testItems.add(ITEM_2); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java index be1bd3534bab..cc1ed8b1620e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java @@ -40,10 +40,12 @@ import org.mockito.quality.Strictness; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import com.azure.core.util.Configuration; import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; +import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; @@ -108,6 +110,9 @@ public void init() { MockitoAnnotations.openMocks(this); + when(clientMock.getTracingInfo()) + .thenReturn(new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); + testItems = new ArrayList<>(); testItems.add(ITEM_1); testItems.add(ITEM_2); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java index e3b5ff4f8bd6..6eaf41d6276d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java @@ -30,6 +30,7 @@ import org.mockito.quality.Strictness; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import com.azure.core.util.Configuration; import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting; @@ -37,6 +38,7 @@ import com.azure.security.keyvault.secrets.SecretClientBuilder; import com.azure.security.keyvault.secrets.models.KeyVaultSecret; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; import com.azure.spring.cloud.appconfiguration.config.implementation.stores.AppConfigurationSecretClientManager; public class AppConfigurationPropertySourceKeyVaultTest { @@ -86,6 +88,9 @@ public void init() { MockitoAnnotations.openMocks(this); + when(replicaClientMock.getTracingInfo()) + .thenReturn(new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); + String[] labelFilter = { "\0" }; propertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, replicaClientMock, keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java index 3e18eb2263ec..ddb944574bfa 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java @@ -37,6 +37,7 @@ import com.azure.core.http.rest.PagedResponse; import com.azure.core.http.rest.PagedResponseBase; import com.azure.core.http.rest.Response; +import com.azure.core.util.Configuration; import com.azure.core.util.Context; import com.azure.data.appconfiguration.ConfigurationClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; @@ -46,6 +47,7 @@ import com.azure.data.appconfiguration.models.SnapshotComposition; import com.azure.identity.CredentialUnavailableException; import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings; +import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; import reactor.core.publisher.Mono; @@ -93,7 +95,7 @@ public void cleanup() throws Exception { @Test public void getWatchKeyTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); ConfigurationSetting watchKey = new ConfigurationSetting().setKey("watch").setLabel("\0"); @@ -124,7 +126,7 @@ public void getWatchKeyTest() { @Test public void listSettingsTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); ConfigurationSetting configurationSetting = new ConfigurationSetting().setKey("test-key"); List configurations = List.of(configurationSetting); @@ -159,7 +161,7 @@ public void listSettingsTest() { @Test public void listFeatureFlagsTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false); List configurations = List.of(featureFlag); @@ -197,7 +199,7 @@ public void listFeatureFlagsTest() { @Test public void listSettingsUnknownHostTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenThrow(new UncheckedIOException(new UnknownHostException())); @@ -207,7 +209,7 @@ public void listSettingsUnknownHostTest() { @Test public void listSettingsNoCredentialTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenThrow(new CredentialUnavailableException("No Credential")); @@ -218,7 +220,7 @@ public void listSettingsNoCredentialTest() { @Test public void getWatchNoCredentialTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any())) @@ -229,7 +231,7 @@ public void getWatchNoCredentialTest() { @Test public void backoffTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); // Setups in the past and with no errors. assertTrue(client.getBackoffEndTime().isBefore(Instant.now())); @@ -257,7 +259,7 @@ public void backoffTest() { @Test public void listSettingSnapshotTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); List configurations = new ArrayList<>(); ConfigurationSnapshot snapshot = new ConfigurationSnapshot(null); @@ -295,7 +297,7 @@ public void listSettingSnapshotTest() { @Test public void listSettingSnapshotInvalidCompositionTypeTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); ConfigurationSnapshot snapshot = new ConfigurationSnapshot(null); snapshot.setSnapshotComposition(SnapshotComposition.KEY_LABEL); @@ -311,7 +313,7 @@ public void listSettingSnapshotInvalidCompositionTypeTest() { @Test public void updateSyncTokenTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); String fakeToken = "fake_sync_token"; client.updateSyncToken(fakeToken); @@ -324,7 +326,7 @@ public void updateSyncTokenTest() { @Test public void checkWatchKeysTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false); List configurations = List.of(featureFlag); @@ -364,7 +366,7 @@ public void checkWatchKeysTest() { @Test public void watchedConfigurationSettingsTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); ConfigurationSetting setting1 = new ConfigurationSetting().setKey("key1").setLabel("label1"); ConfigurationSetting setting2 = new ConfigurationSetting().setKey("key2").setLabel("label2"); @@ -392,7 +394,7 @@ public void watchedConfigurationSettingsTest() { @Test public void watchedConfigurationSettingsErrorTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); @@ -416,7 +418,7 @@ public void watchedConfigurationSettingsErrorTest() { @Test public void watchedConfigurationSettingsUncheckedIOExceptionTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock, new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenThrow(new UncheckedIOException(new IOException("Network error"))); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java index 545921567228..5fa65a3af68f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java @@ -33,6 +33,8 @@ import org.springframework.boot.logging.DeferredLog; import org.springframework.boot.logging.DeferredLogFactory; +import com.azure.core.util.Configuration; +import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreTrigger; @@ -108,6 +110,8 @@ public void setup() { .thenReturn(keyVaultClientFactoryMock); lenient().when(bootstrapContextMock.get(StateHolder.class)).thenReturn(stateHolderMock); lenient().when(logFactoryMock.getLog(any(Class.class))).thenReturn(new DeferredLog()); + lenient().when(clientMock.getTracingInfo()) + .thenReturn(new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); } @AfterEach diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracingTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracingTest.java index 04d6e5a4c92c..33004603f093 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracingTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracingTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -168,4 +169,58 @@ public void usesMultipleFilters() { assertEquals("", tracing.toString()); } + @Test + public void usesTelemetryFlag() { + FeatureFlagTracing tracing = new FeatureFlagTracing(); + assertEquals("", tracing.createFFFeaturesString()); + + tracing.setUsesTelemetry(true); + assertEquals("Telemetry", tracing.createFFFeaturesString()); + + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.createFFFeaturesString()); + } + + @Test + public void usesSeedFlag() { + FeatureFlagTracing tracing = new FeatureFlagTracing(); + assertEquals("", tracing.createFFFeaturesString()); + + tracing.setUsesSeed(true); + assertEquals("Seed", tracing.createFFFeaturesString()); + + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.createFFFeaturesString()); + } + + @Test + public void usesSeedAndTelemetry() { + FeatureFlagTracing tracing = new FeatureFlagTracing(); + tracing.setUsesSeed(true); + tracing.setUsesTelemetry(true); + assertEquals("Seed+Telemetry", tracing.createFFFeaturesString()); + + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.createFFFeaturesString()); + } + + @Test + public void maxVariantsTracking() { + FeatureFlagTracing tracing = new FeatureFlagTracing(); + assertNull(tracing.getMaxVariants()); + + tracing.updateMaxVariants(3); + assertEquals(3, tracing.getMaxVariants()); + + // Should only update if larger + tracing.updateMaxVariants(2); + assertEquals(3, tracing.getMaxVariants()); + + tracing.updateMaxVariants(5); + assertEquals(5, tracing.getMaxVariants()); + + tracing.resetFeatureFilterTelemetry(); + assertNull(tracing.getMaxVariants()); + } + } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java index 4ad16ca71d67..b05f5b10a85f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collections; import java.util.HashMap; @@ -62,6 +63,157 @@ public void disableTracingTest() { assertNotEquals("", tracingInfo.getValue(false, false, null)); } + @Test + public void snapshotReferenceTracingTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.setUsesSnapshotReference(true); + String value = tracingInfo.getValue(false, false, null); + assertTrue(value.contains("Features=SnapshotRef")); + } + + @Test + public void loadBalancingTracingTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.setUsesLoadBalancing(true); + String value = tracingInfo.getValue(false, false, null); + assertTrue(value.contains("Features=LB")); + } + + @Test + public void aiConfigurationTracingTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.updateAiConfigurationTracing("application/json; profile=\"https://azconfig.io/mime-profiles/ai\""); + String value = tracingInfo.getValue(false, false, null); + assertTrue(value.contains("Features=AI")); + } + + @Test + public void aiChatCompletionTracingTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.updateAiConfigurationTracing( + "application/json; profile=\"https://azconfig.io/mime-profiles/ai-chat-completion\""); + String value = tracingInfo.getValue(false, false, null); + assertTrue(value.contains("AI+AICC")); + } + + @Test + public void aiConfigurationTracingNullContentTypeTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.updateAiConfigurationTracing(null); + String value = tracingInfo.getValue(false, false, null); + assertEquals("RequestType=Startup", value); + } + + @Test + public void aiConfigurationTracingResetTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.updateAiConfigurationTracing("application/json; profile=\"https://azconfig.io/mime-profiles/ai\""); + tracingInfo.resetAiConfigurationTracing(); + String value = tracingInfo.getValue(false, false, null); + assertEquals("RequestType=Startup", value); + } + + @Test + public void aiConfigurationTracingNonJsonContentTypeTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.updateAiConfigurationTracing("text/plain; profile=\"https://azconfig.io/mime-profiles/ai\""); + String value = tracingInfo.getValue(false, false, null); + assertEquals("RequestType=Startup", value); + } + + @Test + public void aiConfigurationTracingNoProfileParameterTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.updateAiConfigurationTracing("application/json; charset=utf-8"); + String value = tracingInfo.getValue(false, false, null); + assertEquals("RequestType=Startup", value); + } + + @Test + public void aiConfigurationTracingFeatureFlagContentTypeTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.updateAiConfigurationTracing( + "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"); + String value = tracingInfo.getValue(false, false, null); + assertEquals("RequestType=Startup", value); + } + + @Test + public void multipleFeaturesTracingTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.setUsesLoadBalancing(true); + tracingInfo.setUsesSnapshotReference(true); + String value = tracingInfo.getValue(false, false, null); + assertTrue(value.contains("Features=LB+SnapshotRef")); + } + + @Test + public void failoverTracingTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.setFailoverRequest(true); + String value = tracingInfo.getValue(false, false, null); + assertTrue(value.contains("Failover")); + } + + @Test + public void maxVariantsTracingTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + FeatureFlagTracing ffTracing = new FeatureFlagTracing(); + ffTracing.updateMaxVariants(5); + String value = tracingInfo.getValue(false, false, ffTracing); + assertTrue(value.contains("MaxVariants=5")); + } + + @Test + public void ffFeaturesTracingTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + FeatureFlagTracing ffTracing = new FeatureFlagTracing(); + ffTracing.setUsesTelemetry(true); + ffTracing.setUsesSeed(true); + String value = tracingInfo.getValue(false, false, ffTracing); + assertTrue(value.contains("FFFeatures=Seed+Telemetry")); + } + + @Test + public void fullCorrelationContextTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(true, 2, configuration); + tracingInfo.setUsesLoadBalancing(true); + tracingInfo.setUsesSnapshotReference(true); + tracingInfo.setFailoverRequest(true); + + FeatureFlagTracing ffTracing = new FeatureFlagTracing(); + ffTracing.updateFeatureFilterTelemetry("Targeting"); + ffTracing.setUsesTelemetry(true); + ffTracing.updateMaxVariants(3); + + String value = tracingInfo.getValue(false, true, ffTracing); + + // Verify ordering: key-value pairs first, then tags + assertTrue(value.startsWith("RequestType=Startup")); + assertTrue(value.contains("ReplicaCount=2")); + assertTrue(value.contains("Filter=TRGT")); + assertTrue(value.contains("MaxVariants=3")); + assertTrue(value.contains("FFFeatures=Telemetry")); + assertTrue(value.contains("Features=LB+SnapshotRef")); + assertTrue(value.contains("UsesKeyVault")); + assertTrue(value.contains("PushRefresh")); + assertTrue(value.contains("Failover")); + } + private static final ConfigurationSource EMPTY_SOURCE = new ConfigurationSource() { @Override public Map getProperties(String source) {