Skip to content

Commit

Permalink
Support of evaluation context enrichment (#1285)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaspoignant authored Nov 20, 2023
1 parent 8ffeb58 commit b3874ea
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 114 deletions.
11 changes: 10 additions & 1 deletion cmd/relayproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,18 @@ 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.
// 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

// apiKeySet is the internal representation of the list of api keys configured
Expand Down
21 changes: 11 additions & 10 deletions cmd/relayproxy/service/gofeatureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ type Config struct {
// No notification will be sent neither.
// Default: false
Offline bool

// 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
EvaluationContextEnrichment map[string]interface{}
}

// GetRetrievers returns a retriever.Retriever configure with the retriever available in the config.
Expand All @@ -78,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)
Expand Down
1 change: 1 addition & 0 deletions examples/retriever_file/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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").
Expand Down
18 changes: 14 additions & 4 deletions internal/flag/context.go
Original file line number Diff line number Diff line change
@@ -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
// 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
EvaluationContextEnrichment map[string]interface{}

// DefaultSdkValue is the default value of the SDK when calling the variation.
DefaultSdkValue interface{}
}

func (s *Context) AddIntoEvaluationContextEnrichment(key string, value interface{}) {
if s.EvaluationContextEnrichment == nil {
s.EvaluationContextEnrichment = make(map[string]interface{})
}
s.EvaluationContextEnrichment[key] = value
}
57 changes: 57 additions & 0 deletions internal/flag/context_test.go
Original file line number Diff line number Diff line change
@@ -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])
})
}
}
5 changes: 3 additions & 2 deletions internal/flag/internal_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -63,8 +64,8 @@ func (f *InternalFlag) Value(
) (interface{}, ResolutionDetails) {
f.applyScheduledRolloutSteps()

if flagContext.Environment != "" {
evaluationCtx.AddCustomAttribute("env", flagContext.Environment)
if flagContext.EvaluationContextEnrichment != nil {
maps.Copy(evaluationCtx.GetCustom(), flagContext.EvaluationContextEnrichment)
}

if f.IsDisable() || f.isExperimentationOver() {
Expand Down
Loading

0 comments on commit b3874ea

Please sign in to comment.