From 04649c5b954531601dc3e8a474bbff66094d3b1c Mon Sep 17 00:00:00 2001 From: Skye Gill Date: Fri, 2 Sep 2022 10:13:42 +0100 Subject: [PATCH] feat: implemented structured logging (#54) * feat: implemented structured logging Signed-off-by: Skye Gill * Removed ambiguity from docs. Signed-off-by: Skye Gill * Update README.md Co-authored-by: Todd Baert Signed-off-by: Skye Gill Signed-off-by: Skye Gill Co-authored-by: Todd Baert --- README.md | 28 ++++++- go.mod | 2 + go.sum | 2 + pkg/openfeature/client.go | 98 +++++++++++++++++++--- pkg/openfeature/client_test.go | 18 ++-- pkg/openfeature/evaluation_context.go | 4 +- pkg/openfeature/evaluation_context_test.go | 2 +- pkg/openfeature/hooks_test.go | 32 +++---- pkg/openfeature/logger.go | 28 +++++++ pkg/openfeature/openfeature.go | 19 +++++ pkg/openfeature/openfeature_test.go | 3 +- 11 files changed, 194 insertions(+), 42 deletions(-) create mode 100644 pkg/openfeature/logger.go diff --git a/README.md b/README.md index 53ce8d8b..da37e5fe 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,35 @@ import ( func main() { openfeature.SetProvider(openfeature.NoopProvider{}) - client := openfeature.GetClient("app") - value, err := client.BooleanValue("v2_enabled", false, nil) + client := openfeature.NewClient("app") + value, err := client.BooleanValue("v2_enabled", false, openfeature.EvaluationContext{}, openfeature.EvaluationOptions{}) } ``` +## Configuration + +### Logging + +If not configured, the logger falls back to the standard Go log package at error level only. + +In order to avoid coupling to any particular logging implementation the sdk uses the structured logging [logr](https://github.com/go-logr/logr) +API. This allows integration to any package that implements the layer between their logger and this API. +Thankfully there is already [integration implementations](https://github.com/go-logr/logr#implementations-non-exhaustive) +for many of the popular logger packages. + +```go +var l logr.Logger +l = integratedlogr.New() // replace with your chosen integrator + +openfeature.SetLogger(l) // set the logger at global level + +c := openfeature.NewClient("log").WithLogger(l) // set the logger at client level + +``` + +[logr](https://github.com/go-logr/logr) uses incremental verbosity levels (akin to named levels but in integer form). +The sdk logs `info` at level `0` and `debug` at level `1`. Errors are always logged. + ## Development ### Installation and Dependencies diff --git a/go.mod b/go.mod index c1148a7c..b84aa890 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,5 @@ require ( github.com/golang/mock v1.6.0 golang.org/x/text v0.3.7 ) + +require github.com/go-logr/logr v1.2.3 // indirect diff --git a/go.sum b/go.sum index 71ba6cfb..c17ed87a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= diff --git a/pkg/openfeature/client.go b/pkg/openfeature/client.go index eeb495c4..83b03e2a 100644 --- a/pkg/openfeature/client.go +++ b/pkg/openfeature/client.go @@ -3,6 +3,8 @@ package openfeature import ( "errors" "fmt" + + "github.com/go-logr/logr" ) // IClient defines the behaviour required of an openfeature client @@ -38,6 +40,7 @@ type Client struct { metadata ClientMetadata hooks []Hook evaluationContext EvaluationContext + logger logr.Logger } // NewClient returns a new Client. Name is a unique identifier for this client @@ -46,9 +49,16 @@ func NewClient(name string) *Client { metadata: ClientMetadata{name: name}, hooks: []Hook{}, evaluationContext: EvaluationContext{}, + logger: api.logger, } } +// WithLogger sets the logger of the client +func (c *Client) WithLogger(l logr.Logger) *Client { + c.logger = l + return c +} + // Metadata returns the client's metadata func (c Client) Metadata() ClientMetadata { return c.metadata @@ -57,11 +67,15 @@ func (c Client) Metadata() ClientMetadata { // AddHooks appends to the client's collection of any previously added hooks func (c *Client) AddHooks(hooks ...Hook) { c.hooks = append(c.hooks, hooks...) + c.logger.V(info).Info("appended hooks to client", "client", c.Metadata().name, "hooks", hooks) } // SetEvaluationContext sets the client's evaluation context func (c *Client) SetEvaluationContext(evalCtx EvaluationContext) { c.evaluationContext = evalCtx + c.logger.V(info).Info( + "set client evaluation context", "client", c.Metadata().name, "evaluationContext", evalCtx, + ) } // EvaluationContext returns the client's evaluation context @@ -80,6 +94,18 @@ const ( Object ) +func (t Type) String() string { + return typeToString[t] +} + +var typeToString = map[Type]string{ + Boolean: "bool", + String: "string", + Float: "float", + Int: "int", + Object: "object", +} + type EvaluationDetails struct { FlagKey string FlagType Type @@ -88,14 +114,20 @@ type EvaluationDetails struct { // BooleanValue return boolean evaluation for flag func (c Client) BooleanValue(flag string, defaultValue bool, evalCtx EvaluationContext, options EvaluationOptions) (bool, error) { + evalDetails, err := c.evaluate(flag, Boolean, defaultValue, evalCtx, options) if err != nil { - return defaultValue, fmt.Errorf("evaluate: %w", err) + return defaultValue, err } value, ok := evalDetails.Value.(bool) if !ok { - return defaultValue, errors.New("evaluated value is not a boolean") + err := errors.New("evaluated value is not a boolean") + c.logger.Error( + err, "invalid flag resolution type", "expectedType", "bool", + "gotType", fmt.Sprintf("%T", evalDetails.Value), + ) + return defaultValue, err } return value, nil @@ -105,12 +137,17 @@ func (c Client) BooleanValue(flag string, defaultValue bool, evalCtx EvaluationC func (c Client) StringValue(flag string, defaultValue string, evalCtx EvaluationContext, options EvaluationOptions) (string, error) { evalDetails, err := c.evaluate(flag, String, defaultValue, evalCtx, options) if err != nil { - return defaultValue, fmt.Errorf("evaluate: %w", err) + return defaultValue, err } value, ok := evalDetails.Value.(string) if !ok { - return defaultValue, errors.New("evaluated value is not a string") + err := errors.New("evaluated value is not a string") + c.logger.Error( + err, "invalid flag resolution type", "expectedType", "string", + "gotType", fmt.Sprintf("%T", evalDetails.Value), + ) + return defaultValue, err } return value, nil @@ -120,12 +157,17 @@ func (c Client) StringValue(flag string, defaultValue string, evalCtx Evaluation func (c Client) FloatValue(flag string, defaultValue float64, evalCtx EvaluationContext, options EvaluationOptions) (float64, error) { evalDetails, err := c.evaluate(flag, Float, defaultValue, evalCtx, options) if err != nil { - return defaultValue, fmt.Errorf("evaluate: %w", err) + return defaultValue, err } value, ok := evalDetails.Value.(float64) if !ok { - return defaultValue, errors.New("evaluated value is not a float64") + err := errors.New("evaluated value is not a float64") + c.logger.Error( + err, "invalid flag resolution type", "expectedType", "float64", + "gotType", fmt.Sprintf("%T", evalDetails.Value), + ) + return defaultValue, err } return value, nil @@ -135,12 +177,17 @@ func (c Client) FloatValue(flag string, defaultValue float64, evalCtx Evaluation func (c Client) IntValue(flag string, defaultValue int64, evalCtx EvaluationContext, options EvaluationOptions) (int64, error) { evalDetails, err := c.evaluate(flag, Int, defaultValue, evalCtx, options) if err != nil { - return defaultValue, fmt.Errorf("evaluate: %w", err) + return defaultValue, err } value, ok := evalDetails.Value.(int64) if !ok { - return defaultValue, errors.New("evaluated value is not an int64") + err := errors.New("evaluated value is not an int64") + c.logger.Error( + err, "invalid flag resolution type", "expectedType", "int64", + "gotType", fmt.Sprintf("%T", evalDetails.Value), + ) + return defaultValue, err } return value, nil @@ -180,6 +227,10 @@ func (c Client) ObjectValueDetails(flag string, defaultValue interface{}, evalCt func (c Client) evaluate( flag string, flagType Type, defaultValue interface{}, evalCtx EvaluationContext, options EvaluationOptions, ) (EvaluationDetails, error) { + c.logger.V(debug).Info( + "evaluating flag", "flag", flag, "type", flagType.String(), "defaultValue", defaultValue, + "evaluationContext", evalCtx, "evaluationOptions", options, + ) evalCtx = mergeContexts(evalCtx, c.evaluationContext, api.evaluationContext) // API (global) -> client -> invocation var err error @@ -208,7 +259,11 @@ func (c Client) evaluate( evalCtx, err = c.beforeHooks(hookCtx, apiClientInvocationProviderHooks, evalCtx, options) hookCtx.evaluationContext = evalCtx if err != nil { - err = fmt.Errorf("execute before hook: %w", err) + c.logger.Error( + err, "before hook", "flag", flag, "defaultValue", defaultValue, + "evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), + ) + err = fmt.Errorf("before hook: %w", err) c.errorHooks(hookCtx, providerInvocationClientApiHooks, err, options) return evalDetails, err } @@ -238,7 +293,11 @@ func (c Client) evaluate( err = resolution.Error() if err != nil { - err = fmt.Errorf("evaluate the flag: %w", err) + c.logger.Error( + err, "flag resolution", "flag", flag, "defaultValue", defaultValue, + "evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), "errorCode", err, + ) + err = fmt.Errorf("error code: %w", err) c.errorHooks(hookCtx, providerInvocationClientApiHooks, err, options) return evalDetails, err } @@ -247,11 +306,16 @@ func (c Client) evaluate( } if err := c.afterHooks(hookCtx, providerInvocationClientApiHooks, evalDetails, options); err != nil { - err = fmt.Errorf("execute after hook: %w", err) + c.logger.Error( + err, "after hook", "flag", flag, "defaultValue", defaultValue, + "evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), + ) + err = fmt.Errorf("after hook: %w", err) c.errorHooks(hookCtx, providerInvocationClientApiHooks, err, options) return evalDetails, err } + c.logger.V(debug).Info("evaluated flag", "flag", flag, "details", evalDetails, "type", flagType) return evalDetails, nil } @@ -269,6 +333,9 @@ func flattenContext(evalCtx EvaluationContext) map[string]interface{} { func (c Client) beforeHooks( hookCtx HookContext, hooks []Hook, evalCtx EvaluationContext, options EvaluationOptions, ) (EvaluationContext, error) { + c.logger.V(debug).Info("executing before hooks") + defer c.logger.V(debug).Info("executed before hooks") + for _, hook := range hooks { resultEvalCtx, err := hook.Before(hookCtx, options.hookHints) if resultEvalCtx != nil { @@ -285,6 +352,9 @@ func (c Client) beforeHooks( func (c Client) afterHooks( hookCtx HookContext, hooks []Hook, evalDetails EvaluationDetails, options EvaluationOptions, ) error { + c.logger.V(debug).Info("executing after hooks") + defer c.logger.V(debug).Info("executed after hooks") + for _, hook := range hooks { if err := hook.After(hookCtx, evalDetails, options.hookHints); err != nil { return err @@ -295,12 +365,18 @@ func (c Client) afterHooks( } func (c Client) errorHooks(hookCtx HookContext, hooks []Hook, err error, options EvaluationOptions) { + c.logger.V(debug).Info("executing error hooks") + defer c.logger.V(debug).Info("executed error hooks") + for _, hook := range hooks { hook.Error(hookCtx, err, options.hookHints) } } func (c Client) finallyHooks(hookCtx HookContext, hooks []Hook, options EvaluationOptions) { + c.logger.V(debug).Info("executing finally hooks") + defer c.logger.V(debug).Info("executed finally hooks") + for _, hook := range hooks { hook.Finally(hookCtx, options.hookHints) } diff --git a/pkg/openfeature/client_test.go b/pkg/openfeature/client_test.go index 749cc360..bf2e2542 100644 --- a/pkg/openfeature/client_test.go +++ b/pkg/openfeature/client_test.go @@ -218,7 +218,7 @@ func TestRequirement_1_4_9(t *testing.T) { defer t.Cleanup(initSingleton) mockProvider := NewMockFeatureProvider(ctrl) defaultValue := true - mockProvider.EXPECT().Metadata().Times(2) + mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() mockProvider.EXPECT().BooleanEvaluation(flagKey, defaultValue, flatCtx). Return(BoolResolutionDetail{ @@ -254,7 +254,7 @@ func TestRequirement_1_4_9(t *testing.T) { defer t.Cleanup(initSingleton) mockProvider := NewMockFeatureProvider(ctrl) defaultValue := "default" - mockProvider.EXPECT().Metadata().Times(2) + mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() mockProvider.EXPECT().StringEvaluation(flagKey, defaultValue, flatCtx). Return(StringResolutionDetail{ @@ -290,7 +290,7 @@ func TestRequirement_1_4_9(t *testing.T) { defer t.Cleanup(initSingleton) mockProvider := NewMockFeatureProvider(ctrl) defaultValue := 3.14159 - mockProvider.EXPECT().Metadata().Times(2) + mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() mockProvider.EXPECT().FloatEvaluation(flagKey, defaultValue, flatCtx). Return(FloatResolutionDetail{ @@ -326,7 +326,7 @@ func TestRequirement_1_4_9(t *testing.T) { defer t.Cleanup(initSingleton) mockProvider := NewMockFeatureProvider(ctrl) var defaultValue int64 = 3 - mockProvider.EXPECT().Metadata().Times(2) + mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() mockProvider.EXPECT().IntEvaluation(flagKey, defaultValue, flatCtx). Return(IntResolutionDetail{ @@ -365,7 +365,7 @@ func TestRequirement_1_4_9(t *testing.T) { foo string } defaultValue := obj{foo: "bar"} - mockProvider.EXPECT().Metadata().Times(2) + mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() mockProvider.EXPECT().ObjectEvaluation(flagKey, defaultValue, flatCtx). Return(ResolutionDetail{ @@ -420,8 +420,8 @@ func TestClient_ProviderEvaluationReturnsUnexpectedType(t *testing.T) { defer t.Cleanup(initSingleton) ctrl := gomock.NewController(t) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockProvider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any()). Return(BoolResolutionDetail{ResolutionDetail: ResolutionDetail{Value: 3}}) @@ -436,8 +436,8 @@ func TestClient_ProviderEvaluationReturnsUnexpectedType(t *testing.T) { defer t.Cleanup(initSingleton) ctrl := gomock.NewController(t) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockProvider.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any()). Return(StringResolutionDetail{ResolutionDetail: ResolutionDetail{Value: 3}}) @@ -452,8 +452,8 @@ func TestClient_ProviderEvaluationReturnsUnexpectedType(t *testing.T) { defer t.Cleanup(initSingleton) ctrl := gomock.NewController(t) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockProvider.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any()). Return(FloatResolutionDetail{ResolutionDetail: ResolutionDetail{Value: false}}) @@ -468,8 +468,8 @@ func TestClient_ProviderEvaluationReturnsUnexpectedType(t *testing.T) { defer t.Cleanup(initSingleton) ctrl := gomock.NewController(t) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockProvider.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any()). Return(IntResolutionDetail{ResolutionDetail: ResolutionDetail{Value: false}}) diff --git a/pkg/openfeature/evaluation_context.go b/pkg/openfeature/evaluation_context.go index 096314ca..074758f6 100644 --- a/pkg/openfeature/evaluation_context.go +++ b/pkg/openfeature/evaluation_context.go @@ -3,6 +3,6 @@ package openfeature // EvaluationContext // https://github.com/open-feature/spec/blob/main/specification/evaluation-context/evaluation-context.md type EvaluationContext struct { - TargetingKey string // uniquely identifying the subject (end-user, or client service) of a flag evaluation - Attributes map[string]interface{} + TargetingKey string `json:"targetingKey"` // uniquely identifying the subject (end-user, or client service) of a flag evaluation + Attributes map[string]interface{} `json:"attributes"` } diff --git a/pkg/openfeature/evaluation_context_test.go b/pkg/openfeature/evaluation_context_test.go index 3004ab3e..7af3a039 100644 --- a/pkg/openfeature/evaluation_context_test.go +++ b/pkg/openfeature/evaluation_context_test.go @@ -99,6 +99,7 @@ func TestRequirement_3_2_2(t *testing.T) { SetEvaluationContext(apiEvalCtx) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) client := NewClient("test") @@ -120,7 +121,6 @@ func TestRequirement_3_2_2(t *testing.T) { }, } - mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() expectedMergedEvalCtx := EvaluationContext{ TargetingKey: "Client", diff --git a/pkg/openfeature/hooks_test.go b/pkg/openfeature/hooks_test.go index 850149b7..ae275677 100644 --- a/pkg/openfeature/hooks_test.go +++ b/pkg/openfeature/hooks_test.go @@ -155,6 +155,7 @@ func TestRequirement_4_3_2(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) flagKey := "foo" @@ -163,7 +164,6 @@ func TestRequirement_4_3_2(t *testing.T) { flatCtx := flattenContext(evalCtx) evalOptions := NewEvaluationOptions([]Hook{mockHook}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() // assert that the Before hooks are executed prior to the flag evaluation @@ -198,6 +198,7 @@ func TestRequirement_4_3_3(t *testing.T) { ctrl := gomock.NewController(t) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) mockHook1 := NewMockHook(ctrl) mockHook2 := NewMockHook(ctrl) @@ -208,7 +209,6 @@ func TestRequirement_4_3_3(t *testing.T) { evalCtx := EvaluationContext{} evalOptions := NewEvaluationOptions([]Hook{mockHook1, mockHook2}, HookHints{}) - mockProvider.EXPECT().Metadata().Times(2) mockProvider.EXPECT().Hooks().AnyTimes() hook1Ctx := HookContext{ @@ -248,6 +248,7 @@ func TestRequirement_4_3_4(t *testing.T) { ctrl := gomock.NewController(t) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) mockHook := NewMockHook(ctrl) client := NewClient("test") @@ -280,7 +281,6 @@ func TestRequirement_4_3_4(t *testing.T) { } evalOptions := NewEvaluationOptions([]Hook{mockHook}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() hookEvalCtxResult := &EvaluationContext{ @@ -321,6 +321,7 @@ func TestRequirement_4_3_5(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) flagKey := "foo" @@ -329,7 +330,6 @@ func TestRequirement_4_3_5(t *testing.T) { flatCtx := flattenContext(evalCtx) evalOptions := NewEvaluationOptions([]Hook{mockHook}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockHook.EXPECT().Before(gomock.Any(), gomock.Any()) @@ -374,10 +374,10 @@ func TestRequirement_4_3_6(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) evalOptions := NewEvaluationOptions([]Hook{mockHook}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() // assert that the Error hooks are executed after the failed Before hooks @@ -395,10 +395,10 @@ func TestRequirement_4_3_6(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) evalOptions := NewEvaluationOptions([]Hook{mockHook}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockHook.EXPECT().Before(gomock.Any(), gomock.Any()) @@ -420,10 +420,10 @@ func TestRequirement_4_3_6(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) evalOptions := NewEvaluationOptions([]Hook{mockHook}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockHook.EXPECT().Before(gomock.Any(), gomock.Any()) @@ -468,10 +468,10 @@ func TestRequirement_4_3_7(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) evalOptions := NewEvaluationOptions([]Hook{mockHook}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() // assert that the Finally hook runs after the Before & After stages @@ -490,10 +490,10 @@ func TestRequirement_4_3_7(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) evalOptions := NewEvaluationOptions([]Hook{mockHook}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockHook.EXPECT().Before(gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) @@ -607,8 +607,8 @@ func TestRequirement_4_4_2(t *testing.T) { mockProviderHook := NewMockHook(ctrl) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(2) // before: API, Client, Invocation, Provider @@ -650,8 +650,8 @@ func TestRequirement_4_4_2(t *testing.T) { mockProviderHook := NewMockHook(ctrl) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(2) mockAPIHook.EXPECT().Before(gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) @@ -709,10 +709,10 @@ func TestRequirement_4_4_6(t *testing.T) { mockHook2 := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) evalOptions := NewEvaluationOptions([]Hook{mockHook1, mockHook2}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockHook1.EXPECT().Before(gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) @@ -736,10 +736,10 @@ func TestRequirement_4_4_6(t *testing.T) { mockHook2 := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) evalOptions := NewEvaluationOptions([]Hook{mockHook1, mockHook2}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockHook1.EXPECT().Before(gomock.Any(), gomock.Any()) @@ -773,10 +773,10 @@ func TestRequirement_4_4_7(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) evalOptions := NewEvaluationOptions([]Hook{mockHook}, HookHints{}) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() mockHook.EXPECT().Before(gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) @@ -813,8 +813,8 @@ func TestRequirement_4_5_2(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() hookHints := NewHookHints(map[string]interface{}{"foo": "bar"}) @@ -837,8 +837,8 @@ func TestRequirement_4_5_2(t *testing.T) { mockHook := NewMockHook(ctrl) client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) - mockProvider.EXPECT().Metadata() mockProvider.EXPECT().Hooks().AnyTimes() hookHints := NewHookHints(map[string]interface{}{"foo": "bar"}) diff --git a/pkg/openfeature/logger.go b/pkg/openfeature/logger.go new file mode 100644 index 00000000..575b7386 --- /dev/null +++ b/pkg/openfeature/logger.go @@ -0,0 +1,28 @@ +package openfeature + +import ( + "log" + + "github.com/go-logr/logr" +) + +const ( + info = 0 + debug = 1 +) + +// logger is the sdk's default logger +// logs using the standard log package on error, all other logs are no-ops +type logger struct{} + +func (l logger) Init(info logr.RuntimeInfo) {} + +func (l logger) Enabled(level int) bool { return true } + +func (l logger) Info(level int, msg string, keysAndValues ...interface{}) {} + +func (l logger) Error(err error, msg string, keysAndValues ...interface{}) { log.Println(err) } + +func (l logger) WithValues(keysAndValues ...interface{}) logr.LogSink { return l } + +func (l logger) WithName(name string) logr.LogSink { return l } diff --git a/pkg/openfeature/openfeature.go b/pkg/openfeature/openfeature.go index 67a548e2..0e979c68 100644 --- a/pkg/openfeature/openfeature.go +++ b/pkg/openfeature/openfeature.go @@ -2,12 +2,15 @@ package openfeature import ( "sync" + + "github.com/go-logr/logr" ) type evaluationAPI struct { provider FeatureProvider hooks []Hook evaluationContext EvaluationContext + logger logr.Logger sync.RWMutex } @@ -24,6 +27,7 @@ func initSingleton() { provider: NoopProvider{}, hooks: []Hook{}, evaluationContext: EvaluationContext{}, + logger: logr.New(logger{}), } } @@ -55,12 +59,21 @@ func (api *evaluationAPI) setProvider(provider FeatureProvider) { api.Lock() defer api.Unlock() api.provider = provider + api.logger.V(info).Info("set global provider", "name", provider.Metadata().Name) } func (api *evaluationAPI) setEvaluationContext(evalCtx EvaluationContext) { api.Lock() defer api.Unlock() api.evaluationContext = evalCtx + api.logger.V(info).Info("set global evaluation context", "evaluationContext", evalCtx) +} + +func (api *evaluationAPI) setLogger(l logr.Logger) { + api.Lock() + defer api.Unlock() + api.logger = l + api.logger.V(info).Info("set global logger") } // SetProvider sets the global provider. @@ -73,6 +86,11 @@ func SetEvaluationContext(evalCtx EvaluationContext) { api.setEvaluationContext(evalCtx) } +// SetLogger sets the global logger. +func SetLogger(l logr.Logger) { + api.setLogger(l) +} + // ProviderMetadata returns the global provider's metadata func ProviderMetadata() Metadata { return api.provider.Metadata() @@ -83,4 +101,5 @@ func AddHooks(hooks ...Hook) { api.Lock() defer api.Unlock() api.hooks = append(api.hooks, hooks...) + api.logger.V(info).Info("appended hooks to the global singleton", "hooks", hooks) } diff --git a/pkg/openfeature/openfeature_test.go b/pkg/openfeature/openfeature_test.go index 11e8b608..2c7bb6c8 100644 --- a/pkg/openfeature/openfeature_test.go +++ b/pkg/openfeature/openfeature_test.go @@ -14,6 +14,7 @@ func TestRequirement_1_1_1(t *testing.T) { ctrl := gomock.NewController(t) mockProvider := NewMockFeatureProvider(ctrl) + mockProvider.EXPECT().Metadata().AnyTimes() SetProvider(mockProvider) if api.provider != mockProvider { @@ -29,8 +30,8 @@ func TestRequirement_1_1_2(t *testing.T) { mockProvider := NewMockFeatureProvider(ctrl) mockProviderName := "mock-provider" + mockProvider.EXPECT().Metadata().Return(Metadata{Name: mockProviderName}).AnyTimes() SetProvider(mockProvider) - mockProvider.EXPECT().Metadata().Return(Metadata{Name: mockProviderName}).Times(2) if ProviderMetadata() != mockProvider.Metadata() { t.Error("globally set provider's metadata doesn't match the mock provider's metadata")