Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature flag telemetry support #94

Merged
merged 1 commit into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions internal/loader/configuraiton_setting_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ func mockFeatureFlagSettings() []azappconfig.Setting {
return settingsToReturn
}

func mockVariantFeatureFlagSettings(key, label string, telemetryEnabled bool) []azappconfig.Setting {
settingsToReturn := make([]azappconfig.Setting, 1)
settingsToReturn[0] = newFeatureFlagVariant(key, label, telemetryEnabled)

return settingsToReturn
}

func newKeyValueSelector(key string, label *string) acpv1.Selector {
return acpv1.Selector{
KeyFilter: &key,
Expand Down Expand Up @@ -133,6 +140,49 @@ func newFeatureFlagSettings(key string, label string) azappconfig.Setting {
}
}

func newFeatureFlagVariant(key string, label string, telemetryEnabled bool) azappconfig.Setting {
featureFlagContentType := FeatureFlagContentType
fakeETag := azcore.ETag("fakeETag")
featureFlagId := strings.TrimPrefix(key, ".appconfig.featureflag/")
featureFlagValue := fmt.Sprintf(`{
"id": "%s",
"description": "",
"enabled": false,
"variants": [
{
"name": "Off",
"configuration_value": false
},
{
"name": "On",
"configuration_value": true
}
],
"allocation": {
"percentile": [
{
"variant": "Off",
"from": 0,
"to": 100
}
],
"default_when_enabled": "Off",
"default_when_disabled": "Off"
},
"telemetry": {
"enabled": %t
}
}`, featureFlagId, telemetryEnabled)

return azappconfig.Setting{
Key: &key,
Value: &featureFlagValue,
Label: &label,
ContentType: &featureFlagContentType,
ETag: &fakeETag,
}
}

type MockResolveSecretReference struct {
ctrl *gomock.Controller
recorder *MockResolveSecretReferenceMockRecorder
Expand Down Expand Up @@ -1074,6 +1124,108 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() {
Expect(allSettings.ConfigMapSettings["settings.json"]).Should(Equal("{\"feature_management\":{\"feature_flags\":[{\"conditions\":{\"client_filters\":[]},\"description\":\"\",\"enabled\":false,\"id\":\"Beta\"}]}}"))
})

It("Succeed to get feature flag settings", func() {
By("By updating telemetry when telemetry is enabled")
featureFlagKeyFilter := "*"
testSpec := acpv1.AzureAppConfigurationProviderSpec{
Endpoint: &EndpointName,
ReplicaDiscoveryEnabled: false,
Target: acpv1.ConfigurationGenerationParameters{
ConfigMapName: ConfigMapName,
ConfigMapData: &acpv1.ConfigMapDataOptions{
Type: acpv1.Json,
Key: "settings.json",
},
},
FeatureFlag: &acpv1.AzureAppConfigurationFeatureFlagOptions{
Selectors: []acpv1.Selector{
{
KeyFilter: &featureFlagKeyFilter,
},
},
},
}
testProvider := acpv1.AzureAppConfigurationProvider{
TypeMeta: metav1.TypeMeta{
APIVersion: "azconfig.io/v1",
Kind: "AppConfigurationProvider",
},
ObjectMeta: metav1.ObjectMeta{
Name: "testName",
Namespace: "testNamespace",
},
Spec: testSpec,
}

featureFlagsToReturn := mockVariantFeatureFlagSettings(".appconfig.featureflag/Telemetry_2", "Test", true)
featureFlagEtags := make(map[acpv1.Selector][]*azcore.ETag)
featureFlagEtags[newFeatureFlagSelector("*", nil)] = []*azcore.ETag{}
settingsResponse := &SettingsResponse{
Settings: featureFlagsToReturn,
Etags: featureFlagEtags,
}
mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(2)
mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(2)
configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient)
allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference)

Expect(err).Should(BeNil())
Expect(len(allSettings.ConfigMapSettings)).Should(Equal(1))
Expect(allSettings.ConfigMapSettings["settings.json"]).Should(
Equal("{\"feature_management\":{\"feature_flags\":[{\"allocation\":{\"default_when_disabled\":\"Off\",\"default_when_enabled\":\"Off\",\"percentile\":[{\"from\":0,\"to\":100,\"variant\":\"Off\"}]},\"description\":\"\",\"enabled\":false,\"id\":\"Telemetry_2\",\"telemetry\":{\"enabled\":true,\"metadata\":{\"ETag\":\"fakeETag\",\"FeatureFlagId\":\"Rc8Am7HIGDT7HC5Ovs3wKN_aGaaK_Uz1mH2e11gaK0o\",\"FeatureFlagReference\":\"/kv/.appconfig.featureflag/Telemetry_2?label=Test\"}},\"variants\":[{\"configuration_value\":false,\"name\":\"Off\"},{\"configuration_value\":true,\"name\":\"On\"}]}]}}"))
})

It("Succeed to get feature flag settings", func() {
By("By not populating telemetry when telemetry is disabled")
featureFlagKeyFilter := "*"
testSpec := acpv1.AzureAppConfigurationProviderSpec{
Endpoint: &EndpointName,
ReplicaDiscoveryEnabled: false,
Target: acpv1.ConfigurationGenerationParameters{
ConfigMapName: ConfigMapName,
ConfigMapData: &acpv1.ConfigMapDataOptions{
Type: acpv1.Json,
Key: "settings.json",
},
},
FeatureFlag: &acpv1.AzureAppConfigurationFeatureFlagOptions{
Selectors: []acpv1.Selector{
{
KeyFilter: &featureFlagKeyFilter,
},
},
},
}
testProvider := acpv1.AzureAppConfigurationProvider{
TypeMeta: metav1.TypeMeta{
APIVersion: "azconfig.io/v1",
Kind: "AppConfigurationProvider",
},
ObjectMeta: metav1.ObjectMeta{
Name: "testName",
Namespace: "testNamespace",
},
Spec: testSpec,
}

featureFlagsToReturn := mockVariantFeatureFlagSettings(".appconfig.featureflag/Telemetry_2", "Test", false)
featureFlagEtags := make(map[acpv1.Selector][]*azcore.ETag)
featureFlagEtags[newFeatureFlagSelector("*", nil)] = []*azcore.ETag{}
settingsResponse := &SettingsResponse{
Settings: featureFlagsToReturn,
Etags: featureFlagEtags,
}
mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(2)
mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(2)
configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient)
allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference)

Expect(err).Should(BeNil())
Expect(len(allSettings.ConfigMapSettings)).Should(Equal(1))
Expect(allSettings.ConfigMapSettings["settings.json"]).Should(
Equal("{\"feature_management\":{\"feature_flags\":[{\"allocation\":{\"default_when_disabled\":\"Off\",\"default_when_enabled\":\"Off\",\"percentile\":[{\"from\":0,\"to\":100,\"variant\":\"Off\"}]},\"description\":\"\",\"enabled\":false,\"id\":\"Telemetry_2\",\"telemetry\":{\"enabled\":false},\"variants\":[{\"configuration_value\":false,\"name\":\"Off\"},{\"configuration_value\":true,\"name\":\"On\"}]}]}}"))
})

It("Fail to get all configuration settings", func() {
By("By getting error from Azure App Configuration")
testSpec := acpv1.AzureAppConfigurationProviderSpec{
Expand Down Expand Up @@ -1397,6 +1549,16 @@ func TestGetFilters(t *testing.T) {
assert.Equal(t, "snapshot", *filters10[1].SnapshotName)
}

func TestFeatureFlagId(t *testing.T) {
telemetrySetting1 := newFeatureFlagVariant(".appconfig.featureflag/Telemetry_1", "", true)
calculatedId1 := calculateFeatureFlagId(telemetrySetting1)
assert.Equal(t, "krkOsu9dVV9huwbQDPR6gkV_2T0buWxOCS-nNsj5-6g", calculatedId1)

telemetrySetting2 := newFeatureFlagVariant(".appconfig.featureflag/Telemetry_2", "Test", true)
calculatedId2 := calculateFeatureFlagId(telemetrySetting2)
assert.Equal(t, "Rc8Am7HIGDT7HC5Ovs3wKN_aGaaK_Uz1mH2e11gaK0o", calculatedId2)
}

func TestCompare(t *testing.T) {
var nilString *string = nil
stringA := "stringA"
Expand Down
70 changes: 69 additions & 1 deletion internal/loader/configuration_setting_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package loader
import (
acpv1 "azappconfig/provider/api/v1"
"context"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
Expand All @@ -19,6 +20,7 @@ import (

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
"golang.org/x/crypto/pkcs12"
"golang.org/x/exp/maps"
Expand Down Expand Up @@ -99,6 +101,16 @@ const (
RequestTracingEnabled string = "REQUEST_TRACING_ENABLED"
)

// Feature flag telemetry
const (
TelemetryKey string = "telemetry"
EnabledKey string = "enabled"
MetadataKey string = "metadata"
ETagKey string = "ETag"
FeatureFlagIdKey string = "FeatureFlagId"
FeatureFlagReferenceKey string = "FeatureFlagReference"
)

func NewConfigurationSettingLoader(provider acpv1.AzureAppConfigurationProvider, clientManager ClientManager, settingsClient SettingsClient) (*ConfigurationSettingLoader, error) {
return &ConfigurationSettingLoader{
AzureAppConfigurationProvider: provider,
Expand Down Expand Up @@ -372,6 +384,10 @@ func (csl *ConfigurationSettingLoader) getFeatureFlagSettings(ctx context.Contex
settingsLength := len(settingsResponse.Settings)
featureFlagExist := make(map[string]bool, settingsLength)
deduplicatedFeatureFlags := make([]interface{}, 0)
clientEndpoint := ""
if manager, ok := csl.ClientManager.(*ConfigurationClientManager); ok {
clientEndpoint = manager.lastSuccessfulEndpoint
}

// if settings returned like this: [{"id": "Beta"...}, {"id": "Alpha"...}, {"id": "Beta"...}], we need to deduplicate it to [{"id": "Alpha"...}, {"id": "Beta"...}], the last one wins
for i := settingsLength - 1; i >= 0; i-- {
Expand All @@ -380,11 +396,12 @@ func (csl *ConfigurationSettingLoader) getFeatureFlagSettings(ctx context.Contex
continue
}
featureFlagExist[key] = true
var out interface{}
var out map[string]interface{}
err := json.Unmarshal([]byte(*settingsResponse.Settings[i].Value), &out)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal feature flag settings: %s", err.Error())
}
populateTelemetryMetadata(out, settingsResponse.Settings[i], clientEndpoint)
deduplicatedFeatureFlags = append(deduplicatedFeatureFlags, out)
}

Expand Down Expand Up @@ -899,3 +916,54 @@ func reverseClients(clients []*ConfigurationClientWrapper, start, end int) {
end--
}
}

func calculateFeatureFlagId(setting azappconfig.Setting) string {
// Create the basic value string
featureFlagId := *setting.Key + "\n"
if setting.Label != nil && strings.TrimSpace(*setting.Label) != "" {
featureFlagId += *setting.Label
}

// Generate SHA-256 hash, and encode it to Base64
hash := sha256.Sum256([]byte(featureFlagId))
encodedFeatureFlag := base64.StdEncoding.EncodeToString(hash[:])

// Replace '+' with '-' and '/' with '_'
encodedFeatureFlag = strings.ReplaceAll(encodedFeatureFlag, "+", "-")
encodedFeatureFlag = strings.ReplaceAll(encodedFeatureFlag, "/", "_")

// Remove all instances of "=" at the end of the string that were added as padding
if idx := strings.Index(encodedFeatureFlag, "="); idx != -1 {
encodedFeatureFlag = encodedFeatureFlag[:idx]
}

return encodedFeatureFlag
}

func generateFeatureFlagReference(setting azappconfig.Setting, endpoint string) string {
featureFlagReference := fmt.Sprintf("%s/kv/%s", endpoint, *setting.Key)

// Check if the label is present and not empty
if setting.Label != nil && strings.TrimSpace(*setting.Label) != "" {
featureFlagReference += fmt.Sprintf("?label=%s", *setting.Label)
}

return featureFlagReference
}

func populateTelemetryMetadata(featureFlag map[string]interface{}, setting azappconfig.Setting, endpoint string) {
if telemetry, ok := featureFlag[TelemetryKey].(map[string]interface{}); ok {
if enabled, ok := telemetry[EnabledKey].(bool); ok && enabled {
metadata, _ := telemetry[MetadataKey].(map[string]interface{})
if metadata == nil {
metadata = make(map[string]interface{})
}

// Set the new metadata
metadata[ETagKey] = *setting.ETag
metadata[FeatureFlagIdKey] = calculateFeatureFlagId(setting)
metadata[FeatureFlagReferenceKey] = generateFeatureFlagReference(setting, endpoint)
telemetry[MetadataKey] = metadata
}
}
}