mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 12:04:45 +08:00
Compare commits
1 Commits
docs/add-t
...
undef1nd/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dafd900bc |
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user