Skip to content

Commit

Permalink
feature flag telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
linglingye001 committed Oct 21, 2024
1 parent 99cbeee commit 7efeeae
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 3 deletions.
101 changes: 101 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() []azappconfig.Setting {
settingsToReturn := make([]azappconfig.Setting, 1)
settingsToReturn[0] = newFeatureFlagVariant(".appconfig.featureflag/Telemetry_2", "Test", true)

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,57 @@ 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 with feature flag variants")
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()
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("Fail to get all configuration settings", func() {
By("By getting error from Azure App Configuration")
testSpec := acpv1.AzureAppConfigurationProviderSpec{
Expand Down
71 changes: 68 additions & 3 deletions 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 Down Expand Up @@ -91,6 +92,12 @@ const (
FeatureFlagKeyPrefix string = ".appconfig.featureflag/"
FeatureFlagSectionName string = "feature_flags"
FeatureManagementSectionName string = "feature_management"
FeatureFlagTelemetryKey string = "telemetry"
FeatureFlagEnabledKey string = "enabled"
FeatureFlagMetadataKey string = "metadata"
FeatureFlagETagKey string = "ETag"
FeatureFlagIdKey string = "FeatureFlagId"
FeatureFlagReferenceKey string = "FeatureFlagReference"
PreservedSecretTypeTag string = ".kubernetes.secret.type"
CertTypePem string = "application/x-pem-file"
CertTypePfx string = "application/x-pkcs12"
Expand Down Expand Up @@ -371,13 +378,18 @@ func (csl *ConfigurationSettingLoader) getFeatureFlagSettings(ctx context.Contex
var featureFlagSection = map[string]interface{}{
FeatureFlagSectionName: make([]interface{}, 0),
}
clientEndpoint := ""
if manager, ok := csl.ClientManager.(*ConfigurationClientManager); ok {
clientEndpoint = manager.lastSuccessfulEndpoint
}
for _, setting := range deduplicateFeatureFlags {
var out interface{}
err := json.Unmarshal([]byte(*setting.Value), &out)
var featureFlag map[string]interface{}
err := json.Unmarshal([]byte(*setting.Value), &featureFlag)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal feature flag settings: %s", err.Error())
}
featureFlagSection[FeatureFlagSectionName] = append(featureFlagSection[FeatureFlagSectionName].([]interface{}), out)
updateFeatureFlagTelemetry(featureFlag, setting, clientEndpoint)
featureFlagSection[FeatureFlagSectionName] = append(featureFlagSection[FeatureFlagSectionName].([]interface{}), featureFlag)
}

return featureFlagSection, settingsResponse.Etags, nil
Expand Down Expand Up @@ -886,3 +898,56 @@ func reverseClients(clients []*ConfigurationClientWrapper, start, end int) {
end--
}
}

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

// Generate SHA-256 hash, and encode it to Base64
hash := sha256.Sum256([]byte(featureFlagValue))
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 createFeatureFlagReference(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) != "" {
// Encode the label to ensure it is safe for URLs
encodedLabel := url.QueryEscape(*setting.Label)
featureFlagReference += fmt.Sprintf("?label=%s", encodedLabel)
}

return featureFlagReference
}

func updateFeatureFlagTelemetry(featureFlag map[string]interface{}, setting azappconfig.Setting, endpoint string) {
if telemetry, ok := featureFlag[FeatureFlagTelemetryKey].(map[string]interface{}); ok {
if enabled, ok := telemetry[FeatureFlagEnabledKey].(bool); ok && enabled {
metadata, _ := telemetry[FeatureFlagMetadataKey].(map[string]interface{})
if metadata == nil {
metadata = make(map[string]interface{})
}

// Set the new metadata
metadata[FeatureFlagETagKey] = *setting.ETag
metadata[FeatureFlagIdKey] = calculateFeatureFlagId(setting)
metadata[FeatureFlagReferenceKey] = createFeatureFlagReference(setting, endpoint)
telemetry[FeatureFlagMetadataKey] = metadata
}
}
}

0 comments on commit 7efeeae

Please sign in to comment.