From 5bcf7abfe515063770232b74793ab49e60564818 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 29 Apr 2026 11:06:33 -0700 Subject: [PATCH 1/5] Adding Snapshot Reference Support --- ...ationApplicationSettingPropertySource.java | 51 ++++++++++++++++--- ...ppConfigurationSnapshotPropertySource.java | 30 ++++++++--- .../AzureAppConfigDataLoader.java | 2 +- ...nApplicationSettingPropertySourceTest.java | 13 +++-- ...nfigurationPropertySourceKeyVaultTest.java | 5 +- 5 files changed, 79 insertions(+), 22 deletions(-) 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..8193605a06d0 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 @@ -24,6 +24,7 @@ import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings; /** * Azure App Configuration PropertySource unique per Store Label(Profile) combo. @@ -45,9 +46,15 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP private final List tagsFilter; + protected List featureFlagsList = new ArrayList<>(); + + private final String SNAPSHOT_REF_CONTENT_TYPE = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8"; + + protected final FeatureFlagClient featureFlagClient; + AppConfigurationApplicationSettingPropertySource(String name, AppConfigurationReplicaClient replicaClient, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, String keyFilter, String[] labelFilters, - List tagsFilter) { + List tagsFilter, FeatureFlagClient featureFlagClient) { // The context alone does not uniquely define a PropertySource, append storeName // and label to uniquely define a PropertySource super(name + getLabelName(labelFilters), replicaClient); @@ -55,6 +62,7 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP this.keyFilter = keyFilter; this.labelFilters = labelFilters; this.tagsFilter = tagsFilter; + this.featureFlagClient = featureFlagClient; } /** @@ -66,7 +74,8 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP * @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type */ @Override - public void initProperties(List keyPrefixTrimValues, Context context) throws InvalidConfigurationPropertyValueException { + public void initProperties(List keyPrefixTrimValues, Context context) + throws InvalidConfigurationPropertyValueException { List labels = Arrays.asList(labelFilters); // Reverse labels so they have the right priority order. @@ -80,7 +89,8 @@ public void initProperties(List keyPrefixTrimValues, Context context) th } // * for wildcard match - processConfigurationSettings(replicaClient.listSettings(settingSelector, context), settingSelector.getKeyFilter(), + processConfigurationSettings(replicaClient.listSettings(settingSelector, context), + settingSelector.getKeyFilter(), keyPrefixTrimValues); } } @@ -88,6 +98,9 @@ public void initProperties(List keyPrefixTrimValues, Context context) th protected void processConfigurationSettings(List settings, String keyFilter, List keyPrefixTrimValues) throws InvalidConfigurationPropertyValueException { + // First resolve snapshot references + settings = resolveSnapshotReferences(settings); + for (ConfigurationSetting setting : settings) { if (keyPrefixTrimValues == null && StringUtils.hasText(keyFilter)) { keyPrefixTrimValues = new ArrayList<>(); @@ -107,6 +120,29 @@ protected void processConfigurationSettings(List settings, properties.put(key, setting.getValue()); } } + + WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, featureFlagsList); + featureFlagClient.proccessFeatureFlags(featureFlags, replicaClient.getEndpoint()); + } + + private List resolveSnapshotReferences(List settings) { + List resolvedSettings = new ArrayList<>(); + for (ConfigurationSetting setting : settings) { + if (SNAPSHOT_REF_CONTENT_TYPE.equals(setting.getContentType())) { + // Handle snapshot reference + List snapshotSettings = replicaClient.listSettingSnapshot(setting.getValue(), + Context.NONE); + resolvedSettings.addAll(snapshotSettings); + } else if (setting instanceof FeatureFlagConfigurationSetting) { + // We need to strip feature flags as we only support feature flags from snapshots, and if they are in a + // snapshot reference we won't be able to resolve them. + LOGGER.warn("Feature Flag {} with key {} is being ignored as it is not from a snapshot reference.", + setting.getLabel(), setting.getKey()); + } else { + resolvedSettings.add(setting); + } + } + return resolvedSettings; } /** @@ -117,7 +153,7 @@ protected void processConfigurationSettings(List settings, * @return Key Vault Secret Value * @throws InvalidConfigurationPropertyValueException */ - private void handleKeyVaultReference(String key, SecretReferenceConfigurationSetting secretReference) + protected void handleKeyVaultReference(String key, SecretReferenceConfigurationSetting secretReference) throws InvalidConfigurationPropertyValueException { // Parsing Key Vault Reference for URI try { @@ -136,10 +172,11 @@ private void handleKeyVaultReference(String key, SecretReferenceConfigurationSet void handleFeatureFlag(String key, FeatureFlagConfigurationSetting setting, List trimStrings) throws InvalidConfigurationPropertyValueException { - // Feature Flags aren't loaded as configuration, but are loaded as feature flags when loading a snapshot. + // Feature Flags are only part of this if they come from a snapshot + featureFlagsList.add(setting); } - private void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues) + protected void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues) throws InvalidConfigurationPropertyValueException { Map jsonSettings = JsonConfigurationParser.parseJsonSetting(setting); for (Entry jsonSetting : jsonSettings.entrySet()) { @@ -148,7 +185,7 @@ private void handleJson(ConfigurationSetting setting, List keyPrefixTrim } } - private String trimKey(String key, List trimStrings) { + protected String trimKey(String key, List trimStrings) { key = key.trim(); if (trimStrings != null) { for (String trim : trimStrings) { 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..8038f42ede86 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 @@ -2,14 +2,17 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; -import java.util.ArrayList; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; + import java.util.List; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.util.StringUtils; 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.SecretReferenceConfigurationSetting; import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings; /** @@ -23,18 +26,13 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli private final String snapshotName; - private final FeatureFlagClient featureFlagClient; - - private List featureFlagsList = new ArrayList<>(); - AppConfigurationSnapshotPropertySource(String name, AppConfigurationReplicaClient replicaClient, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, String snapshotName, FeatureFlagClient featureFlagClient) { // The context alone does not uniquely define a PropertySource, append storeName // and label to uniquely define a PropertySource - super(name, replicaClient, keyVaultClientFactory, null, null, null); + super(name, replicaClient, keyVaultClientFactory, null, null, null, featureFlagClient); this.snapshotName = snapshotName; - this.featureFlagClient = featureFlagClient; } /** @@ -47,7 +45,23 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli * @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type */ public void initProperties(List trim, Context context) throws InvalidConfigurationPropertyValueException { - processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName, context), null, trim); + List settings = replicaClient.listSettingSnapshot(snapshotName, context); + + for (ConfigurationSetting setting : settings) { + String key = trimKey(setting.getKey(), trim); + + if (setting instanceof SecretReferenceConfigurationSetting) { + handleKeyVaultReference(key, (SecretReferenceConfigurationSetting) setting); + } else if (setting instanceof FeatureFlagConfigurationSetting + && FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) { + handleFeatureFlag(key, (FeatureFlagConfigurationSetting) setting, trim); + } else if (StringUtils.hasText(setting.getContentType()) + && JsonConfigurationParser.isJsonContentType(setting.getContentType())) { + handleJson(setting, trim); + } else { + properties.put(key, setting.getValue()); + } + } WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, featureFlagsList); featureFlagClient.proccessFeatureFlags(featureFlags, replicaClient.getEndpoint()); 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..5d61ff2cea2b 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 @@ -371,7 +371,7 @@ private List createSettings(AppConfigurationRepl propertySource = new AppConfigurationApplicationSettingPropertySource( selectedKeys.getKeyFilter() + resource.getEndpoint() + "/", client, keyVaultClientFactory, selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles), - selectedKeys.getTagsFilter()); + selectedKeys.getTagsFilter(), featureFlagClient); } propertySource.initProperties(resource.getTrimKeyPrefix(), requestContext); sourceList.add(propertySource); 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..92b86c73b21a 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 @@ -94,6 +94,9 @@ public class AppConfigurationApplicationSettingPropertySourceTest { @Mock private Context contextMock; + @Mock + private FeatureFlagClient featureFlagClientMock; + private MockitoSession session; @BeforeAll @@ -117,7 +120,7 @@ public void init() { String[] labelFilter = { "\0" }; propertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock, - keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null); + keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null, featureFlagClientMock); } @AfterEach @@ -204,7 +207,7 @@ public void initPropertiesWithTagsFilterTest() throws IOException { List tagsFilter = Arrays.asList("env=prod", "team=backend"); AppConfigurationApplicationSettingPropertySource taggedPropertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock, - keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter); + keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter, featureFlagClientMock); when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems); @@ -226,7 +229,7 @@ public void initPropertiesWithNullTagsFilterTest() throws IOException { String[] labelFilter = { "\0" }; AppConfigurationApplicationSettingPropertySource untaggedPropertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock, - keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null); + keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null, featureFlagClientMock); when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems); @@ -248,7 +251,7 @@ public void initPropertiesWithEmptyTagsFilterTest() throws IOException { List tagsFilter = new ArrayList<>(); AppConfigurationApplicationSettingPropertySource emptyTagPropertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock, - keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter); + keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter, featureFlagClientMock); when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems); @@ -270,7 +273,7 @@ public void initPropertiesWithTagsFilterMultipleLabelsTest() throws IOException List tagsFilter = Arrays.asList("env=staging"); AppConfigurationApplicationSettingPropertySource multiLabelPropertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock, - keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter); + keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter, featureFlagClientMock); when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems); 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..f47b703ad868 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 @@ -74,6 +74,9 @@ public class AppConfigurationPropertySourceKeyVaultTest { @Mock private Context contextMock; + + @Mock + private FeatureFlagClient featureFlagClientMock; private MockitoSession session; @@ -88,7 +91,7 @@ public void init() { String[] labelFilter = { "\0" }; propertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, replicaClientMock, - keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null); + keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null, featureFlagClientMock); } @AfterEach From c1a9d28a17b72028ff1a90ffe291f03b9405ad20 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 29 Apr 2026 11:12:17 -0700 Subject: [PATCH 2/5] New Test + Change Log --- .../CHANGELOG.md | 2 + ...nApplicationSettingPropertySourceTest.java | 119 ++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md index 1d0ea68a0e0a..e4a92c1ef229 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added support for Snapshot References in App Configuration stores. Snapshot reference settings are now automatically resolved, loading the referenced snapshot's configuration settings as properties. + ### Breaking Changes ### Bugs Fixed 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 92b86c73b21a..cc8359f4ac23 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 @@ -289,4 +289,123 @@ public void initPropertiesWithTagsFilterMultipleLabelsTest() throws IOException assertThat(capturedSelector.getTagsFilter()).containsExactly("env=staging"); } } + + @Test + public void snapshotReferenceIsResolved() throws IOException { + // Create a snapshot reference setting + String snapshotRefContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8"; + ConfigurationSetting snapshotRef = new ConfigurationSetting() + .setKey("snapshot-ref-key") + .setValue("my-snapshot") + .setContentType(snapshotRefContentType); + + List settingsWithRef = new ArrayList<>(); + settingsWithRef.add(snapshotRef); + + // The snapshot contains regular settings + List snapshotSettings = new ArrayList<>(); + snapshotSettings.add(createItem(KEY_FILTER, TEST_KEY_1, TEST_VALUE_1, TEST_LABEL_1, EMPTY_CONTENT_TYPE)); + snapshotSettings.add(createItem(KEY_FILTER, TEST_KEY_2, TEST_VALUE_2, TEST_LABEL_2, EMPTY_CONTENT_TYPE)); + + when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(settingsWithRef); + when(clientMock.listSettingSnapshot(Mockito.eq("my-snapshot"), Mockito.any(Context.class))) + .thenReturn(snapshotSettings); + + propertySource.initProperties(null, contextMock); + + String[] keyNames = propertySource.getPropertyNames(); + assertThat(keyNames).hasSize(2); + assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1); + assertThat(propertySource.getProperty(TEST_KEY_2)).isEqualTo(TEST_VALUE_2); + } + + @Test + public void snapshotReferenceWithMixedSettings() throws IOException { + // Mix of snapshot references and regular settings + String snapshotRefContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8"; + ConfigurationSetting snapshotRef = new ConfigurationSetting() + .setKey("snapshot-ref-key") + .setValue("my-snapshot") + .setContentType(snapshotRefContentType); + ConfigurationSetting regularSetting = createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3, + EMPTY_CONTENT_TYPE); + + List mixedSettings = new ArrayList<>(); + mixedSettings.add(snapshotRef); + mixedSettings.add(regularSetting); + + List snapshotSettings = new ArrayList<>(); + snapshotSettings.add(createItem(KEY_FILTER, TEST_KEY_1, TEST_VALUE_1, TEST_LABEL_1, EMPTY_CONTENT_TYPE)); + + when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(mixedSettings); + when(clientMock.listSettingSnapshot(Mockito.eq("my-snapshot"), Mockito.any(Context.class))) + .thenReturn(snapshotSettings); + + propertySource.initProperties(null, contextMock); + + String[] keyNames = propertySource.getPropertyNames(); + assertThat(keyNames).hasSize(2); + assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1); + assertThat(propertySource.getProperty(TEST_KEY_3)).isEqualTo(TEST_VALUE_3); + } + + @Test + public void featureFlagsAreStrippedFromNonSnapshotSettings() throws IOException { + // Feature flags outside snapshots should be ignored/stripped + List settingsWithFeatureFlag = new ArrayList<>(); + settingsWithFeatureFlag.add(ITEM_1); + settingsWithFeatureFlag.add(FEATURE_FLAG); + + when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(settingsWithFeatureFlag); + + propertySource.initProperties(null, contextMock); + + // Only the regular setting should be loaded as a property + String[] keyNames = propertySource.getPropertyNames(); + assertThat(keyNames).hasSize(1); + assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1); + } + + @Test + public void featureFlagsFromSnapshotAreCollected() throws IOException { + // Feature flags from snapshot references should be collected + String snapshotRefContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8"; + ConfigurationSetting snapshotRef = new ConfigurationSetting() + .setKey("snapshot-ref-key") + .setValue("my-snapshot") + .setContentType(snapshotRefContentType); + + List settings = new ArrayList<>(); + settings.add(snapshotRef); + + FeatureFlagConfigurationSetting featureFlag = createItemFeatureFlag("Beta", "/0"); + List snapshotSettings = new ArrayList<>(); + snapshotSettings.add(createItem(KEY_FILTER, TEST_KEY_1, TEST_VALUE_1, TEST_LABEL_1, EMPTY_CONTENT_TYPE)); + snapshotSettings.add(featureFlag); + + when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(settings); + when(clientMock.listSettingSnapshot(Mockito.eq("my-snapshot"), Mockito.any(Context.class))) + .thenReturn(snapshotSettings); + + propertySource.initProperties(null, contextMock); + + // Feature flag should be in the featureFlagsList + assertThat(propertySource.featureFlagsList).hasSize(1); + assertThat(propertySource.featureFlagsList.get(0)).isInstanceOf(FeatureFlagConfigurationSetting.class); + + // Regular setting should be loaded + assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1); + + // featureFlagClient should have been called + Mockito.verify(featureFlagClientMock).proccessFeatureFlags(Mockito.any(), Mockito.any()); + } + + @Test + public void featureFlagClientIsCalledOnInit() throws IOException { + when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems); + + propertySource.initProperties(null, contextMock); + + Mockito.verify(featureFlagClientMock).proccessFeatureFlags(Mockito.any(), Mockito.any()); + } } From 78990d5aa8b43ccd2188781f26791edeff5bd56c Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 8 Jun 2026 11:52:09 -0700 Subject: [PATCH 3/5] Adding telemetry back --- ...gurationApplicationSettingPropertySource.java | 3 ++- .../AppConfigurationConstants.java | 5 +++++ .../implementation/http/policy/TracingInfo.java | 16 ++++++++++++++++ ...tionApplicationSettingPropertySourceTest.java | 4 ++-- .../http/policy/TracingInfoTest.java | 15 +++++++++++++-- 5 files changed, 38 insertions(+), 5 deletions(-) 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 d2ba2b624ea2..359911fff209 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 @@ -125,7 +125,7 @@ protected void processConfigurationSettings(List settings, } WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, featureFlagsList); - featureFlagClient.proccessFeatureFlags(featureFlags, replicaClient.getEndpoint()); + featureFlagClient.processFeatureFlags(featureFlags, replicaClient.getEndpoint()); } private List resolveSnapshotReferences(List settings) { @@ -133,6 +133,7 @@ private List resolveSnapshotReferences(List snapshotSettings = replicaClient.listSettingSnapshot(setting.getValue(), Context.NONE); resolvedSettings.addAll(snapshotSettings); 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 0b8869814b4d..89304c594c8d 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 @@ -93,4 +93,9 @@ public class AppConfigurationConstants { * Constant for tracing AI Chat Completion configuration usage. */ public static final String AI_CHAT_COMPLETION_FEATURE = "AICC"; + + /** + * Constant for tracing snapshot reference usage. + */ + public static final String SNAPSHOT_REFERENCE_TAG = "SnapshotRef"; } 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 e4eb53addcf8..94533bf73ea6 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 @@ -12,6 +12,7 @@ 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; @@ -35,6 +36,8 @@ public class TracingInfo { private boolean usesAiccConfiguration = false; + private boolean usesSnapshotReference = false; + private boolean isFailoverRequest = false; public TracingInfo(boolean isKeyVaultConfigured, int replicaCount, Configuration configuration) { @@ -58,6 +61,13 @@ public void setFailoverRequest() { this.isFailoverRequest = true; } + /** + * Marks snapshot references as used. + */ + public void setUsesSnapshotReference() { + this.usesSnapshotReference = true; + } + /** * Resets AI configuration tracing flags. */ @@ -199,6 +209,12 @@ private String createFeaturesString() { } sb.append(AI_CHAT_COMPLETION_FEATURE); } + if (usesSnapshotReference) { + if (sb.length() > 0) { + sb.append(DELIMITER); + } + sb.append(SNAPSHOT_REFERENCE_TAG); + } return sb.toString(); } 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 1b7f1c4bcd48..19778edfda1d 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 @@ -402,7 +402,7 @@ public void featureFlagsFromSnapshotAreCollected() throws IOException { assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1); // featureFlagClient should have been called - Mockito.verify(featureFlagClientMock).proccessFeatureFlags(Mockito.any(), Mockito.any()); + Mockito.verify(featureFlagClientMock).processFeatureFlags(Mockito.any(), Mockito.any()); } @Test @@ -411,6 +411,6 @@ public void featureFlagClientIsCalledOnInit() throws IOException { propertySource.initProperties(null, contextMock); - Mockito.verify(featureFlagClientMock).proccessFeatureFlags(Mockito.any(), Mockito.any()); + Mockito.verify(featureFlagClientMock).processFeatureFlags(Mockito.any(), Mockito.any()); } } 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 1a3d85c6afe7..a1f2257f3164 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 @@ -72,6 +72,15 @@ public void loadBalancingTracingTest() { assertTrue(value.contains("Features=LB")); } + @Test + public void snapshotReferenceTracingTest() { + Configuration configuration = getConfiguration("false"); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); + tracingInfo.setUsesSnapshotReference(); + String value = tracingInfo.getValue(false, false, null); + assertTrue(value.contains("Features=SnapshotRef")); + } + @Test public void aiConfigurationTracingTest() { Configuration configuration = getConfiguration("false"); @@ -144,8 +153,9 @@ public void multipleFeaturesTracingTest() { TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); tracingInfo.setUsesLoadBalancing(); tracingInfo.updateAiConfigurationTracing("application/json; profile=\"https://azconfig.io/mime-profiles/ai\""); + tracingInfo.setUsesSnapshotReference(); String value = tracingInfo.getValue(false, false, null); - assertTrue(value.contains("Features=LB+AI")); + assertTrue(value.contains("Features=LB+AI+SnapshotRef")); } @Test @@ -183,6 +193,7 @@ public void fullCorrelationContextTest() { Configuration configuration = getConfiguration("false"); TracingInfo tracingInfo = new TracingInfo(true, 2, configuration); tracingInfo.setUsesLoadBalancing(); + tracingInfo.setUsesSnapshotReference(); tracingInfo.setFailoverRequest(); FeatureFlagTracing ffTracing = new FeatureFlagTracing(); @@ -198,7 +209,7 @@ public void fullCorrelationContextTest() { assertTrue(value.contains("Filter=TRGT")); assertTrue(value.contains("MaxVariants=3")); assertTrue(value.contains("FFFeatures=Telemetry")); - assertTrue(value.contains("Features=LB")); + assertTrue(value.contains("Features=LB+SnapshotRef")); assertTrue(value.contains("UsesKeyVault")); assertTrue(value.contains("PushRefresh")); assertTrue(value.contains("Failover")); From 94a9f6bd3bd8f89734dc1f364cac40700db5b585 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 8 Jun 2026 15:24:20 -0700 Subject: [PATCH 4/5] Update AppConfigurationApplicationSettingPropertySource.java --- .../AppConfigurationApplicationSettingPropertySource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 359911fff209..a7859e17c781 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 @@ -22,8 +22,8 @@ import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.security.keyvault.secrets.models.KeyVaultSecret; -import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings; import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; +import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings; /** * Azure App Configuration PropertySource unique per Store Label(Profile) combo. @@ -47,7 +47,7 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP protected List featureFlagsList = new ArrayList<>(); - private final String SNAPSHOT_REF_CONTENT_TYPE = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8"; + private static final String SNAPSHOT_REF_CONTENT_TYPE = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8"; protected final FeatureFlagClient featureFlagClient; From beed55c92ff84dfbcf0bed67491a562ea8d2b117 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 9 Jun 2026 13:00:03 -0700 Subject: [PATCH 5/5] Review comments --- ...nfigurationApplicationSettingPropertySource.java | 13 ++++++++----- .../AppConfigurationSnapshotPropertySource.java | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) 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 a7859e17c781..0a33e68af2d4 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 @@ -93,15 +93,18 @@ public void initProperties(List keyPrefixTrimValues, Context context) // * for wildcard match processConfigurationSettings(replicaClient.listSettings(settingSelector, context), settingSelector.getKeyFilter(), - keyPrefixTrimValues); + keyPrefixTrimValues, context); } } protected void processConfigurationSettings(List settings, String keyFilter, - List keyPrefixTrimValues) + List keyPrefixTrimValues, Context context) throws InvalidConfigurationPropertyValueException { + // Reset per-label state so flags from a previous label aren't re-processed. + featureFlagsList.clear(); + // First resolve snapshot references - settings = resolveSnapshotReferences(settings); + settings = resolveSnapshotReferences(settings, context); for (ConfigurationSetting setting : settings) { replicaClient.getTracingInfo().updateAiConfigurationTracing(setting.getContentType()); @@ -128,14 +131,14 @@ protected void processConfigurationSettings(List settings, featureFlagClient.processFeatureFlags(featureFlags, replicaClient.getEndpoint()); } - private List resolveSnapshotReferences(List settings) { + private List resolveSnapshotReferences(List settings, Context context) { List resolvedSettings = new ArrayList<>(); for (ConfigurationSetting setting : settings) { if (SNAPSHOT_REF_CONTENT_TYPE.equals(setting.getContentType())) { // Handle snapshot reference replicaClient.getTracingInfo().setUsesSnapshotReference(); List snapshotSettings = replicaClient.listSettingSnapshot(setting.getValue(), - Context.NONE); + context); resolvedSettings.addAll(snapshotSettings); } else if (setting instanceof FeatureFlagConfigurationSetting) { // We need to strip feature flags as we only support feature flags from snapshots, and if they are in a 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 2f4262524b61..2465fd8dfe72 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 @@ -40,9 +40,10 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli *

* * @param trim prefix to trim - * @param isRefresh true if a refresh triggered the loading of the Snapshot. + * @param context request context propagated to the App Configuration client. * @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type */ + @Override public void initProperties(List trim, Context context) throws InvalidConfigurationPropertyValueException { replicaClient.getTracingInfo().resetAiConfigurationTracing(); List settings = replicaClient.listSettingSnapshot(snapshotName, context);