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);