Repository: open-feature/go-sdk
Pull request #289 (2024-10-03 09:56:35)

feat: add logging hook, rm logging from evaluation
Implements:  [[FEATURE] Implement Logging Hook](https://github.com/open-feature/go-sdk/issues/284#top)

This PR removes logging from the main evaluation code path, with a few exceptions, like unhandled errors in event handlers. These cases are printed with default _slog_.
It also adds a LoggingHook as per the spec link above, using [slog](https://pkg.go.dev/log/slog).

_logr_ usage is removed, but exported APIs remains for backward compatibility.

[Java implementation](https://github.com/open-feature/java-sdk/pull/1084)

Closes: https://github.com/open-feature/go-sdk/issues/284

@toddbaert @Kavindu-Dodan 


toddbaert: @liran2000 sorry I missed this! I will review today. Looks like there's some CI failures though.
liran2000: > @liran2000 sorry I missed this! I will review today. Looks like there's some CI failures though.

Thanks, this PR requires GO version to be upgraded to 1.21 before proceeding, hence the failures. 
toddbaert: I've opened: https://github.com/open-feature/go-sdk/pull/294
toddbaert: @liran2000 I've merged the Go 1.2.1 change, there's a few lint/CI failures.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/open-feature/go-sdk/pull/289?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) Report
Attention: Patch coverage is `81.35593%` with `11 lines` in your changes missing coverage. Please review.
> Project coverage is 82.13%. Comparing base [(`ddfffdd`)](https://app.codecov.io/gh/open-feature/go-sdk/commit/ddfffddf52c1cbd48e0c40a828f33d3342824706?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) to head [(`be2d8bc`)](https://app.codecov.io/gh/open-feature/go-sdk/commit/be2d8bc06eef97461b79d5ca929d60f01187299d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature).

| [Files with missing lines](https://app.codecov.io/gh/open-feature/go-sdk/pull/289?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Patch % | Lines |
|---|---|---|
| [openfeature/hooks/logging\_hook.go](https://app.codecov.io/gh/open-feature/go-sdk/pull/289?src=pr&el=tree&filepath=openfeature%2Fhooks%2Flogging_hook.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-b3BlbmZlYXR1cmUvaG9va3MvbG9nZ2luZ19ob29rLmdv) | 81.63% | [6 Missing and 3 partials :warning: ](https://app.codecov.io/gh/open-feature/go-sdk/pull/289?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) |
| [openfeature/event\_executor.go](https://app.codecov.io/gh/open-feature/go-sdk/pull/289?src=pr&el=tree&filepath=openfeature%2Fevent_executor.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-b3BlbmZlYXR1cmUvZXZlbnRfZXhlY3V0b3IuZ28=) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/open-feature/go-sdk/pull/289?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) |
| [openfeature/openfeature.go](https://app.codecov.io/gh/open-feature/go-sdk/pull/289?src=pr&el=tree&filepath=openfeature%2Fopenfeature.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-b3BlbmZlYXR1cmUvb3BlbmZlYXR1cmUuZ28=) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/open-feature/go-sdk/pull/289?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #289      +/-   ##
==========================================
+ Coverage   81.89%   82.13%   +0.23%     
==========================================
  Files          10       11       +1     
  Lines        1215     1220       +5     
==========================================
+ Hits          995     1002       +7     
+ Misses        199      194       -5     
- Partials       21       24       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/open-feature/go-sdk/pull/289?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature).

toddbaert: Thanks again @liran2000 

Patch:
diff --git a/README.md b/README.md
index 019927b..02f7127 100644
--- a/README.md
+++ b/README.md
@@ -175,21 +175,39 @@ value, err := client.BooleanValue(
 
 ### Logging
 
-The standard Go log package is used by default to show error logs.
-This can be overridden using the structured logging, [logr](https://github.com/go-logr/logr) API, allowing integration to any package.
-There are already [integration implementations](https://github.com/go-logr/logr#implementations-non-exhaustive) for many of the popular logger packages.
+Note that in accordance with the OpenFeature specification, the SDK doesn't generally log messages during flag evaluation.
+
+#### Logging Hook
+
+The GO SDK includes a `LoggingHook`, which logs detailed information at key points during flag evaluation, using [slog](https://pkg.go.dev/log/slog) structured logging API.
+This hook can be particularly helpful for troubleshooting and debugging; simply attach it at the global, client or invocation level and ensure your log level is set to "debug".
+
+##### Usage example
 
 ```go
-var l logr.Logger
-l = integratedlogr.New() // replace with your chosen integrator
+// configure slog
+var programLevel = new(slog.LevelVar)
+programLevel.Set(slog.LevelDebug)
+h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})
+slog.SetDefault(slog.New(h))
+
+// add a hook globally, to run on all evaluations
+hook, err := NewLoggingHook(false)
+if err != nil {
+  // handle error
+}
 
-openfeature.SetLogger(l) // set the logger at global level
+openfeature.AddHooks(hook)
+
+client.BooleanValueDetails(context.Background(), "not-exist", true, openfeature.EvaluationContext{})
 
-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.
+###### Output
+> {"time":"2024-10-23T13:33:09.8870867+03:00","level":"DEBUG","msg":"Before stage","domain":"test-client","provider_name":"InMemoryProvider","flag_key":"not-exist","default_value":true}  
+{"time":"2024-10-23T13:33:09.8968242+03:00","level":"ERROR","msg":"Error stage","domain":"test-client","provider_name":"InMemoryProvider","flag_key":"not-exist","default_value":true,"error_message":"error code: FLAG_NOT_FOUND: flag for key not-exist not found"}
+
+See [hooks](#hooks) for more information on configuring hooks.
 
 ### Domains
 Clients can be assigned to a domain. A domain is a logical identifier which can be used to associate clients with a particular provider. If a domain has no associated provider, the default provider is used.
diff --git a/openfeature/client.go b/openfeature/client.go
index f66e192..d44bc43 100644
--- a/openfeature/client.go
+++ b/openfeature/client.go
@@ -41,7 +41,6 @@ type Client struct {
 	metadata          ClientMetadata
 	hooks             []Hook
 	evaluationContext EvaluationContext
-	logger            logr.Logger
 
 	mx sync.RWMutex
 }
@@ -52,25 +51,24 @@ var _ IClient = (*Client)(nil)
 // NewClient returns a new Client. Name is a unique identifier for this client
 // This helper exists for historical reasons. It is recommended to interact with IEvaluation to derive IClient instances.
 func NewClient(name string) *Client {
-	return newClient(name, api, eventing, logger)
+	return newClient(name, api, eventing)
 }
 
-func newClient(name string, apiRef evaluationImpl, eventRef clientEvent, log logr.Logger) *Client {
+func newClient(name string, apiRef evaluationImpl, eventRef clientEvent) *Client {
 	return &Client{
 		api:               apiRef,
 		clientEventing:    eventRef,
-		logger:            log,
 		metadata:          ClientMetadata{name: name},
 		hooks:             []Hook{},
 		evaluationContext: EvaluationContext{},
 	}
 }
 
+// Deprecated
 // WithLogger sets the logger of the client
 func (c *Client) WithLogger(l logr.Logger) *Client {
 	c.mx.Lock()
 	defer c.mx.Unlock()
-	c.logger = l
 	return c
 }
 
@@ -395,10 +393,6 @@ func (c *Client) BooleanValueDetails(ctx context.Context, flag string, defaultVa
 	value, ok := evalDetails.Value.(bool)
 	if !ok {
 		err := errors.New("evaluated value is not a boolean")
-		c.logger.Error(
-			err, "invalid flag resolution type", "expectedType", "boolean",
-			"gotType", fmt.Sprintf("%T", evalDetails.Value),
-		)
 		boolEvalDetails := BooleanEvaluationDetails{
 			Value:             defaultValue,
 			EvaluationDetails: evalDetails.EvaluationDetails,
@@ -443,10 +437,6 @@ func (c *Client) StringValueDetails(ctx context.Context, flag string, defaultVal
 	value, ok := evalDetails.Value.(string)
 	if !ok {
 		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),
-		)
 		strEvalDetails := StringEvaluationDetails{
 			Value:             defaultValue,
 			EvaluationDetails: evalDetails.EvaluationDetails,
@@ -491,10 +481,6 @@ func (c *Client) FloatValueDetails(ctx context.Context, flag string, defaultValu
 	value, ok := evalDetails.Value.(float64)
 	if !ok {
 		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),
-		)
 		floatEvalDetails := FloatEvaluationDetails{
 			Value:             defaultValue,
 			EvaluationDetails: evalDetails.EvaluationDetails,
@@ -539,10 +525,6 @@ func (c *Client) IntValueDetails(ctx context.Context, flag string, defaultValue
 	value, ok := evalDetails.Value.(int64)
 	if !ok {
 		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),
-		)
 		intEvalDetails := IntEvaluationDetails{
 			Value:             defaultValue,
 			EvaluationDetails: evalDetails.EvaluationDetails,
@@ -698,10 +680,6 @@ func (c *Client) evaluate(
 	evalCtx, err = c.beforeHooks(ctx, hookCtx, apiClientInvocationProviderHooks, evalCtx, options)
 	hookCtx.evaluationContext = evalCtx
 	if err != nil {
-		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(ctx, hookCtx, providerInvocationClientApiHooks, err, options)
 		return evalDetails, err
@@ -736,11 +714,6 @@ func (c *Client) evaluate(
 
 	err = resolution.Error()
 	if err != nil {
-		c.logger.Error(
-			err, "flag resolution", "flag", flag, "defaultValue", defaultValue,
-			"evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), "errorCode", err,
-			"errMessage", resolution.ResolutionError.message,
-		)
 		err = fmt.Errorf("error code: %w", err)
 		c.errorHooks(ctx, hookCtx, providerInvocationClientApiHooks, err, options)
 		evalDetails.ResolutionDetail = resolution.ResolutionDetail()
@@ -751,10 +724,6 @@ func (c *Client) evaluate(
 	evalDetails.ResolutionDetail = resolution.ResolutionDetail()
 
 	if err := c.afterHooks(ctx, hookCtx, providerInvocationClientApiHooks, evalDetails, options); err != nil {
-		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(ctx, hookCtx, providerInvocationClientApiHooks, err, options)
 		return evalDetails, err
diff --git a/openfeature/event_executor.go b/openfeature/event_executor.go
index 93d5db4..388cfb4 100644
--- a/openfeature/event_executor.go
+++ b/openfeature/event_executor.go
@@ -6,7 +6,8 @@ import (
 	"sync"
 	"time"
 
-	"github.com/go-logr/logr"
+	"log/slog"
+
 	"golang.org/x/exp/maps"
 )
 
@@ -37,19 +38,17 @@ type eventExecutor struct {
 	activeSubscriptions      []providerReference
 	apiRegistry              map[EventType][]EventCallback
 	scopedRegistry           map[string]scopedCallback
-	logger                   logr.Logger
 	eventChan                chan eventPayload
 	once                     sync.Once
 	mu                       sync.Mutex
 }
 
-func newEventExecutor(logger logr.Logger) *eventExecutor {
+func newEventExecutor() *eventExecutor {
 	executor := eventExecutor{
 		namedProviderReference: map[string]providerReference{},
 		activeSubscriptions:    []providerReference{},
 		apiRegistry:            map[EventType][]EventCallback{},
 		scopedRegistry:         map[string]scopedCallback{},
-		logger:                 logger,
 		eventChan:              make(chan eventPayload, 5),
 	}
 
@@ -87,14 +86,6 @@ type providerReference struct {
 	shutdownSemaphore chan interface{}
 }
 
-// updateLogger updates the executor's logger
-func (e *eventExecutor) updateLogger(l logr.Logger) {
-	e.mu.Lock()
-	defer e.mu.Unlock()
-
-	e.logger = l
-}
-
 // AddHandler adds an API(global) level handler
 func (e *eventExecutor) AddHandler(t EventType, c EventCallback) {
 	e.mu.Lock()
@@ -377,7 +368,7 @@ func (e *eventExecutor) executeHandler(f func(details EventDetails), event Event
 	go func() {
 		defer func() {
 			if r := recover(); r != nil {
-				e.logger.Info("recovered from a panic")
+				slog.Info("recovered from a panic")
 			}
 		}()
 
diff --git a/openfeature/hooks/logging_hook.go b/openfeature/hooks/logging_hook.go
new file mode 100644
index 0000000..0d08d66
--- /dev/null
+++ b/openfeature/hooks/logging_hook.go
@@ -0,0 +1,96 @@
+package hooks
+
+import (
+	"context"
+	"log/slog"
+
+	of "github.com/open-feature/go-sdk/openfeature"
+)
+
+const (
+	DOMAIN_KEY             = "domain"
+	PROVIDER_NAME_KEY      = "provider_name"
+	FLAG_KEY_KEY           = "flag_key"
+	DEFAULT_VALUE_KEY      = "default_value"
+	EVALUATION_CONTEXT_KEY = "evaluation_context"
+	ERROR_MESSAGE_KEY      = "error_message"
+	REASON_KEY             = "reason"
+	VARIANT_KEY            = "variant"
+	VALUE_KEY              = "value"
+)
+
+type LoggingHook struct {
+	includeEvaluationContext bool
+	logger                   *slog.Logger
+}
+
+func NewLoggingHook(includeEvaluationContext bool) (*LoggingHook, error) {
+	return NewCustomLoggingHook(includeEvaluationContext, slog.Default())
+}
+
+func NewCustomLoggingHook(includeEvaluationContext bool, logger *slog.Logger) (*LoggingHook, error) {
+	return &LoggingHook{
+		logger:                   logger,
+		includeEvaluationContext: includeEvaluationContext,
+	}, nil
+}
+
+type MarshaledEvaluationContext struct {
+	TargetingKey string
+	Attributes   map[string]interface{}
+}
+
+func (l LoggingHook) buildArgs(hookContext of.HookContext) ([]interface{}, error) {
+
+	args := []interface{}{
+		DOMAIN_KEY, hookContext.ClientMetadata().Domain(),
+		PROVIDER_NAME_KEY, hookContext.ProviderMetadata().Name,
+		FLAG_KEY_KEY, hookContext.FlagKey(),
+		DEFAULT_VALUE_KEY, hookContext.DefaultValue(),
+	}
+	if l.includeEvaluationContext {
+		marshaledEvaluationContext := MarshaledEvaluationContext{
+			TargetingKey: hookContext.EvaluationContext().TargetingKey(),
+			Attributes:   hookContext.EvaluationContext().Attributes(),
+		}
+		args = append(args, EVALUATION_CONTEXT_KEY, marshaledEvaluationContext)
+	}
+
+	return args, nil
+}
+
+func (h *LoggingHook) Before(ctx context.Context, hookContext of.HookContext,
+	hint of.HookHints) (*of.EvaluationContext, error) {
+	var args, err = h.buildArgs(hookContext)
+	if err != nil {
+		return nil, err
+	}
+	h.logger.Debug("Before stage", args...)
+	return nil, nil
+}
+
+func (h *LoggingHook) After(ctx context.Context, hookContext of.HookContext,
+	flagEvaluationDetails of.InterfaceEvaluationDetails, hookHints of.HookHints) error {
+	var args, err = h.buildArgs(hookContext)
+	if err != nil {
+		return err
+	}
+	args = append(args, REASON_KEY, flagEvaluationDetails.Reason)
+	args = append(args, VARIANT_KEY, flagEvaluationDetails.Variant)
+	args = append(args, VALUE_KEY, flagEvaluationDetails.Value)
+	h.logger.Debug("After stage", args...)
+	return nil
+}
+
+func (h *LoggingHook) Error(ctx context.Context, hookContext of.HookContext, err error, hint of.HookHints) {
+	args, buildArgsErr := h.buildArgs(hookContext)
+	if buildArgsErr != nil {
+		slog.Error("Error building args", "error", buildArgsErr)
+	}
+	args = append(args, ERROR_MESSAGE_KEY, err)
+	h.logger.Error("Error stage", args...)
+}
+
+func (h *LoggingHook) Finally(ctx context.Context, hCtx of.HookContext, hint of.HookHints) {
+
+}
diff --git a/openfeature/internal/logger.go b/openfeature/internal/logger.go
deleted file mode 100644
index 1b355f9..0000000
--- a/openfeature/internal/logger.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package internal
-
-import (
-	"log"
-
-	"github.com/go-logr/logr"
-)
-
-// Logger is the sdk's default logr.LogSink implementation.
-// Logs using this logger logs only 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("openfeature:", err)
-}
-
-func (l Logger) WithValues(keysAndValues ...interface{}) logr.LogSink { return l }
-
-func (l Logger) WithName(name string) logr.LogSink { return l }
diff --git a/openfeature/openfeature.go b/openfeature/openfeature.go
index 8455128..c6f297f 100644
--- a/openfeature/openfeature.go
+++ b/openfeature/openfeature.go
@@ -1,14 +1,10 @@
 package openfeature
 
-import (
-	"github.com/go-logr/logr"
-	"github.com/open-feature/go-sdk/openfeature/internal"
-)
+import "github.com/go-logr/logr"
 
 // api is the global evaluationImpl implementation. This is a singleton and there can only be one instance.
 var api evaluationImpl
 var eventing eventingImpl
-var logger logr.Logger
 
 // init initializes the OpenFeature evaluation API
 func init() {
@@ -16,12 +12,10 @@ func init() {
 }
 
 func initSingleton() {
-	logger = logr.New(internal.Logger{})
-
-	var exec = newEventExecutor(logger)
+	var exec = newEventExecutor()
 	eventing = exec
 
-	api = newEvaluationAPI(exec, logger)
+	api = newEvaluationAPI(exec)
 }
 
 // GetApiInstance returns the current singleton IEvaluation instance.
@@ -69,9 +63,9 @@ func SetEvaluationContext(evalCtx EvaluationContext) {
 	api.SetEvaluationContext(evalCtx)
 }
 
+// Deprecated
 // SetLogger sets the global Logger.
 func SetLogger(l logr.Logger) {
-	api.SetLogger(l)
 }
 
 // AddHooks appends to the collection of any previously added hooks
diff --git a/openfeature/openfeature_api.go b/openfeature/openfeature_api.go
index 4c6a5c1..0d854df 100644
--- a/openfeature/openfeature_api.go
+++ b/openfeature/openfeature_api.go
@@ -15,7 +15,10 @@ type evaluationImpl interface {
 	GetProvider() FeatureProvider
 	GetNamedProviders() map[string]FeatureProvider
 	GetHooks() []Hook
+
+	// Deprecated
 	SetLogger(l logr.Logger)
+
 	ForEvaluation(clientName string) (FeatureProvider, []Hook, EvaluationContext)
 }
 
@@ -26,18 +29,16 @@ type evaluationAPI struct {
 	hks             []Hook
 	apiCtx          EvaluationContext
 	eventExecutor   *eventExecutor
-	logger          logr.Logger
 	mu              sync.RWMutex
 }
 
 // newEvaluationAPI is a helper to generate an API. Used internally
-func newEvaluationAPI(eventExecutor *eventExecutor, log logr.Logger) *evaluationAPI {
+func newEvaluationAPI(eventExecutor *eventExecutor) *evaluationAPI {
 	return &evaluationAPI{
 		defaultProvider: NoopProvider{},
 		namedProviders:  map[string]FeatureProvider{},
 		hks:             []Hook{},
 		apiCtx:          EvaluationContext{},
-		logger:          log,
 		mu:              sync.RWMutex{},
 		eventExecutor:   eventExecutor,
 	}
@@ -109,12 +110,12 @@ func (api *evaluationAPI) GetNamedProviders() map[string]FeatureProvider {
 
 // GetClient returns a IClient bound to the default provider
 func (api *evaluationAPI) GetClient() IClient {
-	return newClient("", api, api.eventExecutor, api.logger)
+	return newClient("", api, api.eventExecutor)
 }
 
 // GetNamedClient returns a IClient bound to the given named provider
 func (api *evaluationAPI) GetNamedClient(clientName string) IClient {
-	return newClient(clientName, api, api.eventExecutor, api.logger)
+	return newClient(clientName, api, api.eventExecutor)
 }
 
 func (api *evaluationAPI) SetEvaluationContext(apiCtx EvaluationContext) {
@@ -124,12 +125,9 @@ func (api *evaluationAPI) SetEvaluationContext(apiCtx EvaluationContext) {
 	api.apiCtx = apiCtx
 }
 
+// Deprecated
 func (api *evaluationAPI) SetLogger(l logr.Logger) {
-	api.mu.Lock()
-	defer api.mu.Unlock()
 
-	api.logger = l
-	api.eventExecutor.updateLogger(l)
 }
 
 func (api *evaluationAPI) AddHooks(hooks ...Hook) {


Test patch:
diff --git a/openfeature/event_executor_test.go b/openfeature/event_executor_test.go
index 2d8339d..83f85b8 100644
--- a/openfeature/event_executor_test.go
+++ b/openfeature/event_executor_test.go
@@ -6,13 +6,11 @@ import (
 	"testing"
 	"time"
 
-	"github.com/go-logr/logr"
-	"github.com/open-feature/go-sdk/openfeature/internal"
 	"golang.org/x/exp/slices"
 )
 
 func init() {
-	logger = logr.New(internal.Logger{})
+
 }
 
 // Requirement 5.1.1 The provider MAY define a mechanism for signaling the occurrence of one of a set of events,
@@ -31,7 +29,7 @@ func TestEventHandler_RegisterUnregisterEventProvider(t *testing.T) {
 			eventingImpl,
 		}
 
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 		err := executor.registerDefaultProvider(eventingProvider)
 		if err != nil {
 			t.Fatal(err)
@@ -1173,7 +1171,7 @@ func TestEventHandler_1ToNMapping(t *testing.T) {
 			&ProviderEventing{},
 		}
 
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 
 		err := executor.registerDefaultProvider(eventingProvider)
 		if err != nil {
@@ -1215,7 +1213,7 @@ func TestEventHandler_1ToNMapping(t *testing.T) {
 			},
 		}
 
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 
 		err := executor.registerDefaultProvider(eventingProvider)
 		if err != nil {
@@ -1266,7 +1264,7 @@ func TestEventHandler_1ToNMapping(t *testing.T) {
 			},
 		}
 
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 
 		err := executor.registerNamedEventingProvider("clientA", eventingProvider)
 		if err != nil {
@@ -1317,7 +1315,7 @@ func TestEventHandler_1ToNMapping(t *testing.T) {
 			},
 		}
 
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 
 		err := executor.registerNamedEventingProvider("clientA", eventingProvider)
 		if err != nil {
@@ -1354,7 +1352,7 @@ func TestEventHandler_1ToNMapping(t *testing.T) {
 
 func TestEventHandler_Registration(t *testing.T) {
 	t.Run("API handlers", func(t *testing.T) {
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 
 		// Add multiple - ProviderReady
 		executor.AddHandler(ProviderReady, &h1)
@@ -1392,7 +1390,7 @@ func TestEventHandler_Registration(t *testing.T) {
 	})
 
 	t.Run("Client handlers", func(t *testing.T) {
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 
 		// Add multiple - client a
 		executor.AddClientHandler("a", ProviderReady, &h1)
@@ -1429,7 +1427,7 @@ func TestEventHandler_Registration(t *testing.T) {
 
 func TestEventHandler_APIRemoval(t *testing.T) {
 	t.Run("API level removal", func(t *testing.T) {
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 
 		// Add multiple - ProviderReady
 		executor.AddHandler(ProviderReady, &h1)
@@ -1482,7 +1480,7 @@ func TestEventHandler_APIRemoval(t *testing.T) {
 	})
 
 	t.Run("Client level removal", func(t *testing.T) {
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 
 		// Add multiple - client a
 		executor.AddClientHandler("a", ProviderReady, &h1)
@@ -1539,7 +1537,7 @@ func TestEventHandler_APIRemoval(t *testing.T) {
 	})
 
 	t.Run("remove handlers that were not added", func(t *testing.T) {
-		executor := newEventExecutor(logger)
+		executor := newEventExecutor()
 
 		// removal of non-added handlers shall not panic
 		executor.RemoveHandler(ProviderReady, &h1)
diff --git a/openfeature/hooks/logging_hook_test.go b/openfeature/hooks/logging_hook_test.go
new file mode 100644
index 0000000..a8b20a8
--- /dev/null
+++ b/openfeature/hooks/logging_hook_test.go
@@ -0,0 +1,237 @@
+package hooks
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"os"
+	"testing"
+
+	"log/slog"
+
+	"github.com/open-feature/go-sdk/openfeature"
+	"github.com/open-feature/go-sdk/openfeature/memprovider"
+)
+
+func TestCreateLoggingHookWithDefaultLoggerAndContextInclusion(t *testing.T) {
+	hook, err := NewLoggingHook(true)
+	if err != nil {
+		t.Fatalf("expected no error, got %v", err)
+	}
+	if hook == nil {
+		t.Fatal("expected a valid LoggingHook, got nil")
+	}
+}
+
+func TestLoggingHookInitializesCorrectly(t *testing.T) {
+	logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
+	hook, err := NewCustomLoggingHook(true, logger)
+	if err != nil {
+		t.Error("expected no error")
+	}
+
+	if hook.logger != logger {
+		t.Errorf("Expected logger to be %v, got %v", logger, hook.logger)
+	}
+
+	if !hook.includeEvaluationContext {
+		t.Errorf("Expected includeEvaluationContext to be true, got %v", hook.includeEvaluationContext)
+	}
+}
+
+func TestLoggingHookHandlesNilLoggerGracefully(t *testing.T) {
+	hook, err := NewCustomLoggingHook(false, nil)
+	if err != nil {
+		t.Error("expected no error")
+	}
+
+	if hook.logger != nil {
+		t.Errorf("Expected logger to be nil, got %v", hook.logger)
+	}
+
+	if hook.includeEvaluationContext {
+		t.Errorf("Expected includeEvaluationContext to be false, got %v", hook.includeEvaluationContext)
+	}
+}
+
+func TestLoggingHookLogsMessagesAsExpected(t *testing.T) {
+	var buf *bytes.Buffer = new(bytes.Buffer)
+	handler := slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
+	logger := slog.New(handler)
+
+	hook, err := NewCustomLoggingHook(false, logger)
+	if err != nil {
+		t.Error("expected no error")
+	}
+
+	// Check if resultMap contains all key-value pairs from expected
+	testLoggingHookLogsMessagesAsExpected(*hook, logger, t, buf)
+}
+
+func TestLoggingHookLogsMessagesAsExpectedIncludeEvaluationContext(t *testing.T) {
+	var buf *bytes.Buffer = new(bytes.Buffer)
+	handler := slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
+	logger := slog.New(handler)
+
+	hook, err := NewCustomLoggingHook(true, logger)
+	if err != nil {
+		t.Error("expected no error")
+	}
+
+	// Check if resultMap contains all key-value pairs from expected
+	testLoggingHookLogsMessagesAsExpected(*hook, logger, t, buf)
+}
+
+func testLoggingHookLogsMessagesAsExpected(hook LoggingHook, logger *slog.Logger, t *testing.T, buf *bytes.Buffer) {
+	if hook.logger != logger {
+		t.Errorf("Expected logger to be %v, got %v", logger, hook.logger)
+	}
+
+	memoryProvider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{
+		"boolFlag": {
+			Key:            "boolFlag",
+			State:          memprovider.Enabled,
+			DefaultVariant: "true",
+			Variants: map[string]interface{}{
+				"true":  true,
+				"false": false,
+			},
+			ContextEvaluator: nil,
+		},
+	})
+
+	err := openfeature.SetProviderAndWait(memoryProvider)
+	if err != nil {
+		t.Error("error setting provider", err)
+	}
+	openfeature.AddHooks(&hook)
+	client := openfeature.NewClient("test-app")
+
+	t.Run("test boolean success", func(t *testing.T) {
+		res, err := client.BooleanValue(
+			context.Background(),
+			"boolFlag",
+			false,
+			openfeature.NewEvaluationContext(
+				"target1",
+				map[string]interface{}{
+					"color": "green",
+				},
+			),
+		)
+		if err != nil {
+			t.Error("expected nil error")
+		}
+		if res != true {
+			t.Errorf("incorect evaluation, expected %t, got %t", true, res)
+		}
+
+		ms := prepareOutput(buf, t)
+
+		var expected = map[string]map[string]any{
+			"Before stage": {
+				"provider_name": "InMemoryProvider",
+				"domain":        "test-app",
+			},
+			"After stage": {
+				"provider_name": "InMemoryProvider",
+				"domain":        "test-app",
+				"flag_key":      "boolFlag",
+			},
+		}
+
+		compare(expected, ms, t, hook)
+	})
+
+	t.Run("test boolean error", func(t *testing.T) {
+		res, err := client.BooleanValue(
+			context.Background(),
+			"non-existing",
+			false,
+			openfeature.NewEvaluationContext(
+				"target1",
+				map[string]interface{}{
+					"color": "green",
+				},
+			),
+		)
+		if err == nil {
+			t.Error("expected error")
+		}
+		if res != false {
+			t.Errorf("incorect evaluation, expected %t, got %t", false, res)
+		}
+
+		ms := prepareOutput(buf, t)
+
+		var expected = map[string]map[string]any{
+			"Before stage": {
+				"provider_name": "InMemoryProvider",
+				"domain":        "test-app",
+			},
+			"Error stage": {
+				"provider_name": "InMemoryProvider",
+				"domain":        "test-app",
+				"flag_key":      "non-existing",
+			},
+		}
+
+		compare(expected, ms, t, hook)
+	})
+}
+
+func prepareOutput(buf *bytes.Buffer, t *testing.T) map[string]map[string]any {
+	var ms map[string]map[string]any = make(map[string]map[string]any)
+	for _, line := range bytes.Split(buf.Bytes(), []byte{'\n'}) {
+		if len(line) == 0 {
+			continue
+		}
+		var m map[string]any
+		if err := json.Unmarshal(line, &m); err != nil {
+			t.Fatal(err)
+		}
+		ms[m["msg"].(string)] = m
+	}
+	return ms
+}
+
+func compare(expected map[string]map[string]any, ms map[string]map[string]any, t *testing.T, hook LoggingHook) {
+	for key, expectedInnerMap := range expected {
+		resultInnerMap, exists := ms[key]
+		if !exists {
+			t.Errorf("Key %s not found in resultMap", key)
+			continue
+		}
+		for innerKey, expectedValue := range expectedInnerMap {
+			resultValue, exists := resultInnerMap[innerKey]
+			if !exists {
+				t.Errorf("Inner key %s not found in resultMap[%s]", innerKey, key)
+				continue
+			}
+			if resultValue != expectedValue {
+				t.Errorf("Value for resultMap[%s][%s] does not match. Expected %v, got %v", key, innerKey, expectedValue, resultValue)
+			}
+
+			if hook.includeEvaluationContext {
+				evaluationContext, exists := resultInnerMap[EVALUATION_CONTEXT_KEY]
+				if !exists {
+					t.Errorf("Inner key %s not found in resultMap[%s]", EVALUATION_CONTEXT_KEY, key)
+				}
+				attributes, attributesExists := evaluationContext.(map[string]any)["Attributes"]
+				if !attributesExists {
+					t.Errorf("attributes do not exist")
+				}
+				color, colorExists := attributes.(map[string]any)["color"]
+				if !colorExists {
+					t.Errorf("color not exist")
+				}
+				if color != "green" {
+					t.Errorf("expected green color in evaluationContext")
+				}
+				if evaluationContext.(map[string]any)["TargetingKey"] != "target1" {
+					t.Errorf("expected TargetingKey in evaluationContext")
+				}
+			}
+		}
+	}
+}
diff --git a/openfeature/openfeature_test.go b/openfeature/openfeature_test.go
index da81bf6..f01eacd 100644
--- a/openfeature/openfeature_test.go
+++ b/openfeature/openfeature_test.go
@@ -6,9 +6,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/go-logr/logr"
 	"github.com/golang/mock/gomock"
-	"github.com/open-feature/go-sdk/openfeature/internal"
 )
 
 // The `API`, and any state it maintains SHOULD exist as a global singleton,
@@ -720,18 +718,6 @@ func TestDefaultClientUsage(t *testing.T) {
 	}
 }
 
-// Ability to override default logger
-func TestLoggerOverride(t *testing.T) {
-	defer t.Cleanup(initSingleton)
-
-	newOverride := internal.Logger{}
-	SetLogger(logr.New(newOverride))
-
-	if !reflect.DeepEqual(logger.GetSink(), newOverride) {
-		t.Error("logger overriding failed")
-	}
-}
-
 // Nil providers are not accepted for default and named providers
 func TestForNilProviders(t *testing.T) {
 	defer t.Cleanup(initSingleton)