diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationConstants.java index 4f3b619d34e1..c9104db4b9d2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationConstants.java @@ -27,20 +27,15 @@ public class AppConfigurationConstants { */ public static final String FEATURE_FLAG_PREFIX = ".appconfig.featureflag/"; - /** - * Feature Store Prefix - */ - public static final String FEATURE_STORE_SUFFIX = ".appconfig"; - /** * Separator for multiple labels. */ public static final String LABEL_SEPARATOR = ","; /** - * Key for returning all feature flags + * The key filter for selecting all feature flags. */ - public static final String FEATURE_STORE_WATCH_KEY = FEATURE_STORE_SUFFIX + "*"; + public static final String SELECT_ALL_FEATURE_FLAGS = FEATURE_FLAG_PREFIX + "*"; /** * Constant for tracing if the library is being used with a dev profile. diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySource.java index 96439fac5e5b..12c74027e2ff 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySource.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySource.java @@ -8,6 +8,7 @@ import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_MANAGEMENT_KEY; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.SELECT_ALL_FEATURE_FLAGS; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.GROUPS; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.GROUPS_CAPS; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.TARGETING_FILTER; @@ -30,6 +31,7 @@ import com.azure.data.appconfiguration.models.FeatureFlagFilter; import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.spring.cloud.config.implementation.feature.management.entity.Feature; +import com.azure.spring.cloud.config.implementation.http.policy.TracingInfo; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -47,13 +49,6 @@ final class AppConfigurationFeatureManagementPropertySource extends AppConfigura private static final ObjectMapper CASE_INSENSITIVE_MAPPER = JsonMapper.builder() .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); - /** - * App Configuration Feature Filter prefix. - */ - private static final String KEY_FILTER_PREFIX = ".appconfig.featureflag/"; - - private static final String KEY_FILTER_DEFAULT = KEY_FILTER_PREFIX + "*"; - private final List featureConfigurationSettings; AppConfigurationFeatureManagementPropertySource(String originEndpoint, AppConfigurationReplicaClient replicaClient, @@ -84,10 +79,10 @@ private static List convertToListOrEmptyList(Map paramet public void initProperties() { SettingSelector settingSelector = new SettingSelector(); - String keyFilter = KEY_FILTER_DEFAULT; + String keyFilter = SELECT_ALL_FEATURE_FLAGS; if (StringUtils.hasText(this.keyFilter)) { - keyFilter = KEY_FILTER_PREFIX + this.keyFilter; + keyFilter = FEATURE_FLAG_PREFIX + this.keyFilter; } settingSelector.setKeyFilter(keyFilter); @@ -99,18 +94,21 @@ public void initProperties() { settingSelector.setLabelFilter(label); List features = replicaClient.listSettings(settingSelector); + TracingInfo tracing = replicaClient.getTracingInfo(); // Reading In Features for (ConfigurationSetting setting : features) { if (setting instanceof FeatureFlagConfigurationSetting && FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) { featureConfigurationSettings.add(setting); - Object feature = createFeature((FeatureFlagConfigurationSetting) setting); + FeatureFlagConfigurationSetting featureFlag = (FeatureFlagConfigurationSetting) setting; - String configName = FEATURE_MANAGEMENT_KEY + String configName = FEATURE_MANAGEMENT_KEY + setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()); - properties.put(configName, feature); + updateTelemetry(featureFlag, tracing); + + properties.put(configName, createFeature(featureFlag)); } } } @@ -172,6 +170,18 @@ private Object createFeature(FeatureFlagConfigurationSetting item) { return feature; } + + /** + * Looks at each filter used in a Feature Flag to check what types it is using. + * + * @param featureFlag FeatureFlagConfigurationSetting + * @param tracing The TracingInfo for this store. + */ + private void updateTelemetry(FeatureFlagConfigurationSetting featureFlag, TracingInfo tracing) { + for (FeatureFlagFilter filter : featureFlag.getClientFilters()) { + tracing.getFeatureFlagTracing().updateFeatureFilterTelemetry(filter.getName()); + } + } private String getFeatureSimpleName(ConfigurationSetting setting) { return setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java index 17b5297183a3..a0b369741b9d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java @@ -61,7 +61,7 @@ public final class AppConfigurationPropertySourceLocator implements PropertySour */ public AppConfigurationPropertySourceLocator(AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, - Duration refreshInterval, List configStores) { + Duration refreshInterval, List configStores) { this.refreshInterval = refreshInterval; this.appProperties = appProperties; this.configStores = configStores; @@ -210,12 +210,10 @@ private List getWatchKeys(AppConfigurationReplicaClient cl private List getFeatureFlagWatchKeys(ConfigStore configStore, List sources) { List watchKeysFeatures = new ArrayList<>(); - if (configStore.getFeatureFlags().getEnabled()) { for (AppConfigurationPropertySource propertySource : sources) { if (propertySource instanceof AppConfigurationFeatureManagementPropertySource) { - watchKeysFeatures = ((AppConfigurationFeatureManagementPropertySource) propertySource) - .getFeatureFlagSettings(); + watchKeysFeatures.addAll(((AppConfigurationFeatureManagementPropertySource) propertySource).getFeatureFlagSettings()); } } } @@ -261,14 +259,6 @@ private List create(AppConfigurationReplicaClien List sourceList = new ArrayList<>(); List selects = store.getSelects(); - for (AppConfigurationKeyValueSelector selectedKeys : selects) { - AppConfigurationApplicationSettingPropertySource propertySource = new AppConfigurationApplicationSettingPropertySource( - store.getEndpoint(), client, keyVaultClientFactory, selectedKeys.getKeyFilter(), - selectedKeys.getLabelFilter(profiles), appProperties.getMaxRetryTime()); - propertySource.initProperties(); - sourceList.add(propertySource); - } - if (store.getFeatureFlags().getEnabled()) { for (FeatureFlagKeyValueSelector selectedKeys : store.getFeatureFlags().getSelects()) { AppConfigurationFeatureManagementPropertySource propertySource = new AppConfigurationFeatureManagementPropertySource( @@ -280,6 +270,14 @@ private List create(AppConfigurationReplicaClien } } + for (AppConfigurationKeyValueSelector selectedKeys : selects) { + AppConfigurationApplicationSettingPropertySource propertySource = new AppConfigurationApplicationSettingPropertySource( + store.getEndpoint(), client, keyVaultClientFactory, selectedKeys.getKeyFilter(), + selectedKeys.getLabelFilter(profiles), appProperties.getMaxRetryTime()); + propertySource.initProperties(); + sourceList.add(propertySource); + } + return sourceList; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java index c6bf5d85dff4..a647219fd384 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java @@ -12,14 +12,16 @@ import org.springframework.util.StringUtils; 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.config.implementation.http.policy.BaseAppConfigurationPolicy; import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring; import com.azure.spring.cloud.config.implementation.properties.FeatureFlagKeyValueSelector; import com.azure.spring.cloud.config.implementation.properties.FeatureFlagStore; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX; -import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_STORE_WATCH_KEY; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.SELECT_ALL_FEATURE_FLAGS; class AppConfigurationRefreshUtil { @@ -171,8 +173,7 @@ private static void refreshWithTime(AppConfigurationReplicaClient client, State if (eventData.getDoRefresh()) { // Just need to reset refreshInterval, if a refresh was triggered it will be updated after loading the - // new - // configurations. + // new configurations. StateHolder.getCurrentState().updateStateRefresh(state, refreshInterval); } } @@ -207,8 +208,10 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl Instant date = Instant.now(); if (date.isAfter(state.getNextRefreshCheck())) { + int watchedKeySize = 0; + for (FeatureFlagKeyValueSelector watchKey : featureStore.getSelects()) { - String keyFilter = FEATURE_STORE_WATCH_KEY; + String keyFilter = SELECT_ALL_FEATURE_FLAGS; if (StringUtils.hasText(watchKey.getKeyFilter())) { keyFilter = FEATURE_FLAG_PREFIX + watchKey.getKeyFilter(); @@ -218,42 +221,46 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl .setLabelFilter(watchKey.getLabelFilterText(profiles)); List currentKeys = client.listSettings(selector); - int watchedKeySize = 0; - - keyCheck: for (ConfigurationSetting currentKey : currentKeys) { + watchedKeySize = checkFeatureFlags(currentKeys, state, client, eventData); + } - watchedKeySize += 1; - for (ConfigurationSetting watchFlag : state.getWatchKeys()) { + if (!eventData.getDoRefresh() && watchedKeySize != state.getWatchKeys().size()) { + String eventDataInfo = ".appconfig.featureflag/*"; - // If there is no result, etag will be considered empty. - // A refresh will trigger once the selector returns a value. - if (watchFlag != null && watchFlag.getKey().equals(currentKey.getKey()) - && watchFlag.getLabel().equals(currentKey.getLabel())) { - checkETag(watchFlag, currentKey, client.getEndpoint(), eventData); - if (eventData.getDoRefresh()) { - break keyCheck; + // Only one refresh Event needs to be call to update all of the + // stores, not one for each. + LOGGER.info("Configuration Refresh Event triggered by " + eventDataInfo); - } - } + eventData.setMessage(eventDataInfo); + } - } - } + // Just need to reset refreshInterval, if a refresh was triggered it will be updated after loading the new + // configurations. + StateHolder.getCurrentState().updateStateRefresh(state, refreshInterval); + } + } - if (watchedKeySize != state.getWatchKeys().size()) { - String eventDataInfo = ".appconfig.featureflag/*"; + private static int checkFeatureFlags(List currentKeys, State state, + AppConfigurationReplicaClient client, RefreshEventData eventData) { + int watchedKeySize = 0; + for (ConfigurationSetting currentKey : currentKeys) { + if (currentKey instanceof FeatureFlagConfigurationSetting + && FEATURE_FLAG_CONTENT_TYPE.equals(currentKey.getContentType())) { - // Only one refresh Event needs to be call to update all of the - // stores, not one for each. - LOGGER.info("Configuration Refresh Event triggered by " + eventDataInfo); + watchedKeySize += 1; + for (ConfigurationSetting watchFlag : state.getWatchKeys()) { - eventData.setMessage(eventDataInfo); + // If there is no result, etag will be considered empty. + // A refresh will trigger once the selector returns a value. + if (compairKeys(watchFlag, currentKey, client.getEndpoint(), eventData)) { + if (eventData.getDoRefresh()) { + return watchedKeySize; + } + } } } - - // Just need to reset refreshInterval, if a refresh was triggered it will be updated after loading the new - // configurations. - StateHolder.getCurrentState().updateStateRefresh(state, refreshInterval); } + return watchedKeySize; } private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient client, @@ -272,9 +279,7 @@ private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient // If there is no result, etag will be considered empty. // A refresh will trigger once the selector returns a value. - if (watchFlag != null && watchFlag.getKey().equals(currentTriggerConfiguration.getKey()) - && watchFlag.getLabel().equals(currentTriggerConfiguration.getLabel())) { - checkETag(watchFlag, currentTriggerConfiguration, client.getEndpoint(), eventData); + if (compairKeys(watchFlag, currentTriggerConfiguration, client.getEndpoint(), eventData)) { if (eventData.getDoRefresh()) { return; } @@ -297,6 +302,16 @@ private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient } } + private static Boolean compairKeys(ConfigurationSetting key1, ConfigurationSetting key2, + String endpoint, RefreshEventData eventData) { + if (key1 != null && key1.getKey().equals(key2.getKey()) && key1.getLabel().equals(key2.getLabel())) { + checkETag(key1, key2, endpoint, eventData); + return true; + } + return false; + + } + private static void checkETag(ConfigurationSetting watchSetting, ConfigurationSetting currentTriggerConfiguration, String endpoint, RefreshEventData eventData) { if (currentTriggerConfiguration == null) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java index acb0e0a82ea5..03288a2d928d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java @@ -13,6 +13,7 @@ import com.azure.data.appconfiguration.ConfigurationClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; +import com.azure.spring.cloud.config.implementation.http.policy.TracingInfo; /** * Client for connecting to App Configuration when multiple replicas are in use. @@ -27,16 +28,19 @@ class AppConfigurationReplicaClient { private int failedAttempts; + private final TracingInfo tracingInfo; + /** * Holds Configuration Client and info needed to manage backoff. * @param endpoint client endpoint * @param client Configuration Client to App Configuration store */ - AppConfigurationReplicaClient(String endpoint, ConfigurationClient client) { + AppConfigurationReplicaClient(String endpoint, ConfigurationClient client, TracingInfo tracingInfo) { this.endpoint = endpoint; this.client = client; this.backoffEndTime = Instant.now().minusMillis(1); this.failedAttempts = 0; + this.tracingInfo = tracingInfo; } /** @@ -148,4 +152,8 @@ void updateSyncToken(String syncToken) { } } + TracingInfo getTracingInfo() { + return tracingInfo; + } + } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java index 876eb22b531d..35f6c53fc7ab 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java @@ -9,6 +9,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.convert.DurationStyle; @@ -20,12 +21,14 @@ import com.azure.core.http.policy.ExponentialBackoff; import com.azure.core.http.policy.RetryPolicy; import com.azure.core.http.policy.RetryStrategy; +import com.azure.core.util.Configuration; import com.azure.data.appconfiguration.ConfigurationClientBuilder; import com.azure.identity.ManagedIdentityCredentialBuilder; import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.appconfiguration.AzureAppConfigurationProperties; import com.azure.spring.cloud.config.ConfigurationClientCustomizer; import com.azure.spring.cloud.config.implementation.http.policy.BaseAppConfigurationPolicy; +import com.azure.spring.cloud.config.implementation.http.policy.TracingInfo; import com.azure.spring.cloud.config.implementation.properties.ConfigStore; import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory; @@ -72,7 +75,7 @@ public class AppConfigurationReplicaClientsBuilder implements EnvironmentAware { private boolean isDev = false; private boolean isKeyVaultConfigured = false; - + private final boolean credentialConfigured; private final int defaultMaxRetries; @@ -134,7 +137,7 @@ List buildClients(ConfigStore configStore) { throw new IllegalArgumentException( "More than 1 Connection method was set for connecting to App Configuration."); } - + boolean connectionStringIsPresent = configStore.getConnectionString() != null; if (credentialConfigured && connectionStringIsPresent) { @@ -182,12 +185,13 @@ List buildClients(ConfigStore configStore) { private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint, Integer replicaCount) { - builder.addPolicy(new BaseAppConfigurationPolicy(isDev, isKeyVaultConfigured, replicaCount)); + TracingInfo tracingInfo = new TracingInfo(isDev, isKeyVaultConfigured, replicaCount, Configuration.getGlobalConfiguration()); + builder.addPolicy(new BaseAppConfigurationPolicy(tracingInfo)); if (clientProvider != null) { clientProvider.customize(builder, endpoint); } - return new AppConfigurationReplicaClient(endpoint, builder.buildClient()); + return new AppConfigurationReplicaClient(endpoint, builder.buildClient(), tracingInfo); } @Override diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/HostType.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/HostType.java index 604dfa2165f9..905321bc8e12 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/HostType.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/HostType.java @@ -25,7 +25,12 @@ public enum HostType { /** * Host is Kubernetes */ - KUBERNETES("Kubernetes"); + KUBERNETES("Kubernetes"), + + /** + * Host is Container App + */ + CONTAINER_APP("ContainerApps"); private final String text; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestTracingConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestTracingConstants.java index 3693a0c09a6b..f8bc1305916a 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestTracingConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestTracingConstants.java @@ -27,6 +27,11 @@ public enum RequestTracingConstants { */ KUBERNETES_ENVIRONMENT_VARIABLE("KUBERNETES_PORT"), + /** + * Constant for checking for use in Container App. + */ + CONTAINER_APP_ENVIRONMENT_VARIABLE("CONTAINER_APP_NAME"), + /** * Constant for tracing the type of request */ diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicy.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicy.java index 0590ae0bc477..3a6c8c940572 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicy.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicy.java @@ -2,20 +2,15 @@ // Licensed under the MIT License. package com.azure.spring.cloud.config.implementation.http.policy; -import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.DEV_ENV_TRACING; -import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.USER_AGENT_TYPE; import org.springframework.util.StringUtils; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; -import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.HttpPipelinePolicy; -import com.azure.spring.cloud.config.implementation.HostType; import com.azure.spring.cloud.config.implementation.RequestTracingConstants; -import com.azure.spring.cloud.config.implementation.RequestType; import reactor.core.publisher.Mono; /** @@ -36,95 +31,14 @@ public final class BaseAppConfigurationPolicy implements HttpPipelinePolicy { static Boolean watchRequests = false; - final boolean isDev; - - final boolean isKeyVaultConfigured; - - final int replicaCount; + final TracingInfo tracingInfo; /** * App Configuration Http Pipeline Policy - * @param isDev is using dev profile - * @param isKeyVaultConfigured is key vault configured - * @param replicaCount number of replicas being used. Should equal the number of endpoints minus one. - */ - public BaseAppConfigurationPolicy(Boolean isDev, Boolean isKeyVaultConfigured, Integer replicaCount) { - this.isDev = isDev; - this.isKeyVaultConfigured = isKeyVaultConfigured; - this.replicaCount = replicaCount; - } - - /** - * - * Checks if Azure App Configuration Tracing is disabled, and if not gets tracing information. - * - * @param request The http request that will be traced, used to check operation being run. - * @return String of the value for the correlation-context header. - */ - private String getTracingInfo(HttpRequest request) { - String track = System.getenv(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString()); - if ("false".equalsIgnoreCase(track)) { - return ""; - } - - RequestType requestTypeValue = watchRequests ? RequestType.WATCH : RequestType.STARTUP; - - String tracingInfo = RequestTracingConstants.REQUEST_TYPE_KEY + "=" + requestTypeValue; - String hostType = getHostType(); - - if (!hostType.isEmpty()) { - tracingInfo += "," + RequestTracingConstants.HOST_TYPE_KEY + "=" + getHostType(); - } - - if (isDev || isKeyVaultConfigured) { - tracingInfo += ",Env=" + getEnvInfo(); - } - - if (replicaCount > 0) { - tracingInfo += "," + RequestTracingConstants.REPLICA_COUNT + "=" + replicaCount; - } - - return tracingInfo; - - } - - private String getEnvInfo() { - String envInfo = ""; - - envInfo = buildEnvTracingInfo(envInfo, isDev, DEV_ENV_TRACING); - envInfo = buildEnvTracingInfo(envInfo, isKeyVaultConfigured, KEY_VAULT_CONFIGURED_TRACING); - - return envInfo; - } - - private String buildEnvTracingInfo(String envInfo, Boolean check, String checkString) { - if (check) { - if (envInfo.length() > 0) { - envInfo += ","; - } - envInfo += checkString; - } - return envInfo; - } - - /** - * Gets the current host machines type; Azure Function, Azure Web App, Kubernetes, or Empty. - * - * @return String of Host Type + * @param tracingInfo Usage info for provider */ - private static String getHostType() { - HostType hostType = HostType.UNIDENTIFIED; - - if (System.getenv(RequestTracingConstants.AZURE_FUNCTIONS_ENVIRONMENT_VARIABLE.toString()) != null) { - hostType = HostType.AZURE_FUNCTION; - } else if (System.getenv(RequestTracingConstants.AZURE_WEB_APP_ENVIRONMENT_VARIABLE.toString()) != null) { - hostType = HostType.AZURE_WEB_APP; - } else if (System.getenv(RequestTracingConstants.KUBERNETES_ENVIRONMENT_VARIABLE.toString()) != null) { - hostType = HostType.KUBERNETES; - } - - return hostType.toString(); - + public BaseAppConfigurationPolicy(TracingInfo tracingInfo) { + this.tracingInfo = tracingInfo; } @Override @@ -132,7 +46,7 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN String sdkUserAgent = context.getHttpRequest().getHeaders().get(USER_AGENT_TYPE).getValue(); context.getHttpRequest().getHeaders().set(USER_AGENT_TYPE, USER_AGENT + " " + sdkUserAgent); context.getHttpRequest().getHeaders().set(RequestTracingConstants.CORRELATION_CONTEXT_HEADER.toString(), - getTracingInfo(context.getHttpRequest())); + tracingInfo.getValue(watchRequests)); return next.process(); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/FeatureFlagTracing.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/FeatureFlagTracing.java new file mode 100644 index 000000000000..8d496c7c2506 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/FeatureFlagTracing.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.http.policy; + +import java.util.Arrays; +import java.util.List; + +public class FeatureFlagTracing { + + private static final String CUSTOM_FILTER = "CSTM"; + + private static final String PERCENTAGE_FILTER = "PRCNT"; + + private static final String TIME_WINDOW_FILTER = "TIME"; + + private static final String TARGETING_FILTER = "TRGT"; + + private static final String FILTER_TYPE_DELIMITER = "+"; + + private static final List PERCENTAGE_FILTER_NAMES = Arrays.asList("Percentage", "Microsoft.Percentage", + "PercentageFilter", "Microsoft.PercentageFilter"); + + private static final List TIME_WINDOW_FILTER_NAMES = Arrays.asList("TimeWindow", "Microsoft.TimeWindow", + "TimeWindowFilter", "Microsoft.TimeWindowFilter"); + + private static final List TARGETING_FILTER_NAMES = Arrays.asList("Targeting", "Microsoft.Targeting", + "TargetingFilter", "Microsoft.TargetingFilter"); + + private Boolean usesCustomFilter = false; + + private Boolean usesPercentageFilter = false; + + private Boolean usesTimeWindowFilter = false; + + private Boolean usesTargetingFilter = false; + + public boolean usesAnyFilter() { + return usesCustomFilter || usesPercentageFilter || usesTimeWindowFilter || usesTargetingFilter; + } + + public void resetFeatureFilterTelemetry() { + usesCustomFilter = false; + usesPercentageFilter = false; + usesTimeWindowFilter = false; + usesTargetingFilter = false; + } + + public void updateFeatureFilterTelemetry(String filterName) { + if (PERCENTAGE_FILTER_NAMES.stream().anyMatch(name -> name.equalsIgnoreCase(filterName))) { + usesPercentageFilter = true; + } else if (TIME_WINDOW_FILTER_NAMES.stream().anyMatch(name -> name.equalsIgnoreCase(filterName))) { + usesTimeWindowFilter = true; + } else if (TARGETING_FILTER_NAMES.stream().anyMatch(name -> name.equalsIgnoreCase(filterName))) { + usesTargetingFilter = true; + } else { + usesCustomFilter = true; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (usesCustomFilter) { + sb.append(CUSTOM_FILTER); + } + if (usesPercentageFilter) { + sb.append(sb.length() > 0 ? FILTER_TYPE_DELIMITER : "").append(PERCENTAGE_FILTER); + } + if (usesTimeWindowFilter) { + sb.append(sb.length() > 0 ? FILTER_TYPE_DELIMITER : "").append(TIME_WINDOW_FILTER); + } + if (usesTargetingFilter) { + sb.append(sb.length() > 0 ? FILTER_TYPE_DELIMITER : "").append(TARGETING_FILTER); + } + return sb.toString(); + } + +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/TracingInfo.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/TracingInfo.java new file mode 100644 index 000000000000..e62b56693b8a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/TracingInfo.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.http.policy; + +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.DEV_ENV_TRACING; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; + +import com.azure.core.util.Configuration; +import com.azure.spring.cloud.config.implementation.HostType; +import com.azure.spring.cloud.config.implementation.RequestTracingConstants; +import com.azure.spring.cloud.config.implementation.RequestType; + +public class TracingInfo { + + private boolean isDev = false; + + private boolean isKeyVaultConfigured = false; + + private int replicaCount; + + private final FeatureFlagTracing featureFlagTracing; + + private final Configuration configuration; + + public TracingInfo(boolean isDev, boolean isKeyVaultConfigured, int replicaCount, Configuration configuration) { + this.isDev = isDev; + this.isKeyVaultConfigured = isKeyVaultConfigured; + this.replicaCount = replicaCount; + this.featureFlagTracing = new FeatureFlagTracing(); + this.configuration = configuration; + } + + public String getValue(boolean watchRequests) { + String track = configuration.get(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString()); + if (track != null && Boolean.valueOf(track)) { + return ""; + } + + RequestType requestTypeValue = watchRequests ? RequestType.WATCH : RequestType.STARTUP; + StringBuilder sb = new StringBuilder(); + + sb.append(RequestTracingConstants.REQUEST_TYPE_KEY).append("=" + requestTypeValue); + + if (featureFlagTracing != null && featureFlagTracing.usesAnyFilter()) { + sb.append(",Filter=").append(featureFlagTracing.toString()); + } + + String hostType = getHostType(); + if (!hostType.isEmpty()) { + sb.append(",").append(RequestTracingConstants.HOST_TYPE_KEY).append("=").append(hostType); + } + + if (isDev) { + sb.append(",Env=").append(DEV_ENV_TRACING); + } + if (isKeyVaultConfigured) { + sb.append(",").append(KEY_VAULT_CONFIGURED_TRACING); + } + + if (replicaCount > 0) { + sb.append(",").append(RequestTracingConstants.REPLICA_COUNT).append("=").append(replicaCount); + } + + return sb.toString(); + } + + /** + * Gets the current host machines type; Azure Function, Azure Web App, Kubernetes, or Empty. + * + * @return String of Host Type + */ + private static String getHostType() { + HostType hostType = HostType.UNIDENTIFIED; + + if (System.getenv(RequestTracingConstants.AZURE_FUNCTIONS_ENVIRONMENT_VARIABLE.toString()) != null) { + hostType = HostType.AZURE_FUNCTION; + } else if (System.getenv(RequestTracingConstants.AZURE_WEB_APP_ENVIRONMENT_VARIABLE.toString()) != null) { + hostType = HostType.AZURE_WEB_APP; + } else if (System.getenv(RequestTracingConstants.KUBERNETES_ENVIRONMENT_VARIABLE.toString()) != null) { + hostType = HostType.KUBERNETES; + } else if (System.getenv(RequestTracingConstants.CONTAINER_APP_ENVIRONMENT_VARIABLE.toString()) != null) { + hostType = HostType.CONTAINER_APP; + } + + return hostType.toString(); + } + + /** + * @return the featureFlagTracing + */ + public FeatureFlagTracing getFeatureFlagTracing() { + return featureFlagTracing; + } + +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/ConfigStore.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/ConfigStore.java index dce7b6e41125..2d39df885a27 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/ConfigStore.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/ConfigStore.java @@ -88,6 +88,13 @@ public List getConnectionStrings() { return connectionStrings; } + /** + * @param connectionStrings the connectionStrings to set + */ + public void setConnectionStrings(List connectionStrings) { + this.connectionStrings = connectionStrings; + } + /** * @return the failFast */ diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java index de07e656bef1..1f4021725d9f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java @@ -35,11 +35,13 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import com.azure.core.util.Configuration; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagFilter; import com.azure.spring.cloud.config.implementation.feature.management.entity.Feature; import com.azure.spring.cloud.config.implementation.feature.management.entity.FeatureSet; +import com.azure.spring.cloud.config.implementation.http.policy.TracingInfo; import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; import com.azure.spring.cloud.config.implementation.properties.FeatureFlagStore; import com.fasterxml.jackson.databind.ObjectMapper; @@ -82,7 +84,7 @@ public class AppConfigurationFeatureManagementPropertySourceTest { @Mock private AppConfigurationReplicaClient clientMock; - + private FeatureFlagStore featureFlagStore; @Mock @@ -124,6 +126,7 @@ public void testFeatureFlagCanBeInitedAndQueried() { when(featureListMock.iterator()).thenReturn(FEATURE_ITEMS.iterator()); when(clientMock.listSettings(Mockito.any())) .thenReturn(featureListMock).thenReturn(featureListMock); + when(clientMock.getTracingInfo()).thenReturn(new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); featureFlagStore.setEnabled(true); propertySource.initProperties(); @@ -149,6 +152,7 @@ public void testFeatureFlagCanBeInitedAndQueried() { public void testFeatureFlagThrowError() { when(featureListMock.iterator()).thenReturn(FEATURE_ITEMS.iterator()); when(clientMock.listSettings(Mockito.any())).thenReturn(featureListMock); + when(clientMock.getTracingInfo()).thenReturn(new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); try { propertySource.initProperties(); } catch (Exception e) { @@ -178,6 +182,7 @@ public void testFeatureFlagTargeting() { when(featureListMock.iterator()).thenReturn(FEATURE_ITEMS_TARGETING.iterator()); when(clientMock.listSettings(Mockito.any())) .thenReturn(featureListMock).thenReturn(featureListMock); + when(clientMock.getTracingInfo()).thenReturn(new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); featureFlagStore.setEnabled(true); propertySource.initProperties(); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java index 77459dee3dd4..7c0f9673b515 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java @@ -363,8 +363,8 @@ public void storeCreatedWithFeatureFlags() { // [/foo_prod/, /foo_dev/, /foo/, /application_prod/, /application_dev/, // /application/] String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/\0", - "FM_store1/" + "FM_store1/", + KEY_FILTER + "store1/\0" }; assertEquals(expectedSourceNames.length, sources.size()); assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); @@ -401,8 +401,8 @@ public void storeCreatedWithFeatureFlagsWithMonitoring() { // [/foo_prod/, /foo_dev/, /foo/, /application_prod/, /application_dev/, // /application/] String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/\0", - "FM_store1/" + "FM_store1/", + KEY_FILTER + "store1/\0" }; assertEquals(expectedSourceNames.length, sources.size()); assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java index 04e5e3c861fe..6bd2c62c3817 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java @@ -20,9 +20,11 @@ import com.azure.core.exception.HttpResponseException; import com.azure.core.http.HttpResponse; import com.azure.core.http.rest.PagedIterable; +import com.azure.core.util.Configuration; import com.azure.data.appconfiguration.ConfigurationClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; +import com.azure.spring.cloud.config.implementation.http.policy.TracingInfo; public class AppConfigurationReplicaClientTest { @@ -47,7 +49,8 @@ public void setup() { @Test public void getWatchKeyTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, + new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); ConfigurationSetting watchKey = new ConfigurationSetting().setKey("watch").setLabel("\0"); @@ -73,7 +76,8 @@ public void getWatchKeyTest() { @Test public void listSettingsTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, + new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); List configurations = new ArrayList<>(); @@ -99,7 +103,8 @@ public void listSettingsTest() { @Test public void backoffTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, + new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); // Setups in the past and with no errors. assertTrue(client.getBackoffEndTime().isBefore(Instant.now())); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java index bba64a81e2df..6ca602ef947d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java @@ -27,6 +27,7 @@ import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpRequest; +import com.azure.core.util.Configuration; // This test class needs to be isolated and ran sequential as it uses BaseAppConfigurationPolicy.setWatchRequests // which mutates a global static and can result in race condition failures. @@ -59,7 +60,8 @@ public void startupThenWatchUpdateTest() throws MalformedURLException { URL url = new URL("https://www.test.url/kv"); HttpRequest request = new HttpRequest(HttpMethod.GET, url); request.setHeader(USER_AGENT_TYPE, "PreExistingUserAgent"); - BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy(false, false, 0); + BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( + new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); when(contextMock.getHttpRequest()).thenReturn(request); @@ -98,7 +100,8 @@ public void startupThenWatchUpdateTest() throws MalformedURLException { @Test public void devIsConfigured() throws MalformedURLException { - BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy(true, false, 0); + BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( + new TracingInfo(true, false, 0, Configuration.getGlobalConfiguration())); URL url = new URL("https://www.test.url/kv"); HttpRequest request = new HttpRequest(HttpMethod.GET, url); @@ -112,7 +115,8 @@ public void devIsConfigured() throws MalformedURLException { @Test public void keyVaultIsConfigured() throws MalformedURLException { - BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy(false, true, 0); + BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( + new TracingInfo(false, true, 0, Configuration.getGlobalConfiguration())); URL url = new URL("https://www.test.url/kv"); HttpRequest request = new HttpRequest(HttpMethod.GET, url); @@ -120,13 +124,14 @@ public void keyVaultIsConfigured() throws MalformedURLException { when(contextMock.getHttpRequest()).thenReturn(request); policy.process(contextMock, nextMock); - assertEquals("RequestType=Startup,Env=" + KEY_VAULT_CONFIGURED_TRACING, + assertEquals("RequestType=Startup," + KEY_VAULT_CONFIGURED_TRACING, contextMock.getHttpRequest().getHeaders().get(CORRELATION_CONTEXT).getValue()); } @Test public void devAndKeyVaultAreConfigured() throws MalformedURLException { - BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy(true, true, 0); + BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( + new TracingInfo(true, true, 0, Configuration.getGlobalConfiguration())); URL url = new URL("https://www.test.url/kv"); HttpRequest request = new HttpRequest(HttpMethod.GET, url); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/FeatureFlagTracingTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/FeatureFlagTracingTest.java new file mode 100644 index 000000000000..777d58365130 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/FeatureFlagTracingTest.java @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.http.policy; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class FeatureFlagTracingTest { + + @Test + public void usesPercentageFilter() { + FeatureFlagTracing tracing = new FeatureFlagTracing(); + tracing.updateFeatureFilterTelemetry("Percentage"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("PRCNT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("Microsoft.Percentage"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("PRCNT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("PercentageFilter"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("PRCNT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("Microsoft.PercentageFilter"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("PRCNT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + } + + @Test + public void usesTimeWindowFilter() { + FeatureFlagTracing tracing = new FeatureFlagTracing(); + tracing.updateFeatureFilterTelemetry("TimeWindow"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("TIME", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("Microsoft.TimeWindow"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("TIME", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("TimeWindowFilter"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("TIME", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("Microsoft.TimeWindowFilter"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("TIME", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + } + + @Test + public void usesTargetingFilter() { + FeatureFlagTracing tracing = new FeatureFlagTracing(); + tracing.updateFeatureFilterTelemetry("Targeting"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("TRGT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("Microsoft.Targeting"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("TRGT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("TargetingFilter"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("TRGT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("Microsoft.TargetingFilter"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("TRGT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + } + + @Test + public void usesCustomFilter() { + FeatureFlagTracing tracing = new FeatureFlagTracing(); + tracing.updateFeatureFilterTelemetry("Random"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("CSTM", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("Microsoft.Random"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("CSTM", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("ABTest"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("CSTM", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("TargingFilter"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("CSTM", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + } + + @Test + public void usesMultipleFilters() { + FeatureFlagTracing tracing = new FeatureFlagTracing(); + assertFalse(tracing.usesAnyFilter()); + + tracing.updateFeatureFilterTelemetry("Percentage"); + tracing.updateFeatureFilterTelemetry("TimeWindow"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("PRCNT+TIME", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("Percentage"); + tracing.updateFeatureFilterTelemetry("Targeting"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("PRCNT+TRGT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + + tracing.updateFeatureFilterTelemetry("Percentage"); + tracing.updateFeatureFilterTelemetry("Random"); + + assertTrue(tracing.usesAnyFilter()); + assertEquals("CSTM+PRCNT", tracing.toString()); + tracing.resetFeatureFilterTelemetry(); + assertEquals("", tracing.toString()); + } + +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/TracingInfoTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/TracingInfoTest.java new file mode 100644 index 000000000000..de7b95575663 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/TracingInfoTest.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.http.policy; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.azure.core.util.Configuration; +import com.azure.core.util.ConfigurationBuilder; +import com.azure.core.util.ConfigurationSource; +import com.azure.spring.cloud.config.implementation.RequestTracingConstants; + +public class TracingInfoTest { + + @Test + public void getValueTest() { + Configuration configuration = getConfiguration("false"); + + TracingInfo tracingInfo = new TracingInfo(false, false, 0, configuration); + assertEquals("RequestType=Startup", tracingInfo.getValue(false)); + assertEquals("RequestType=Watch", tracingInfo.getValue(true)); + + tracingInfo = new TracingInfo(true, false, 0, configuration); + assertEquals("RequestType=Startup,Env=Dev", tracingInfo.getValue(false)); + + tracingInfo = new TracingInfo(false, true, 0, configuration); + assertEquals("RequestType=Startup,UsesKeyVault", tracingInfo.getValue(false)); + + tracingInfo = new TracingInfo(false, false, 1, configuration); + assertEquals("RequestType=Startup,ReplicaCount=1", tracingInfo.getValue(false)); + + tracingInfo = new TracingInfo(false, false, 0, configuration); + tracingInfo.getFeatureFlagTracing().updateFeatureFilterTelemetry("Random"); + assertEquals("RequestType=Startup,Filter=CSTM", tracingInfo.getValue(false)); + } + + @Test + public void disableTracingTest() { + TracingInfo tracingInfo = new TracingInfo(false, false, 0, getConfiguration(null)); + assertNotEquals("", tracingInfo.getValue(false)); + + tracingInfo = new TracingInfo(false, false, 0, getConfiguration("")); + assertNotEquals("", tracingInfo.getValue(false)); + + tracingInfo = new TracingInfo(false, false, 0, getConfiguration("true")); + assertEquals("", tracingInfo.getValue(false)); + + tracingInfo = new TracingInfo(false, false, 0, getConfiguration("false")); + assertNotEquals("", tracingInfo.getValue(false)); + + tracingInfo = new TracingInfo(false, false, 0, getConfiguration("random string")); + assertNotEquals("", tracingInfo.getValue(false)); + } + + private static final ConfigurationSource EMPTY_SOURCE = new ConfigurationSource() { + @Override + public Map getProperties(String source) { + return Collections.emptyMap(); + } + }; + + private Configuration getConfiguration(String value) { + return new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource().put(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString(), value)).build(); + } + + private final class TestConfigurationSource implements ConfigurationSource { + private final Map testData; + + /** + * Creates TestConfigurationSource with given property names and values. + */ + TestConfigurationSource() { + this.testData = new HashMap<>(); + } + + /** + * Adds property name and value to the source. + * + * @param name property name + * @param value property value + * @return this {@code TestConfigurationSource} for chaining. + */ + public TestConfigurationSource put(String name, String value) { + this.testData.put(name, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public Map getProperties(String path) { + if (path == null) { + return testData; + } + return testData.entrySet().stream() + .filter(prop -> prop.getKey().startsWith(path + ".")) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + } + +}