Compare commits

...

1 Commits

Author SHA1 Message Date
Tania B.
7dafd900bc Add support for other flag types than bool to static provider 2025-10-24 13:24:10 +02:00
5 changed files with 242 additions and 43 deletions

View File

@@ -25,8 +25,8 @@ type OpenFeatureConfig struct {
URL *url.URL
// HTTPClient is a pre-configured HTTP client (optional, used for GOFF provider)
HTTPClient *http.Client
// StaticFlags are the feature flags to use with static provider
StaticFlags map[string]bool
// TypedFlags are the feature flags to use with static provider
StaticFlags map[string]setting.TypedFeatureFlag
// TargetingKey is used for evaluation context
TargetingKey string
// ContextAttrs are additional attributes for evaluation context
@@ -60,7 +60,8 @@ func InitOpenFeature(config OpenFeatureConfig) error {
// InitOpenFeatureWithCfg initializes OpenFeature from setting.Cfg
func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
confFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
// Read typed flags from config
confFlags, err := setting.ReadTypedFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
if err != nil {
return fmt.Errorf("failed to read feature flags from config: %w", err)
}
@@ -96,7 +97,7 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
func createProvider(
providerType string,
u *url.URL,
staticFlags map[string]bool,
staticFlags map[string]setting.TypedFeatureFlag,
httpClient *http.Client,
) (openfeature.FeatureProvider, error) {
if providerType != setting.GOFFProviderType {

View File

@@ -24,12 +24,12 @@ func CreateStaticEvaluator(cfg *setting.Cfg) (StaticFlagEvaluator, error) {
return nil, fmt.Errorf("provider is not a static provider, type %s", setting.StaticProviderType)
}
staticFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
typedFlags, err := setting.ReadTypedFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
if err != nil {
return nil, fmt.Errorf("failed to read feature flags from config: %w", err)
}
staticProvider, err := newStaticProvider(staticFlags)
staticProvider, err := newStaticProvider(typedFlags)
if err != nil {
return nil, fmt.Errorf("failed to create static provider: %w", err)
}
@@ -56,19 +56,7 @@ type staticEvaluator struct {
}
func (s *staticEvaluator) EvalFlag(ctx context.Context, flagKey string) (goffmodel.OFREPEvaluateSuccessResponse, error) {
result, err := s.client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx))
if err != nil {
return goffmodel.OFREPEvaluateSuccessResponse{}, fmt.Errorf("failed to evaluate flag %s: %w", flagKey, err)
}
resp := goffmodel.OFREPEvaluateSuccessResponse{
Key: flagKey,
Value: result.Value,
Reason: "static provider evaluation result",
Variant: result.Variant,
Metadata: result.FlagMetadata,
}
return resp, nil
return s.evaluateFlagWithTypeDetection(ctx, flagKey)
}
func (s *staticEvaluator) EvalAllFlags(ctx context.Context) (goffmodel.OFREPBulkEvaluateSuccessResponse, error) {
@@ -79,24 +67,70 @@ func (s *staticEvaluator) EvalAllFlags(ctx context.Context) (goffmodel.OFREPBulk
allFlags := make([]goffmodel.OFREPFlagBulkEvaluateSuccessResponse, 0, len(flags))
for _, flagKey := range flags {
result, err := s.client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx))
result, err := s.evaluateFlagWithTypeDetection(ctx, flagKey)
if err != nil {
s.log.Error("failed to evaluate flag during bulk evaluation", "flagKey", flagKey, "error", err)
continue
}
allFlags = append(allFlags, goffmodel.OFREPFlagBulkEvaluateSuccessResponse{
OFREPEvaluateSuccessResponse: goffmodel.OFREPEvaluateSuccessResponse{
Key: flagKey,
Value: result.Value,
Reason: "static provider evaluation result",
Variant: result.Variant,
Metadata: result.FlagMetadata,
},
ErrorCode: string(result.ErrorCode),
ErrorDetails: result.ErrorMessage,
OFREPEvaluateSuccessResponse: result,
})
}
return goffmodel.OFREPBulkEvaluateSuccessResponse{Flags: allFlags}, nil
}
// evaluateFlagWithTypeDetection tries different flag types and returns the first successful evaluation result
func (s *staticEvaluator) evaluateFlagWithTypeDetection(ctx context.Context, flagKey string) (goffmodel.OFREPEvaluateSuccessResponse, error) {
// Try boolean evaluation first for backward compatibility
result, err := s.client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx))
if err == nil {
return goffmodel.OFREPEvaluateSuccessResponse{
Key: flagKey,
Value: result.Value,
Reason: "static provider evaluation result",
Variant: result.Variant,
Metadata: result.FlagMetadata,
}, nil
}
// If boolean evaluation fails, try other types
s.log.Debug("boolean evaluation failed, trying other types", "flagKey", flagKey, "error", err)
// Try string evaluation
if stringResult, stringErr := s.client.StringValueDetails(ctx, flagKey, "", openfeature.TransactionContext(ctx)); stringErr == nil {
return goffmodel.OFREPEvaluateSuccessResponse{
Key: flagKey,
Value: stringResult.Value,
Reason: "static provider evaluation result",
Variant: stringResult.Variant,
Metadata: stringResult.FlagMetadata,
}, nil
}
// Try number evaluation
if numberResult, numberErr := s.client.FloatValueDetails(ctx, flagKey, 0.0, openfeature.TransactionContext(ctx)); numberErr == nil {
return goffmodel.OFREPEvaluateSuccessResponse{
Key: flagKey,
Value: numberResult.Value,
Reason: "static provider evaluation result",
Variant: numberResult.Variant,
Metadata: numberResult.FlagMetadata,
}, nil
}
// Try object evaluation
if objectResult, objectErr := s.client.ObjectValueDetails(ctx, flagKey, map[string]interface{}{}, openfeature.TransactionContext(ctx)); objectErr == nil {
return goffmodel.OFREPEvaluateSuccessResponse{
Key: flagKey,
Value: objectResult.Value,
Reason: "static provider evaluation result",
Variant: objectResult.Variant,
Metadata: objectResult.FlagMetadata,
}, nil
}
// If all evaluations fail, return the original boolean error
return goffmodel.OFREPEvaluateSuccessResponse{}, fmt.Errorf("failed to evaluate flag %s: %w", flagKey, err)
}

View File

@@ -1,8 +1,13 @@
package featuremgmt
import (
"encoding/json"
"strconv"
"github.com/open-feature/go-sdk/openfeature"
"github.com/open-feature/go-sdk/openfeature/memprovider"
"github.com/grafana/grafana/pkg/setting"
)
// inMemoryBulkProvider is a wrapper around memprovider.InMemoryProvider that
@@ -28,28 +33,59 @@ func (p *inMemoryBulkProvider) ListFlags() ([]string, error) {
return keys, nil
}
func newStaticProvider(confFlags map[string]bool) (openfeature.FeatureProvider, error) {
flags := make(map[string]memprovider.InMemoryFlag, len(standardFeatureFlags))
// newStaticProvider creates a provider with support for different flag types
func newStaticProvider(typedFlags map[string]setting.TypedFeatureFlag) (openfeature.FeatureProvider, error) {
flags := make(map[string]memprovider.InMemoryFlag, len(standardFeatureFlags)+len(typedFlags))
// Add flags from config.ini file
for name, value := range confFlags {
flags[name] = createInMemoryFlag(name, value)
// Add standard flags first (these are always boolean)
for _, flag := range standardFeatureFlags {
enabled := flag.Expression == "true"
flags[flag.Name] = createBooleanFlag(flag.Name, enabled)
}
// Add standard flags
for _, flag := range standardFeatureFlags {
if _, exists := flags[flag.Name]; !exists {
enabled := flag.Expression == "true"
flags[flag.Name] = createInMemoryFlag(flag.Name, enabled)
}
// Add typed flags from config (these can override standard flags)
for n, f := range typedFlags {
flags[n] = createTypedFlag(n, f.Type, f.Value)
}
return newInMemoryBulkProvider(flags), nil
}
func createInMemoryFlag(name string, enabled bool) memprovider.InMemoryFlag {
type FlagType string
const (
FlagTypeBoolean FlagType = "boolean"
FlagTypeString FlagType = "string"
FlagTypeNumber FlagType = "number" // TODO: check in OFREP spec
FlagTypeObject FlagType = "object"
)
// TypedFlag represents a flag with its type and value
type TypedFlag struct {
Name string
Type FlagType
Value interface{}
}
func createTypedFlag(name, flagType string, value interface{}) memprovider.InMemoryFlag {
switch flagType {
case "boolean":
return createBooleanFlag(name, value.(bool))
case "string":
return createStringFlag(name, value.(string))
case "number":
return createNumberFlag(name, value.(float64))
case "object":
return createObjectFlag(name, value.(map[string]interface{}))
default:
// Default to boolean for backward compatibility
return createBooleanFlag(name, false)
}
}
func createBooleanFlag(name string, value bool) memprovider.InMemoryFlag {
variant := "disabled"
if enabled {
if value {
variant = "enabled"
}
@@ -62,3 +98,55 @@ func createInMemoryFlag(name string, enabled bool) memprovider.InMemoryFlag {
},
}
}
func createStringFlag(name string, value string) memprovider.InMemoryFlag {
return memprovider.InMemoryFlag{
Key: name,
DefaultVariant: "default",
Variants: map[string]interface{}{
"default": value,
},
}
}
func createNumberFlag(name string, value float64) memprovider.InMemoryFlag {
return memprovider.InMemoryFlag{
Key: name,
DefaultVariant: "default",
Variants: map[string]interface{}{
"default": value,
},
}
}
func createObjectFlag(name string, value map[string]interface{}) memprovider.InMemoryFlag {
return memprovider.InMemoryFlag{
Key: name,
DefaultVariant: "default",
Variants: map[string]interface{}{
"default": value,
},
}
}
// parseTypedFlagValue attempts to parse a string value into the appropriate type
func parseTypedFlagValue(value string) (interface{}, FlagType, error) {
// Try to parse as boolean
if boolVal, err := strconv.ParseBool(value); err == nil {
return boolVal, FlagTypeBoolean, nil
}
// Try to parse as number
if numVal, err := strconv.ParseFloat(value, 64); err == nil {
return numVal, FlagTypeNumber, nil
}
// Try to parse as JSON object
var objVal map[string]interface{}
if err := json.Unmarshal([]byte(value), &objVal); err == nil {
return objVal, FlagTypeObject, nil
}
// Default to string
return value, FlagTypeString, nil
}

View File

@@ -22,7 +22,7 @@ func Test_StaticProvider(t *testing.T) {
t.Run("empty config loads standard flags", func(t *testing.T) {
setup(t, []byte(``))
// Check for one of the standard flags
feat, err := openfeature.NewDefaultClient().BooleanValueDetails(ctx, stFeatName, !stFeatValue, evalCtx)
feat, err := openfeature.NewClient("").BooleanValueDetails(ctx, stFeatName, !stFeatValue, evalCtx)
assert.NoError(t, err)
assert.True(t, stFeatValue == feat.Value)
})

View File

@@ -1,6 +1,7 @@
package setting
import (
"encoding/json"
"strconv"
"gopkg.in/ini.v1"
@@ -45,3 +46,78 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str
}
return featureToggles, nil
}
// TypedFeatureFlag represents a flag with its type and value
type TypedFeatureFlag struct {
Type string `json:"type"`
Value interface{} `json:"value"`
}
// ReadTypedFeatureTogglesFromInitFile reads feature flags with support for different types
func ReadTypedFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]TypedFeatureFlag, error) {
typedFlags := make(map[string]TypedFeatureFlag, 10)
// parse the comma separated list of values in `enable` key
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
for _, feature := range util.SplitString(featuresTogglesStr) {
typedFlags[feature] = TypedFeatureFlag{
Type: "boolean",
Value: true,
}
}
// read all the other keys under [feature_toggles] section
for _, v := range featureTogglesSection.Keys() {
if v.Name() == "enable" {
continue
}
value := v.Value()
// try to determine the type of flag value
flagType, parsedValue, err := parseTypedFlagValue(value)
if err != nil {
// upon failure, default to boolean for backward compatibility
if boolVal, boolErr := strconv.ParseBool(value); boolErr == nil {
typedFlags[v.Name()] = TypedFeatureFlag{
Type: "boolean",
Value: boolVal,
}
} else {
// treat as string if even parsing as boolean fails
typedFlags[v.Name()] = TypedFeatureFlag{
Type: "string",
Value: value,
}
}
} else {
typedFlags[v.Name()] = TypedFeatureFlag{
Type: flagType,
Value: parsedValue,
}
}
}
return typedFlags, nil
}
// parseTypedFlagValue attempts to parse a string value
// into the appropriate type - bool, float, object
// defaults to string
func parseTypedFlagValue(value string) (string, interface{}, error) {
if boolVal, err := strconv.ParseBool(value); err == nil {
return "boolean", boolVal, nil
}
// TODO: probably int is needed as well
if numVal, err := strconv.ParseFloat(value, 64); err == nil {
return "number", numVal, nil
}
var objVal map[string]interface{}
if err := json.Unmarshal([]byte(value), &objVal); err == nil {
return "object", objVal, nil
}
return "string", value, nil
}