From 30f3820879387f6d26c704ccd6a0e0f21901ad31 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 20 Nov 2023 10:34:33 +0100 Subject: [PATCH 1/6] Support a initial context in the library containing environment, etc ... Signed-off-by: Thomas Poignant --- config.go | 8 ++ examples/retriever_file/main.go | 1 + internal/flag/context.go | 18 +++- internal/flag/internal_flag.go | 5 +- internal/flag/internal_flag_test.go | 148 +++++++++++++++++++++------- testutils/flagv1/flag_data.go | 14 ++- variation.go | 26 +++-- 7 files changed, 161 insertions(+), 59 deletions(-) diff --git a/config.go b/config.go index f219a9ab2ed..d7d0f734ec4 100644 --- a/config.go +++ b/config.go @@ -69,6 +69,14 @@ type Config struct { // No notification will be sent neither. // Default: false Offline bool + + // CommonEvaluationContext (optional) will be merged with the evaluation context sent during the evaluation. + // It is useful to add common attributes to all the evaluation, such as a server version, environment, ... + // + // All those fields will be included in the custom attributes of the evaluation context, + // if in the evaluation context you have a field with the same name, it will override the common one. + // Default: nil + CommonEvaluationContext map[string]interface{} } // GetRetrievers returns a retriever.Retriever configure with the retriever available in the config. diff --git a/examples/retriever_file/main.go b/examples/retriever_file/main.go index bfc56b4237f..6cc89b8e5a0 100644 --- a/examples/retriever_file/main.go +++ b/examples/retriever_file/main.go @@ -42,6 +42,7 @@ func main() { user1 := ffcontext. NewEvaluationContextBuilder("aea2fdc1-b9a0-417a-b707-0c9083de68e3"). AddCustom("anonymous", true). + AddCustom("environment", "dev"). Build() user2 := ffcontext.NewEvaluationContext("332460b9-a8aa-4f7a-bc5d-9cc33632df9a") user3 := ffcontext.NewEvaluationContextBuilder("785a14bf-d2c5-4caa-9c70-2bbc4e3732a5"). diff --git a/internal/flag/context.go b/internal/flag/context.go index f8f414628f1..6e06e7e11bf 100644 --- a/internal/flag/context.go +++ b/internal/flag/context.go @@ -1,11 +1,21 @@ package flag type Context struct { - // Environment is the name of your current env - // this value will be added to the custom information of your user and, - // it will allow to create rules based on this environment, - Environment string + // CommonEvaluationContext will be merged with the evaluation context sent during the evaluation. + // It is useful to add common attributes to all the evaluation, such as a server version, environment, ... + // + // All those fields will be included in the custom attributes of the evaluation context, + // if in the evaluation context you have a field with the same name, it will override the common one. + // Default: nil + CommonEvaluationContext map[string]interface{} // DefaultSdkValue is the default value of the SDK when calling the variation. DefaultSdkValue interface{} } + +func (s *Context) AddIntoCommonContext(key string, value interface{}) { + if s.CommonEvaluationContext == nil { + s.CommonEvaluationContext = make(map[string]interface{}) + } + s.CommonEvaluationContext[key] = value +} diff --git a/internal/flag/internal_flag.go b/internal/flag/internal_flag.go index 62f305e7654..fa53a5e5e07 100644 --- a/internal/flag/internal_flag.go +++ b/internal/flag/internal_flag.go @@ -3,6 +3,7 @@ package flag import ( "fmt" "github.com/thomaspoignant/go-feature-flag/ffcontext" + "maps" "time" "github.com/thomaspoignant/go-feature-flag/internal/internalerror" @@ -63,8 +64,8 @@ func (f *InternalFlag) Value( ) (interface{}, ResolutionDetails) { f.applyScheduledRolloutSteps() - if flagContext.Environment != "" { - evaluationCtx.AddCustomAttribute("env", flagContext.Environment) + if flagContext.CommonEvaluationContext != nil { + maps.Copy(evaluationCtx.GetCustom(), flagContext.CommonEvaluationContext) } if f.IsDisable() || f.isExperimentationOver() { diff --git a/internal/flag/internal_flag_test.go b/internal/flag/internal_flag_test.go index 828d3d53002..ffe975ad09c 100644 --- a/internal/flag/internal_flag_test.go +++ b/internal/flag/internal_flag_test.go @@ -14,9 +14,9 @@ import ( func TestInternalFlag_Value(t *testing.T) { type args struct { - flagName string - user ffcontext.Context - evaluationCtx flag.Context + flagName string + user ffcontext.Context + flagContext flag.Context } tests := []struct { name string @@ -43,7 +43,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContext("user-key"), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: false, }, }, @@ -77,7 +77,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContext("user-key"), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: false, }, }, @@ -104,7 +104,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContext("user-key"), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "default-sdk", }, }, @@ -134,7 +134,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContext("user-key"), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "default-sdk", }, }, @@ -164,7 +164,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContext("user-key"), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "default-sdk", }, }, @@ -204,7 +204,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContext("user-key"), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: false, }, }, @@ -252,7 +252,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContext("user-key"), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -298,7 +298,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContext("user-key"), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -348,7 +348,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").AddCustom("company", "go-feature-flag").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -399,7 +399,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").AddCustom("company", "go-feature-flag").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -444,7 +444,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -485,7 +485,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -523,7 +523,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -574,7 +574,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -624,7 +624,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -674,7 +674,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -715,7 +715,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -770,7 +770,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -818,7 +818,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key-123").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -874,7 +874,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key-123").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -926,7 +926,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -968,7 +968,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1011,7 +1011,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1064,7 +1064,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1107,7 +1107,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1147,7 +1147,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1192,7 +1192,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1246,7 +1246,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1291,7 +1291,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1331,7 +1331,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1364,7 +1364,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1400,7 +1400,7 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "my-flag", user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: "value_default", }, }, @@ -1444,9 +1444,11 @@ func TestInternalFlag_Value(t *testing.T) { args: args{ flagName: "feature", user: ffcontext.NewEvaluationContextBuilder("1").Build(), - evaluationCtx: flag.Context{ + flagContext: flag.Context{ DefaultSdkValue: true, - Environment: "development", + CommonEvaluationContext: map[string]interface{}{ + "env": "development", + }, }, }, want: false, @@ -1461,10 +1463,80 @@ func TestInternalFlag_Value(t *testing.T) { }, }, }, + { + name: "Should have a targeting match if common evaluation context match", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "A": testconvert.Interface("A"), + "B": testconvert.Interface("B"), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String("environment eq \"prod\""), + VariationResult: testconvert.String("A"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("B"), + }, + }, + args: args{ + flagName: "my-flag", + user: ffcontext.NewEvaluationContext("user-key"), + flagContext: flag.Context{ + DefaultSdkValue: "default-sdk", + CommonEvaluationContext: map[string]interface{}{ + "environment": "prod", + }, + }, + }, + want: "A", + want1: flag.ResolutionDetails{ + Variant: "A", + Reason: flag.ReasonTargetingMatch, + RuleIndex: testconvert.Int(0), + Cacheable: true, + }, + }, + { + name: "CommonEvaluationContext should override request evaluation context", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "A": testconvert.Interface("A"), + "B": testconvert.Interface("B"), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String("environment eq \"prod\""), + VariationResult: testconvert.String("A"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("B"), + }, + }, + args: args{ + flagName: "my-flag", + user: ffcontext.NewEvaluationContextBuilder("key1").AddCustom("environment", "dev").Build(), + flagContext: flag.Context{ + DefaultSdkValue: "default-sdk", + CommonEvaluationContext: map[string]interface{}{ + "environment": "prod", + }, + }, + }, + want: "A", + want1: flag.ResolutionDetails{ + Variant: "A", + Reason: flag.ReasonTargetingMatch, + RuleIndex: testconvert.Int(0), + Cacheable: true, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, got1 := tt.flag.Value(tt.args.flagName, tt.args.user, tt.args.evaluationCtx) + got, got1 := tt.flag.Value(tt.args.flagName, tt.args.user, tt.args.flagContext) assert.Equalf(t, tt.want, got, "not expected value: %s", cmp.Diff(tt.want, got)) assert.Equalf(t, tt.want1, got1, "not expected value: %s", cmp.Diff(tt.want1, got1)) }) diff --git a/testutils/flagv1/flag_data.go b/testutils/flagv1/flag_data.go index 4afe21cf730..e38a913b685 100644 --- a/testutils/flagv1/flag_data.go +++ b/testutils/flagv1/flag_data.go @@ -57,11 +57,11 @@ type FlagData struct { } // Value is returning the Value associate to the flag (True / False / Default ) based -// if the toggle apply to the user or not. +// if the toggle applies to the user or not. func (f *FlagData) Value( flagName string, user ffcontext.Context, - evaluationCtx flag.Context, + flagCtx flag.Context, ) (interface{}, flag.ResolutionDetails) { f.updateFlagStage() if f.isExperimentationOver() { @@ -74,7 +74,7 @@ func (f *FlagData) Value( // Flag disable we cannot apply it. if f.IsDisable() { - return evaluationCtx.DefaultSdkValue, flag.ResolutionDetails{ + return flagCtx.DefaultSdkValue, flag.ResolutionDetails{ Variant: flag.VariationSDKDefault, Reason: flag.ReasonDisabled, } @@ -87,8 +87,12 @@ func (f *FlagData) Value( Reason: flag.ReasonTargetingMatch, } } - - if f.evaluateRule(user, evaluationCtx.Environment) { + + env := "" + if flagCtx.CommonEvaluationContext != nil && flagCtx.CommonEvaluationContext["env"] != nil { + env = flagCtx.CommonEvaluationContext["env"].(string) + } + if f.evaluateRule(user, env) { if f.isInPercentage(flagName, user) { // Rule applied and user in the cohort. return f.getTrue(), flag.ResolutionDetails{ diff --git a/variation.go b/variation.go index 22805c7060b..174c6453585 100644 --- a/variation.go +++ b/variation.go @@ -246,7 +246,7 @@ func GetFlagsFromCache() (map[string]flag.Flag, error) { } // AllFlagsState return a flagstate.AllFlags that contains all the flags for a specific user. -func (g *GoFeatureFlag) AllFlagsState(ctx ffcontext.Context) flagstate.AllFlags { +func (g *GoFeatureFlag) AllFlagsState(evaluationCtx ffcontext.Context) flagstate.AllFlags { flags := map[string]flag.Flag{} if g == nil { // empty AllFlags will set valid to false @@ -264,12 +264,14 @@ func (g *GoFeatureFlag) AllFlagsState(ctx ffcontext.Context) flagstate.AllFlags allFlags := flagstate.NewAllFlags() for key, currentFlag := range flags { - flagValue, resolutionDetails := currentFlag.Value(key, ctx, flag.Context{ - Environment: g.config.Environment, - DefaultSdkValue: nil, - }) + flagCtx := flag.Context{ + CommonEvaluationContext: g.config.CommonEvaluationContext, + DefaultSdkValue: nil, + } + flagCtx.AddIntoCommonContext("env", g.config.Environment) + flagValue, resolutionDetails := currentFlag.Value(key, evaluationCtx, flagCtx) - // if the flag is disabled we are ignoring it. + // if the flag is disabled, we are ignoring it. if resolutionDetails.Reason == flag.ReasonDisabled { allFlags.AddFlag(key, flagstate.FlagState{ Timestamp: time.Now().Unix(), @@ -370,7 +372,7 @@ func notifyVariation[T model.JSONType]( // getVariation is the internal generic func that handle the logic of a variation the result will always // contain a valid model.VariationResult func getVariation[T model.JSONType]( - g *GoFeatureFlag, flagKey string, ctx ffcontext.Context, sdkDefaultValue T, expectedType string, + g *GoFeatureFlag, flagKey string, evaluationCtx ffcontext.Context, sdkDefaultValue T, expectedType string, ) (model.VariationResult[T], error) { if g == nil { return model.VariationResult[T]{ @@ -411,13 +413,17 @@ func getVariation[T model.JSONType]( return varResult, err } - flagValue, resolutionDetails := f.Value(flagKey, ctx, - flag.Context{Environment: g.config.Environment, DefaultSdkValue: sdkDefaultValue}) + flagCtx := flag.Context{ + DefaultSdkValue: sdkDefaultValue, + CommonEvaluationContext: g.config.CommonEvaluationContext, + } + flagCtx.AddIntoCommonContext("env", g.config.Environment) + flagValue, resolutionDetails := f.Value(flagKey, evaluationCtx, flagCtx) var convertedValue interface{} switch value := flagValue.(type) { case float64: - // this part ensure that we convert float64 value into int if we call IntVariation on a float64 value. + // this part ensures that we convert float64 value into int if we call IntVariation on a float64 value. if expectedType == "int" { convertedValue = int(value) } else { From 1e63bc3eff284bb694dbcca5744ae2c91653aace Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 20 Nov 2023 10:51:35 +0100 Subject: [PATCH 2/6] Rename field Signed-off-by: Thomas Poignant --- config.go | 6 +++--- internal/flag/context.go | 10 +++++----- internal/flag/internal_flag.go | 4 ++-- internal/flag/internal_flag_test.go | 8 ++++---- testutils/flagv1/flag_data.go | 6 +++--- variation.go | 9 ++++----- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/config.go b/config.go index d7d0f734ec4..1cd31c83f8d 100644 --- a/config.go +++ b/config.go @@ -70,13 +70,13 @@ type Config struct { // Default: false Offline bool - // CommonEvaluationContext (optional) will be merged with the evaluation context sent during the evaluation. + // CommonEvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation. // It is useful to add common attributes to all the evaluation, such as a server version, environment, ... // // All those fields will be included in the custom attributes of the evaluation context, // if in the evaluation context you have a field with the same name, it will override the common one. // Default: nil - CommonEvaluationContext map[string]interface{} + CommonEvaluationContextEnrichment map[string]interface{} } // GetRetrievers returns a retriever.Retriever configure with the retriever available in the config. @@ -86,7 +86,7 @@ func (c *Config) GetRetrievers() ([]retriever.Retriever, error) { } retrievers := make([]retriever.Retriever, 0) - // If we have both Retriever and Retrievers fields configured we are 1st looking at what is available + // If we have both Retriever and Retrievers fields configured, we are 1st looking at what is available // in Retriever before looking at what is in Retrievers. if c.Retriever != nil { retrievers = append(retrievers, c.Retriever) diff --git a/internal/flag/context.go b/internal/flag/context.go index 6e06e7e11bf..81d2735b018 100644 --- a/internal/flag/context.go +++ b/internal/flag/context.go @@ -1,21 +1,21 @@ package flag type Context struct { - // CommonEvaluationContext will be merged with the evaluation context sent during the evaluation. + // CommonContext will be merged with the evaluation context sent during the evaluation. // It is useful to add common attributes to all the evaluation, such as a server version, environment, ... // // All those fields will be included in the custom attributes of the evaluation context, // if in the evaluation context you have a field with the same name, it will override the common one. // Default: nil - CommonEvaluationContext map[string]interface{} + CommonContext map[string]interface{} // DefaultSdkValue is the default value of the SDK when calling the variation. DefaultSdkValue interface{} } func (s *Context) AddIntoCommonContext(key string, value interface{}) { - if s.CommonEvaluationContext == nil { - s.CommonEvaluationContext = make(map[string]interface{}) + if s.CommonContext == nil { + s.CommonContext = make(map[string]interface{}) } - s.CommonEvaluationContext[key] = value + s.CommonContext[key] = value } diff --git a/internal/flag/internal_flag.go b/internal/flag/internal_flag.go index fa53a5e5e07..59295de6db4 100644 --- a/internal/flag/internal_flag.go +++ b/internal/flag/internal_flag.go @@ -64,8 +64,8 @@ func (f *InternalFlag) Value( ) (interface{}, ResolutionDetails) { f.applyScheduledRolloutSteps() - if flagContext.CommonEvaluationContext != nil { - maps.Copy(evaluationCtx.GetCustom(), flagContext.CommonEvaluationContext) + if flagContext.CommonContext != nil { + maps.Copy(evaluationCtx.GetCustom(), flagContext.CommonContext) } if f.IsDisable() || f.isExperimentationOver() { diff --git a/internal/flag/internal_flag_test.go b/internal/flag/internal_flag_test.go index ffe975ad09c..9a27083ddaa 100644 --- a/internal/flag/internal_flag_test.go +++ b/internal/flag/internal_flag_test.go @@ -1446,7 +1446,7 @@ func TestInternalFlag_Value(t *testing.T) { user: ffcontext.NewEvaluationContextBuilder("1").Build(), flagContext: flag.Context{ DefaultSdkValue: true, - CommonEvaluationContext: map[string]interface{}{ + CommonContext: map[string]interface{}{ "env": "development", }, }, @@ -1485,7 +1485,7 @@ func TestInternalFlag_Value(t *testing.T) { user: ffcontext.NewEvaluationContext("user-key"), flagContext: flag.Context{ DefaultSdkValue: "default-sdk", - CommonEvaluationContext: map[string]interface{}{ + CommonContext: map[string]interface{}{ "environment": "prod", }, }, @@ -1499,7 +1499,7 @@ func TestInternalFlag_Value(t *testing.T) { }, }, { - name: "CommonEvaluationContext should override request evaluation context", + name: "CommonEvaluationContextEnrichment should override request evaluation context", flag: flag.InternalFlag{ Variations: &map[string]*interface{}{ "A": testconvert.Interface("A"), @@ -1520,7 +1520,7 @@ func TestInternalFlag_Value(t *testing.T) { user: ffcontext.NewEvaluationContextBuilder("key1").AddCustom("environment", "dev").Build(), flagContext: flag.Context{ DefaultSdkValue: "default-sdk", - CommonEvaluationContext: map[string]interface{}{ + CommonContext: map[string]interface{}{ "environment": "prod", }, }, diff --git a/testutils/flagv1/flag_data.go b/testutils/flagv1/flag_data.go index e38a913b685..3d471d1e17a 100644 --- a/testutils/flagv1/flag_data.go +++ b/testutils/flagv1/flag_data.go @@ -87,10 +87,10 @@ func (f *FlagData) Value( Reason: flag.ReasonTargetingMatch, } } - + env := "" - if flagCtx.CommonEvaluationContext != nil && flagCtx.CommonEvaluationContext["env"] != nil { - env = flagCtx.CommonEvaluationContext["env"].(string) + if flagCtx.CommonContext != nil && flagCtx.CommonContext["env"] != nil { + env = flagCtx.CommonContext["env"].(string) } if f.evaluateRule(user, env) { if f.isInPercentage(flagName, user) { diff --git a/variation.go b/variation.go index 174c6453585..30467bdffa4 100644 --- a/variation.go +++ b/variation.go @@ -265,8 +265,8 @@ func (g *GoFeatureFlag) AllFlagsState(evaluationCtx ffcontext.Context) flagstate allFlags := flagstate.NewAllFlags() for key, currentFlag := range flags { flagCtx := flag.Context{ - CommonEvaluationContext: g.config.CommonEvaluationContext, - DefaultSdkValue: nil, + CommonContext: g.config.CommonEvaluationContextEnrichment, + DefaultSdkValue: nil, } flagCtx.AddIntoCommonContext("env", g.config.Environment) flagValue, resolutionDetails := currentFlag.Value(key, evaluationCtx, flagCtx) @@ -384,7 +384,6 @@ func getVariation[T model.JSONType]( Cacheable: false, }, fmt.Errorf("go-feature-flag is not initialised, default value is used") } - if g.config.Offline { return model.VariationResult[T]{ Value: sdkDefaultValue, @@ -414,8 +413,8 @@ func getVariation[T model.JSONType]( } flagCtx := flag.Context{ - DefaultSdkValue: sdkDefaultValue, - CommonEvaluationContext: g.config.CommonEvaluationContext, + DefaultSdkValue: sdkDefaultValue, + CommonContext: g.config.CommonEvaluationContextEnrichment, } flagCtx.AddIntoCommonContext("env", g.config.Environment) flagValue, resolutionDetails := f.Value(flagKey, evaluationCtx, flagCtx) From f2f9592d266565ad58189944eb42efb520d98a7c Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 20 Nov 2023 11:04:53 +0100 Subject: [PATCH 3/6] Rename to EvaluationContextEnrichment Signed-off-by: Thomas Poignant --- cmd/relayproxy/config/config.go | 5 ++- config.go | 4 +- internal/flag/context.go | 12 +++--- internal/flag/context_test.go | 57 +++++++++++++++++++++++++++++ internal/flag/internal_flag.go | 4 +- internal/flag/internal_flag_test.go | 8 ++-- testutils/flagv1/flag_data.go | 4 +- variation.go | 12 +++--- 8 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 internal/flag/context_test.go diff --git a/cmd/relayproxy/config/config.go b/cmd/relayproxy/config/config.go index a6d1bd9ae3a..70aff04926b 100644 --- a/cmd/relayproxy/config/config.go +++ b/cmd/relayproxy/config/config.go @@ -171,9 +171,12 @@ type Config struct { // APIKeys list of API keys that authorized to use endpoints APIKeys []string `mapstructure:"apiKeys" koanf:"apikeys"` - // StartAsAwsLambda (optional) if true the relay proxy will start ready to be launch as AWS Lambda + // StartAsAwsLambda (optional) if true, the relay proxy will start ready to be launched as AWS Lambda StartAsAwsLambda bool `mapstructure:"startAsAwsLambda" koanf:"startasawslambda"` + // EvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation. + EvaluationContextEnrichment map[string]interface{} `mapstructure:"evaluationContextEnrichment" koanf:"evaluationcontextenrichment"` //nolint: lll + // ---- private fields // apiKeySet is the internal representation of the list of api keys configured diff --git a/config.go b/config.go index 1cd31c83f8d..8931ee3874a 100644 --- a/config.go +++ b/config.go @@ -70,13 +70,13 @@ type Config struct { // Default: false Offline bool - // CommonEvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation. + // EvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation. // It is useful to add common attributes to all the evaluation, such as a server version, environment, ... // // All those fields will be included in the custom attributes of the evaluation context, // if in the evaluation context you have a field with the same name, it will override the common one. // Default: nil - CommonEvaluationContextEnrichment map[string]interface{} + EvaluationContextEnrichment map[string]interface{} } // GetRetrievers returns a retriever.Retriever configure with the retriever available in the config. diff --git a/internal/flag/context.go b/internal/flag/context.go index 81d2735b018..a0c8da72af2 100644 --- a/internal/flag/context.go +++ b/internal/flag/context.go @@ -1,21 +1,21 @@ package flag type Context struct { - // CommonContext will be merged with the evaluation context sent during the evaluation. + // EvaluationContextEnrichment will be merged with the evaluation context sent during the evaluation. // It is useful to add common attributes to all the evaluation, such as a server version, environment, ... // // All those fields will be included in the custom attributes of the evaluation context, // if in the evaluation context you have a field with the same name, it will override the common one. // Default: nil - CommonContext map[string]interface{} + EvaluationContextEnrichment map[string]interface{} // DefaultSdkValue is the default value of the SDK when calling the variation. DefaultSdkValue interface{} } -func (s *Context) AddIntoCommonContext(key string, value interface{}) { - if s.CommonContext == nil { - s.CommonContext = make(map[string]interface{}) +func (s *Context) AddIntoEvaluationContextEnrichment(key string, value interface{}) { + if s.EvaluationContextEnrichment == nil { + s.EvaluationContextEnrichment = make(map[string]interface{}) } - s.CommonContext[key] = value + s.EvaluationContextEnrichment[key] = value } diff --git a/internal/flag/context_test.go b/internal/flag/context_test.go new file mode 100644 index 00000000000..3bb4fbb7bb5 --- /dev/null +++ b/internal/flag/context_test.go @@ -0,0 +1,57 @@ +package flag_test + +import ( + "github.com/stretchr/testify/assert" + "github.com/thomaspoignant/go-feature-flag/internal/flag" + "testing" +) + +func TestContext_AddIntoEvaluationContextEnrichment(t *testing.T) { + type args struct { + key string + value interface{} + } + tests := []struct { + name string + EvaluationContextEnrichment map[string]interface{} + args args + expected interface{} + }{ + { + name: "Add a new key to a nil map", + EvaluationContextEnrichment: nil, + args: args{ + key: "env", + value: "prod", + }, + expected: "prod", + }, + { + name: "Add a new key to an existing map", + EvaluationContextEnrichment: map[string]interface{}{"john": "doe"}, + args: args{ + key: "env", + value: "prod", + }, + expected: "prod", + }, + { + name: "Override an existing key", + EvaluationContextEnrichment: map[string]interface{}{"env": "dev"}, + args: args{ + key: "env", + value: "prod", + }, + expected: "prod", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &flag.Context{ + EvaluationContextEnrichment: tt.EvaluationContextEnrichment, + } + s.AddIntoEvaluationContextEnrichment(tt.args.key, tt.args.value) + assert.Equal(t, tt.expected, s.EvaluationContextEnrichment[tt.args.key]) + }) + } +} diff --git a/internal/flag/internal_flag.go b/internal/flag/internal_flag.go index 59295de6db4..66e95ef2b3d 100644 --- a/internal/flag/internal_flag.go +++ b/internal/flag/internal_flag.go @@ -64,8 +64,8 @@ func (f *InternalFlag) Value( ) (interface{}, ResolutionDetails) { f.applyScheduledRolloutSteps() - if flagContext.CommonContext != nil { - maps.Copy(evaluationCtx.GetCustom(), flagContext.CommonContext) + if flagContext.EvaluationContextEnrichment != nil { + maps.Copy(evaluationCtx.GetCustom(), flagContext.EvaluationContextEnrichment) } if f.IsDisable() || f.isExperimentationOver() { diff --git a/internal/flag/internal_flag_test.go b/internal/flag/internal_flag_test.go index 9a27083ddaa..896e1c95870 100644 --- a/internal/flag/internal_flag_test.go +++ b/internal/flag/internal_flag_test.go @@ -1446,7 +1446,7 @@ func TestInternalFlag_Value(t *testing.T) { user: ffcontext.NewEvaluationContextBuilder("1").Build(), flagContext: flag.Context{ DefaultSdkValue: true, - CommonContext: map[string]interface{}{ + EvaluationContextEnrichment: map[string]interface{}{ "env": "development", }, }, @@ -1485,7 +1485,7 @@ func TestInternalFlag_Value(t *testing.T) { user: ffcontext.NewEvaluationContext("user-key"), flagContext: flag.Context{ DefaultSdkValue: "default-sdk", - CommonContext: map[string]interface{}{ + EvaluationContextEnrichment: map[string]interface{}{ "environment": "prod", }, }, @@ -1499,7 +1499,7 @@ func TestInternalFlag_Value(t *testing.T) { }, }, { - name: "CommonEvaluationContextEnrichment should override request evaluation context", + name: "EvaluationContextEnrichment should override request evaluation context", flag: flag.InternalFlag{ Variations: &map[string]*interface{}{ "A": testconvert.Interface("A"), @@ -1520,7 +1520,7 @@ func TestInternalFlag_Value(t *testing.T) { user: ffcontext.NewEvaluationContextBuilder("key1").AddCustom("environment", "dev").Build(), flagContext: flag.Context{ DefaultSdkValue: "default-sdk", - CommonContext: map[string]interface{}{ + EvaluationContextEnrichment: map[string]interface{}{ "environment": "prod", }, }, diff --git a/testutils/flagv1/flag_data.go b/testutils/flagv1/flag_data.go index 3d471d1e17a..b2eb6352280 100644 --- a/testutils/flagv1/flag_data.go +++ b/testutils/flagv1/flag_data.go @@ -89,8 +89,8 @@ func (f *FlagData) Value( } env := "" - if flagCtx.CommonContext != nil && flagCtx.CommonContext["env"] != nil { - env = flagCtx.CommonContext["env"].(string) + if flagCtx.EvaluationContextEnrichment != nil && flagCtx.EvaluationContextEnrichment["env"] != nil { + env = flagCtx.EvaluationContextEnrichment["env"].(string) } if f.evaluateRule(user, env) { if f.isInPercentage(flagName, user) { diff --git a/variation.go b/variation.go index 30467bdffa4..700ed3f76b8 100644 --- a/variation.go +++ b/variation.go @@ -265,10 +265,10 @@ func (g *GoFeatureFlag) AllFlagsState(evaluationCtx ffcontext.Context) flagstate allFlags := flagstate.NewAllFlags() for key, currentFlag := range flags { flagCtx := flag.Context{ - CommonContext: g.config.CommonEvaluationContextEnrichment, - DefaultSdkValue: nil, + EvaluationContextEnrichment: g.config.EvaluationContextEnrichment, + DefaultSdkValue: nil, } - flagCtx.AddIntoCommonContext("env", g.config.Environment) + flagCtx.AddIntoEvaluationContextEnrichment("env", g.config.Environment) flagValue, resolutionDetails := currentFlag.Value(key, evaluationCtx, flagCtx) // if the flag is disabled, we are ignoring it. @@ -413,10 +413,10 @@ func getVariation[T model.JSONType]( } flagCtx := flag.Context{ - DefaultSdkValue: sdkDefaultValue, - CommonContext: g.config.CommonEvaluationContextEnrichment, + DefaultSdkValue: sdkDefaultValue, + EvaluationContextEnrichment: g.config.EvaluationContextEnrichment, } - flagCtx.AddIntoCommonContext("env", g.config.Environment) + flagCtx.AddIntoEvaluationContextEnrichment("env", g.config.Environment) flagValue, resolutionDetails := f.Value(flagKey, evaluationCtx, flagCtx) var convertedValue interface{} From 337ef758b2d5fb4bdfd8c9b2819333f27cc89094 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 20 Nov 2023 11:07:14 +0100 Subject: [PATCH 4/6] Support EvaluationContextEnrichment in relay proxy Signed-off-by: Thomas Poignant --- cmd/relayproxy/service/gofeatureflag.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/cmd/relayproxy/service/gofeatureflag.go b/cmd/relayproxy/service/gofeatureflag.go index 1086654a4f1..02484cc999a 100644 --- a/cmd/relayproxy/service/gofeatureflag.go +++ b/cmd/relayproxy/service/gofeatureflag.go @@ -75,16 +75,17 @@ func NewGoFeatureFlagClient( notif = append(notif, notifiers...) f := ffclient.Config{ - PollingInterval: time.Duration(proxyConf.PollingInterval) * time.Millisecond, - Logger: zap.NewStdLog(logger), - Context: context.Background(), - Retriever: mainRetriever, - Retrievers: retrievers, - Notifiers: notif, - FileFormat: proxyConf.FileFormat, - DataExporter: exp, - StartWithRetrieverError: proxyConf.StartWithRetrieverError, - EnablePollingJitter: proxyConf.EnablePollingJitter, + PollingInterval: time.Duration(proxyConf.PollingInterval) * time.Millisecond, + Logger: zap.NewStdLog(logger), + Context: context.Background(), + Retriever: mainRetriever, + Retrievers: retrievers, + Notifiers: notif, + FileFormat: proxyConf.FileFormat, + DataExporter: exp, + StartWithRetrieverError: proxyConf.StartWithRetrieverError, + EnablePollingJitter: proxyConf.EnablePollingJitter, + EvaluationContextEnrichment: proxyConf.EvaluationContextEnrichment, } return ffclient.New(f) From 8ac11f45e58b1c638b4482954feb41ce8c9996a8 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 20 Nov 2023 11:15:10 +0100 Subject: [PATCH 5/6] Add integration test to validate evaluationContextEnrichment in the relay proxy Signed-off-by: Thomas Poignant --- openfeature/provider_tests/flags.yaml | 9 +++++++++ .../go-integration-tests/provider_test.go | 20 +++++++++++++++++++ openfeature/provider_tests/goff-proxy.yaml | 4 +++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/openfeature/provider_tests/flags.yaml b/openfeature/provider_tests/flags.yaml index 642581c25c9..8bc7928758b 100644 --- a/openfeature/provider_tests/flags.yaml +++ b/openfeature/provider_tests/flags.yaml @@ -165,3 +165,12 @@ string_key_with_version: metadata: description: this is a test pr_link: https://github.com/thomaspoignant/go-feature-flag/pull/916 +flag-use-evaluation-context-enrichment: + variations: + A: A + B: B + targeting: + - query: environment eq "integration-test" + variation: A + defaultRule: + variation: B diff --git a/openfeature/provider_tests/go-integration-tests/provider_test.go b/openfeature/provider_tests/go-integration-tests/provider_test.go index 4e130f7c1fe..3e20a345b74 100644 --- a/openfeature/provider_tests/go-integration-tests/provider_test.go +++ b/openfeature/provider_tests/go-integration-tests/provider_test.go @@ -256,6 +256,26 @@ func TestProvider_module_StringEvaluation(t *testing.T) { }, }, }, + { + name: "should resolve a targeting match if rule contains filed from evaluationContextEnrichment", + args: args{ + flag: "flag-use-evaluation-context-enrichment", + defaultValue: "default", + evalCtx: defaultEvaluationCtx(), + }, + want: of.StringEvaluationDetails{ + Value: "A", + EvaluationDetails: of.EvaluationDetails{ + FlagKey: "flag-use-evaluation-context-enrichment", + FlagType: of.String, + ResolutionDetail: of.ResolutionDetail{ + Variant: "A", + Reason: of.TargetingMatchReason, + FlagMetadata: map[string]interface{}{}, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/openfeature/provider_tests/goff-proxy.yaml b/openfeature/provider_tests/goff-proxy.yaml index f9ac5b4d0b4..7e3d99392e0 100644 --- a/openfeature/provider_tests/goff-proxy.yaml +++ b/openfeature/provider_tests/goff-proxy.yaml @@ -6,4 +6,6 @@ retriever: path: ./openfeature/provider_tests/flags.yaml exporter: kind: log -enableSwagger: true \ No newline at end of file +enableSwagger: true +evaluationContextEnrichment: + environment: integration-test \ No newline at end of file From bbee1cb87f1ea5d89e7699546142fd23abb64782 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 20 Nov 2023 11:27:16 +0100 Subject: [PATCH 6/6] Documentation about the evaluationContextEnrichment Signed-off-by: Thomas Poignant --- cmd/relayproxy/config/config.go | 6 ++ website/docs/go_module/configuration.md | 29 +++++----- .../docs/relay_proxy/configure_relay_proxy.md | 57 ++++++++++--------- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/cmd/relayproxy/config/config.go b/cmd/relayproxy/config/config.go index 70aff04926b..f4e15b5fb99 100644 --- a/cmd/relayproxy/config/config.go +++ b/cmd/relayproxy/config/config.go @@ -175,6 +175,12 @@ type Config struct { StartAsAwsLambda bool `mapstructure:"startAsAwsLambda" koanf:"startasawslambda"` // EvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation. + // It is useful to add common attributes to all the evaluations, such as a server version, environment, ... + // + // All those fields will be included in the custom attributes of the evaluation context, + // if in the evaluation context you have a field with the same name, + // it will be overridden by the evaluationContextEnrichment. + // Default: nil EvaluationContextEnrichment map[string]interface{} `mapstructure:"evaluationContextEnrichment" koanf:"evaluationcontextenrichment"` //nolint: lll // ---- private fields diff --git a/website/docs/go_module/configuration.md b/website/docs/go_module/configuration.md index 832784c421d..b7436353e9f 100644 --- a/website/docs/go_module/configuration.md +++ b/website/docs/go_module/configuration.md @@ -11,20 +11,21 @@ During the initialization you must give a [`ffclient.Config{}`](https://pkg.go.d ## Configuration fields -| Field | Description | -|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `Retriever` | The configuration retriever you want to use to get your flag file
*See [Store your flag file](./store_file/index.md) for the configuration details*.

*This field is optional if `Retrievers`* is configured. | -| `Retrievers` | `Retrievers` is exactly the same thing as `Retriever` but you can configure more than 1 source for your flags.
All flags are retrieved in parallel, but we are applying them in the order you provided them _(it means that a flag can be overridden by another flag)_.
*See [Store your flag file](./store_file/index.md) for the configuration details*.

*This field is optional if `Retrievers`* is configured. | -| `Context` | *(optional)*
The context used by the retriever.
Default: `context.Background()` | -| `Environment` | *(optional)*
The environment the app is running under, can be checked in feature flag rules.
Default: `""`
*Check [**"environments"** section](../configure_flag/flag_format/#environments) to understand how to use this parameter.* | -| `DataExporter` | *(optional)*
DataExporter defines how to export data on how your flags are used.
*see [export data section](data_collection/index.md) for more details*. | -| `FileFormat` | *(optional)*
Format of your configuration file. Available formats are `yaml`, `toml` and `json`, if you omit the field it will try to unmarshal the file as a `yaml` file.
Default: `YAML` | -| `Logger` | *(optional)*
Logger used to log what `go-feature-flag` is doing.
If no logger is provided the module will not log anything.
Default: No log | -| `Notifiers` | *(optional)*
List of notifiers to call when your flag file has been changed.
*See [notifiers section](./notifier/index.md) for more details*. | -| `PollingInterval` | (optional) Duration to wait before refreshing the flags.
The minimum polling interval is 1 second.
Default: 60 * time.Second | -| `EnablePollingJitter` | (optional) set to true if you want to avoid having true periodicity when retrieving your flags. It is useful to avoid having spike on your flag configuration storage in case your application is starting multiple instance at the same time.
We ensure a deviation that is maximum + or - 10% of your polling interval.
Default: false | -| `StartWithRetrieverError` | *(optional)* If **true**, the SDK will start even if we did not get any flags from the retriever. It will serve only default values until the retriever returns the flags.
The init method will not return any error if the flag file is unreachable.
Default: **false** | -| `Offline` | *(optional)* If **true**, the SDK will not try to retrieve the flag file and will not export any data. No notification will be send neither.
Default: false | +| Field | Description | +|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Retriever` | The configuration retriever you want to use to get your flag file
*See [Store your flag file](./store_file/index.md) for the configuration details*.

*This field is optional if `Retrievers`* is configured. | +| `Retrievers` | `Retrievers` is exactly the same thing as `Retriever` but you can configure more than 1 source for your flags.
All flags are retrieved in parallel, but we are applying them in the order you provided them _(it means that a flag can be overridden by another flag)_.
*See [Store your flag file](./store_file/index.md) for the configuration details*.

*This field is optional if `Retrievers`* is configured. | +| `Context` | *(optional)*
The context used by the retriever.
Default: **`context.Background()`** | +| `Environment` | *(optional)*
The environment the app is running under, can be checked in feature flag rules.
Default: `""`
*Check [**"environments"** section](../configure_flag/flag_format/#environments) to understand how to use this parameter.* | +| `DataExporter` | *(optional)*
DataExporter defines how to export data on how your flags are used.
*see [export data section](data_collection/index.md) for more details*. | +| `FileFormat` | *(optional)*
Format of your configuration file. Available formats are `yaml`, `toml` and `json`, if you omit the field it will try to unmarshal the file as a `yaml` file.
Default: **`YAML`** | +| `Logger` | *(optional)*
Logger used to log what `go-feature-flag` is doing.
If no logger is provided the module will not log anything.
Default: **No log** | +| `Notifiers` | *(optional)*
List of notifiers to call when your flag file has been changed.
*See [notifiers section](./notifier/index.md) for more details*. | +| `PollingInterval` | (optional) Duration to wait before refreshing the flags.
The minimum polling interval is 1 second.
Default: **60 * time.Second** | +| `EnablePollingJitter` | (optional) set to true if you want to avoid having true periodicity when retrieving your flags. It is useful to avoid having spike on your flag configuration storage in case your application is starting multiple instance at the same time.
We ensure a deviation that is maximum + or - 10% of your polling interval.
Default: **false** | +| `StartWithRetrieverError` | *(optional)* If **true**, the SDK will start even if we did not get any flags from the retriever. It will serve only default values until the retriever returns the flags.
The init method will not return any error if the flag file is unreachable.
Default: **false** | +| `Offline` | *(optional)* If **true**, the SDK will not try to retrieve the flag file and will not export any data. No notification will be send neither.
Default: **false** | +| `EvaluationContextEnrichment` | *(optional)* It is a free `map[string]interface{}` field that will be merged with the evaluation context sent during the evaluations. It is useful to add common attributes to all the evaluation, such as a server version, environment, ...
All those fields will be included in the custom attributes of the evaluation context.
If in the evaluation context you have a field with the same name, it will be overriden by the `evaluationContextEnrichment`.
Default: **nil** | ## Example ```go diff --git a/website/docs/relay_proxy/configure_relay_proxy.md b/website/docs/relay_proxy/configure_relay_proxy.md index 2300bfcea08..de5868d8de3 100644 --- a/website/docs/relay_proxy/configure_relay_proxy.md +++ b/website/docs/relay_proxy/configure_relay_proxy.md @@ -27,23 +27,24 @@ If you want to replace a nested fields, please use `_` to separate each field _( ::: -| Field name | Type | Default | Description | -|---------------------------|---------------------------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `retriever` | [retriever](#retriever) | **none** | **(mandatory)** This is the configuration on how to retrieve the configuration of the files.

_Note: this field is mandatory only if `retrievers` is not set._ | | -| `retrievers` | [[]retriever](#retriever) | **none** | **(mandatory)** Exactly the same things as `retriever` except that you can provide more than 1 retriever.

_Note: this field is mandatory only if `retriever` is not set._ | -| `listen` | int | `1031` | This is the port used by the relay proxy when starting the HTTP server. | -| `pollingInterval` | int | `60000` | This is the time interval **in millisecond** when the relay proxy is reloading the configuration file.
The minimum time accepted is 1000 millisecond. | -| `enablePollingJitter` | boolean | false | Set to true if you want to avoid having true periodicity when retrieving your flags. It is useful to avoid having spike on your flag configuration storage in case your application is starting multiple instance at the same time.
We ensure a deviation that is maximum + or - 10% of your polling interval.
Default: false | -| `hideBanner` | boolean | `false` | Should we display the beautiful **go-feature-flag** banner when starting the relay proxy | -| `enableSwagger` | boolean | `false` | Do you want to enable swagger to test the APIs directly. If you are enabling Swagger you will have to provide the `host` configuration and the Swagger UI will be available at `http://:/swagger/`. | -| `host` | string | `localhost` | This is the DNS you will use to access the relay proxy. This field is used by Swagger to query the API at the right place. | -| `restApiTimeout` | int | `5000` | Timeout in millisecond we are accepting to wait in our APIs. | -| `debug` | boolean | `false` | If `true` you will have more logs in the output that will help you to better understand what happen. If an error happen in the API the error will be also shown in the body. | -| `fileFormat` | string | `yaml` | This is the format of your `go-feature-flag` configuration file. Acceptable values are `yaml`, `json`, `toml`. | -| `startWithRetrieverError` | boolean | `false` | By default the **relay proxy** will crash if he is not able to retrieve the flags from the configuration.
If you don't want your relay proxy to crash, you can set `startWithRetrieverError` to true. Until the flag is retrievable the relay proxy will only answer with default values. | -| `exporter` | [exporter](#exporter) | **none** | Exporter is the configuration on how to export data. | -| `notifier` | [notifier](#notifier) | **none** | Notifiers is the configuration on where to notify a flag change. | -| `apiKeys` | []string | **none** | List of authorized API keys. Each request will need to provide one of authorized key inside `Authorization` header with format `Bearer `.

_Note: there will be no authorization when this config is not set._ | +| Field name | Type | Default | Description | +|-------------------------------|---------------------------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `retriever` | [retriever](#retriever) | **none** | **(mandatory)** This is the configuration on how to retrieve the configuration of the files.

_Note: this field is mandatory only if `retrievers` is not set._ | | +| `retrievers` | [[]retriever](#retriever) | **none** | **(mandatory)** Exactly the same things as `retriever` except that you can provide more than 1 retriever.

_Note: this field is mandatory only if `retriever` is not set._ | +| `listen` | int | `1031` | This is the port used by the relay proxy when starting the HTTP server. | +| `pollingInterval` | int | `60000` | This is the time interval **in millisecond** when the relay proxy is reloading the configuration file.
The minimum time accepted is 1000 millisecond. | +| `enablePollingJitter` | boolean | false | Set to true if you want to avoid having true periodicity when retrieving your flags. It is useful to avoid having spike on your flag configuration storage in case your application is starting multiple instance at the same time.
We ensure a deviation that is maximum + or - 10% of your polling interval.
Default: false | +| `hideBanner` | boolean | `false` | Should we display the beautiful **go-feature-flag** banner when starting the relay proxy | +| `enableSwagger` | boolean | `false` | Do you want to enable swagger to test the APIs directly. If you are enabling Swagger you will have to provide the `host` configuration and the Swagger UI will be available at `http://:/swagger/`. | +| `host` | string | `localhost` | This is the DNS you will use to access the relay proxy. This field is used by Swagger to query the API at the right place. | +| `restApiTimeout` | int | `5000` | Timeout in millisecond we are accepting to wait in our APIs. | +| `debug` | boolean | `false` | If `true` you will have more logs in the output that will help you to better understand what happen. If an error happen in the API the error will be also shown in the body. | +| `fileFormat` | string | `yaml` | This is the format of your `go-feature-flag` configuration file. Acceptable values are `yaml`, `json`, `toml`. | +| `startWithRetrieverError` | boolean | `false` | By default the **relay proxy** will crash if he is not able to retrieve the flags from the configuration.
If you don't want your relay proxy to crash, you can set `startWithRetrieverError` to true. Until the flag is retrievable the relay proxy will only answer with default values. | +| `exporter` | [exporter](#exporter) | **none** | Exporter is the configuration on how to export data. | +| `notifier` | [notifier](#notifier) | **none** | Notifiers is the configuration on where to notify a flag change. | +| `apiKeys` | []string | **none** | List of authorized API keys. Each request will need to provide one of authorized key inside `Authorization` header with format `Bearer `.

_Note: there will be no authorization when this config is not set._ | +| `evaluationContextEnrichment` | Object | **none** | It is a free field that will be merged with the evaluation context sent during the evaluation. It is useful to add common attributes to all the evaluations, such as a server version, environment, ...

All those fields will be included in the custom attributes of the evaluation context.

If in the evaluation context you have a field with the same name, it will be overriden by the `evaluationContextEnrichment`. | @@ -55,23 +56,23 @@ In this section we will present all the available retriever configuration availa ### S3 -| Field name | Type | Default | Description | -|------------|--------|----------|--------------------------------------------------------------------------------------------------------------------| +| Field name | Type | Default | Description | +|------------|--------|----------|---------------------------------------------------------------------------------------------------------------------| | `kind` | string | **none** | **(mandatory)** Value should be **`s3`**.
_This field is mandatory and describe which retriever you are using._ | -| `bucket` | string | **none** | **(mandatory)** This is the name of your S3 bucket _(ex: `my-featureflag-bucket`)_. | -| `item` | string | **none** | **(mandatory)** Path to the file inside the bucket _(ex: `config/flag/my-flags.yaml`)_. | +| `bucket` | string | **none** | **(mandatory)** This is the name of your S3 bucket _(ex: `my-featureflag-bucket`)_. | +| `item` | string | **none** | **(mandatory)** Path to the file inside the bucket _(ex: `config/flag/my-flags.yaml`)_. | ### GitHub -| Field name | Type | Default | Description | -|------------------|--------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `kind` | string | **none** | **(mandatory)** Value should be **`github`**.
_This field is mandatory and describe which retriever you are using._ | -| `repositorySlug` | string | **none** | **(mandatory)** The repository slug of the GitHub repository where your file is located _(ex: `thomaspoignant/go-feature-flag`)_. | -| `path` | string | **none** | **(mandatory)** Path to the file inside the repository _(ex: `config/flag/my-flags.yaml`)_. | -| `branch` | string | `main` | The branch we should check in the repository. | +| Field name | Type | Default | Description | +|------------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `kind` | string | **none** | **(mandatory)** Value should be **`github`**.
_This field is mandatory and describe which retriever you are using._ | +| `repositorySlug` | string | **none** | **(mandatory)** The repository slug of the GitHub repository where your file is located _(ex: `thomaspoignant/go-feature-flag`)_. | +| `path` | string | **none** | **(mandatory)** Path to the file inside the repository _(ex: `config/flag/my-flags.yaml`)_. | +| `branch` | string | `main` | The branch we should check in the repository. | | `token` | string | **none** | Github token used to access a private repository, you need the repo permission ([how to create a GitHub token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token)). | -| `timeout` | string | `10000` | Timeout in millisecond used when calling GitHub. | +| `timeout` | string | `10000` | Timeout in millisecond used when calling GitHub. | ### GitLab