Skip to content

Commit 04649c5

Browse files
skyerustoddbaert
andauthored
feat: implemented structured logging (#54)
* feat: implemented structured logging Signed-off-by: Skye Gill <[email protected]> * Removed ambiguity from docs. Signed-off-by: Skye Gill <[email protected]> * Update README.md Co-authored-by: Todd Baert <[email protected]> Signed-off-by: Skye Gill <[email protected]> Signed-off-by: Skye Gill <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent b8383e1 commit 04649c5

File tree

11 files changed

+194
-42
lines changed

11 files changed

+194
-42
lines changed

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,35 @@ import (
2828

2929
func main() {
3030
openfeature.SetProvider(openfeature.NoopProvider{})
31-
client := openfeature.GetClient("app")
32-
value, err := client.BooleanValue("v2_enabled", false, nil)
31+
client := openfeature.NewClient("app")
32+
value, err := client.BooleanValue("v2_enabled", false, openfeature.EvaluationContext{}, openfeature.EvaluationOptions{})
3333
}
3434
```
3535

36+
## Configuration
37+
38+
### Logging
39+
40+
If not configured, the logger falls back to the standard Go log package at error level only.
41+
42+
In order to avoid coupling to any particular logging implementation the sdk uses the structured logging [logr](https://github.com/go-logr/logr)
43+
API. This allows integration to any package that implements the layer between their logger and this API.
44+
Thankfully there is already [integration implementations](https://github.com/go-logr/logr#implementations-non-exhaustive)
45+
for many of the popular logger packages.
46+
47+
```go
48+
var l logr.Logger
49+
l = integratedlogr.New() // replace with your chosen integrator
50+
51+
openfeature.SetLogger(l) // set the logger at global level
52+
53+
c := openfeature.NewClient("log").WithLogger(l) // set the logger at client level
54+
55+
```
56+
57+
[logr](https://github.com/go-logr/logr) uses incremental verbosity levels (akin to named levels but in integer form).
58+
The sdk logs `info` at level `0` and `debug` at level `1`. Errors are always logged.
59+
3660
## Development
3761

3862
### Installation and Dependencies

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ require (
66
github.com/golang/mock v1.6.0
77
golang.org/x/text v0.3.7
88
)
9+
10+
require github.com/go-logr/logr v1.2.3 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
2+
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
13
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
24
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
35
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=

pkg/openfeature/client.go

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package openfeature
33
import (
44
"errors"
55
"fmt"
6+
7+
"github.com/go-logr/logr"
68
)
79

810
// IClient defines the behaviour required of an openfeature client
@@ -38,6 +40,7 @@ type Client struct {
3840
metadata ClientMetadata
3941
hooks []Hook
4042
evaluationContext EvaluationContext
43+
logger logr.Logger
4144
}
4245

4346
// NewClient returns a new Client. Name is a unique identifier for this client
@@ -46,9 +49,16 @@ func NewClient(name string) *Client {
4649
metadata: ClientMetadata{name: name},
4750
hooks: []Hook{},
4851
evaluationContext: EvaluationContext{},
52+
logger: api.logger,
4953
}
5054
}
5155

56+
// WithLogger sets the logger of the client
57+
func (c *Client) WithLogger(l logr.Logger) *Client {
58+
c.logger = l
59+
return c
60+
}
61+
5262
// Metadata returns the client's metadata
5363
func (c Client) Metadata() ClientMetadata {
5464
return c.metadata
@@ -57,11 +67,15 @@ func (c Client) Metadata() ClientMetadata {
5767
// AddHooks appends to the client's collection of any previously added hooks
5868
func (c *Client) AddHooks(hooks ...Hook) {
5969
c.hooks = append(c.hooks, hooks...)
70+
c.logger.V(info).Info("appended hooks to client", "client", c.Metadata().name, "hooks", hooks)
6071
}
6172

6273
// SetEvaluationContext sets the client's evaluation context
6374
func (c *Client) SetEvaluationContext(evalCtx EvaluationContext) {
6475
c.evaluationContext = evalCtx
76+
c.logger.V(info).Info(
77+
"set client evaluation context", "client", c.Metadata().name, "evaluationContext", evalCtx,
78+
)
6579
}
6680

6781
// EvaluationContext returns the client's evaluation context
@@ -80,6 +94,18 @@ const (
8094
Object
8195
)
8296

97+
func (t Type) String() string {
98+
return typeToString[t]
99+
}
100+
101+
var typeToString = map[Type]string{
102+
Boolean: "bool",
103+
String: "string",
104+
Float: "float",
105+
Int: "int",
106+
Object: "object",
107+
}
108+
83109
type EvaluationDetails struct {
84110
FlagKey string
85111
FlagType Type
@@ -88,14 +114,20 @@ type EvaluationDetails struct {
88114

89115
// BooleanValue return boolean evaluation for flag
90116
func (c Client) BooleanValue(flag string, defaultValue bool, evalCtx EvaluationContext, options EvaluationOptions) (bool, error) {
117+
91118
evalDetails, err := c.evaluate(flag, Boolean, defaultValue, evalCtx, options)
92119
if err != nil {
93-
return defaultValue, fmt.Errorf("evaluate: %w", err)
120+
return defaultValue, err
94121
}
95122

96123
value, ok := evalDetails.Value.(bool)
97124
if !ok {
98-
return defaultValue, errors.New("evaluated value is not a boolean")
125+
err := errors.New("evaluated value is not a boolean")
126+
c.logger.Error(
127+
err, "invalid flag resolution type", "expectedType", "bool",
128+
"gotType", fmt.Sprintf("%T", evalDetails.Value),
129+
)
130+
return defaultValue, err
99131
}
100132

101133
return value, nil
@@ -105,12 +137,17 @@ func (c Client) BooleanValue(flag string, defaultValue bool, evalCtx EvaluationC
105137
func (c Client) StringValue(flag string, defaultValue string, evalCtx EvaluationContext, options EvaluationOptions) (string, error) {
106138
evalDetails, err := c.evaluate(flag, String, defaultValue, evalCtx, options)
107139
if err != nil {
108-
return defaultValue, fmt.Errorf("evaluate: %w", err)
140+
return defaultValue, err
109141
}
110142

111143
value, ok := evalDetails.Value.(string)
112144
if !ok {
113-
return defaultValue, errors.New("evaluated value is not a string")
145+
err := errors.New("evaluated value is not a string")
146+
c.logger.Error(
147+
err, "invalid flag resolution type", "expectedType", "string",
148+
"gotType", fmt.Sprintf("%T", evalDetails.Value),
149+
)
150+
return defaultValue, err
114151
}
115152

116153
return value, nil
@@ -120,12 +157,17 @@ func (c Client) StringValue(flag string, defaultValue string, evalCtx Evaluation
120157
func (c Client) FloatValue(flag string, defaultValue float64, evalCtx EvaluationContext, options EvaluationOptions) (float64, error) {
121158
evalDetails, err := c.evaluate(flag, Float, defaultValue, evalCtx, options)
122159
if err != nil {
123-
return defaultValue, fmt.Errorf("evaluate: %w", err)
160+
return defaultValue, err
124161
}
125162

126163
value, ok := evalDetails.Value.(float64)
127164
if !ok {
128-
return defaultValue, errors.New("evaluated value is not a float64")
165+
err := errors.New("evaluated value is not a float64")
166+
c.logger.Error(
167+
err, "invalid flag resolution type", "expectedType", "float64",
168+
"gotType", fmt.Sprintf("%T", evalDetails.Value),
169+
)
170+
return defaultValue, err
129171
}
130172

131173
return value, nil
@@ -135,12 +177,17 @@ func (c Client) FloatValue(flag string, defaultValue float64, evalCtx Evaluation
135177
func (c Client) IntValue(flag string, defaultValue int64, evalCtx EvaluationContext, options EvaluationOptions) (int64, error) {
136178
evalDetails, err := c.evaluate(flag, Int, defaultValue, evalCtx, options)
137179
if err != nil {
138-
return defaultValue, fmt.Errorf("evaluate: %w", err)
180+
return defaultValue, err
139181
}
140182

141183
value, ok := evalDetails.Value.(int64)
142184
if !ok {
143-
return defaultValue, errors.New("evaluated value is not an int64")
185+
err := errors.New("evaluated value is not an int64")
186+
c.logger.Error(
187+
err, "invalid flag resolution type", "expectedType", "int64",
188+
"gotType", fmt.Sprintf("%T", evalDetails.Value),
189+
)
190+
return defaultValue, err
144191
}
145192

146193
return value, nil
@@ -180,6 +227,10 @@ func (c Client) ObjectValueDetails(flag string, defaultValue interface{}, evalCt
180227
func (c Client) evaluate(
181228
flag string, flagType Type, defaultValue interface{}, evalCtx EvaluationContext, options EvaluationOptions,
182229
) (EvaluationDetails, error) {
230+
c.logger.V(debug).Info(
231+
"evaluating flag", "flag", flag, "type", flagType.String(), "defaultValue", defaultValue,
232+
"evaluationContext", evalCtx, "evaluationOptions", options,
233+
)
183234
evalCtx = mergeContexts(evalCtx, c.evaluationContext, api.evaluationContext) // API (global) -> client -> invocation
184235

185236
var err error
@@ -208,7 +259,11 @@ func (c Client) evaluate(
208259
evalCtx, err = c.beforeHooks(hookCtx, apiClientInvocationProviderHooks, evalCtx, options)
209260
hookCtx.evaluationContext = evalCtx
210261
if err != nil {
211-
err = fmt.Errorf("execute before hook: %w", err)
262+
c.logger.Error(
263+
err, "before hook", "flag", flag, "defaultValue", defaultValue,
264+
"evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(),
265+
)
266+
err = fmt.Errorf("before hook: %w", err)
212267
c.errorHooks(hookCtx, providerInvocationClientApiHooks, err, options)
213268
return evalDetails, err
214269
}
@@ -238,7 +293,11 @@ func (c Client) evaluate(
238293

239294
err = resolution.Error()
240295
if err != nil {
241-
err = fmt.Errorf("evaluate the flag: %w", err)
296+
c.logger.Error(
297+
err, "flag resolution", "flag", flag, "defaultValue", defaultValue,
298+
"evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), "errorCode", err,
299+
)
300+
err = fmt.Errorf("error code: %w", err)
242301
c.errorHooks(hookCtx, providerInvocationClientApiHooks, err, options)
243302
return evalDetails, err
244303
}
@@ -247,11 +306,16 @@ func (c Client) evaluate(
247306
}
248307

249308
if err := c.afterHooks(hookCtx, providerInvocationClientApiHooks, evalDetails, options); err != nil {
250-
err = fmt.Errorf("execute after hook: %w", err)
309+
c.logger.Error(
310+
err, "after hook", "flag", flag, "defaultValue", defaultValue,
311+
"evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(),
312+
)
313+
err = fmt.Errorf("after hook: %w", err)
251314
c.errorHooks(hookCtx, providerInvocationClientApiHooks, err, options)
252315
return evalDetails, err
253316
}
254317

318+
c.logger.V(debug).Info("evaluated flag", "flag", flag, "details", evalDetails, "type", flagType)
255319
return evalDetails, nil
256320
}
257321

@@ -269,6 +333,9 @@ func flattenContext(evalCtx EvaluationContext) map[string]interface{} {
269333
func (c Client) beforeHooks(
270334
hookCtx HookContext, hooks []Hook, evalCtx EvaluationContext, options EvaluationOptions,
271335
) (EvaluationContext, error) {
336+
c.logger.V(debug).Info("executing before hooks")
337+
defer c.logger.V(debug).Info("executed before hooks")
338+
272339
for _, hook := range hooks {
273340
resultEvalCtx, err := hook.Before(hookCtx, options.hookHints)
274341
if resultEvalCtx != nil {
@@ -285,6 +352,9 @@ func (c Client) beforeHooks(
285352
func (c Client) afterHooks(
286353
hookCtx HookContext, hooks []Hook, evalDetails EvaluationDetails, options EvaluationOptions,
287354
) error {
355+
c.logger.V(debug).Info("executing after hooks")
356+
defer c.logger.V(debug).Info("executed after hooks")
357+
288358
for _, hook := range hooks {
289359
if err := hook.After(hookCtx, evalDetails, options.hookHints); err != nil {
290360
return err
@@ -295,12 +365,18 @@ func (c Client) afterHooks(
295365
}
296366

297367
func (c Client) errorHooks(hookCtx HookContext, hooks []Hook, err error, options EvaluationOptions) {
368+
c.logger.V(debug).Info("executing error hooks")
369+
defer c.logger.V(debug).Info("executed error hooks")
370+
298371
for _, hook := range hooks {
299372
hook.Error(hookCtx, err, options.hookHints)
300373
}
301374
}
302375

303376
func (c Client) finallyHooks(hookCtx HookContext, hooks []Hook, options EvaluationOptions) {
377+
c.logger.V(debug).Info("executing finally hooks")
378+
defer c.logger.V(debug).Info("executed finally hooks")
379+
304380
for _, hook := range hooks {
305381
hook.Finally(hookCtx, options.hookHints)
306382
}

pkg/openfeature/client_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ func TestRequirement_1_4_9(t *testing.T) {
218218
defer t.Cleanup(initSingleton)
219219
mockProvider := NewMockFeatureProvider(ctrl)
220220
defaultValue := true
221-
mockProvider.EXPECT().Metadata().Times(2)
221+
mockProvider.EXPECT().Metadata().AnyTimes()
222222
mockProvider.EXPECT().Hooks().AnyTimes()
223223
mockProvider.EXPECT().BooleanEvaluation(flagKey, defaultValue, flatCtx).
224224
Return(BoolResolutionDetail{
@@ -254,7 +254,7 @@ func TestRequirement_1_4_9(t *testing.T) {
254254
defer t.Cleanup(initSingleton)
255255
mockProvider := NewMockFeatureProvider(ctrl)
256256
defaultValue := "default"
257-
mockProvider.EXPECT().Metadata().Times(2)
257+
mockProvider.EXPECT().Metadata().AnyTimes()
258258
mockProvider.EXPECT().Hooks().AnyTimes()
259259
mockProvider.EXPECT().StringEvaluation(flagKey, defaultValue, flatCtx).
260260
Return(StringResolutionDetail{
@@ -290,7 +290,7 @@ func TestRequirement_1_4_9(t *testing.T) {
290290
defer t.Cleanup(initSingleton)
291291
mockProvider := NewMockFeatureProvider(ctrl)
292292
defaultValue := 3.14159
293-
mockProvider.EXPECT().Metadata().Times(2)
293+
mockProvider.EXPECT().Metadata().AnyTimes()
294294
mockProvider.EXPECT().Hooks().AnyTimes()
295295
mockProvider.EXPECT().FloatEvaluation(flagKey, defaultValue, flatCtx).
296296
Return(FloatResolutionDetail{
@@ -326,7 +326,7 @@ func TestRequirement_1_4_9(t *testing.T) {
326326
defer t.Cleanup(initSingleton)
327327
mockProvider := NewMockFeatureProvider(ctrl)
328328
var defaultValue int64 = 3
329-
mockProvider.EXPECT().Metadata().Times(2)
329+
mockProvider.EXPECT().Metadata().AnyTimes()
330330
mockProvider.EXPECT().Hooks().AnyTimes()
331331
mockProvider.EXPECT().IntEvaluation(flagKey, defaultValue, flatCtx).
332332
Return(IntResolutionDetail{
@@ -365,7 +365,7 @@ func TestRequirement_1_4_9(t *testing.T) {
365365
foo string
366366
}
367367
defaultValue := obj{foo: "bar"}
368-
mockProvider.EXPECT().Metadata().Times(2)
368+
mockProvider.EXPECT().Metadata().AnyTimes()
369369
mockProvider.EXPECT().Hooks().AnyTimes()
370370
mockProvider.EXPECT().ObjectEvaluation(flagKey, defaultValue, flatCtx).
371371
Return(ResolutionDetail{
@@ -420,8 +420,8 @@ func TestClient_ProviderEvaluationReturnsUnexpectedType(t *testing.T) {
420420
defer t.Cleanup(initSingleton)
421421
ctrl := gomock.NewController(t)
422422
mockProvider := NewMockFeatureProvider(ctrl)
423+
mockProvider.EXPECT().Metadata().AnyTimes()
423424
SetProvider(mockProvider)
424-
mockProvider.EXPECT().Metadata()
425425
mockProvider.EXPECT().Hooks().AnyTimes()
426426
mockProvider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any()).
427427
Return(BoolResolutionDetail{ResolutionDetail: ResolutionDetail{Value: 3}})
@@ -436,8 +436,8 @@ func TestClient_ProviderEvaluationReturnsUnexpectedType(t *testing.T) {
436436
defer t.Cleanup(initSingleton)
437437
ctrl := gomock.NewController(t)
438438
mockProvider := NewMockFeatureProvider(ctrl)
439+
mockProvider.EXPECT().Metadata().AnyTimes()
439440
SetProvider(mockProvider)
440-
mockProvider.EXPECT().Metadata()
441441
mockProvider.EXPECT().Hooks().AnyTimes()
442442
mockProvider.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any()).
443443
Return(StringResolutionDetail{ResolutionDetail: ResolutionDetail{Value: 3}})
@@ -452,8 +452,8 @@ func TestClient_ProviderEvaluationReturnsUnexpectedType(t *testing.T) {
452452
defer t.Cleanup(initSingleton)
453453
ctrl := gomock.NewController(t)
454454
mockProvider := NewMockFeatureProvider(ctrl)
455+
mockProvider.EXPECT().Metadata().AnyTimes()
455456
SetProvider(mockProvider)
456-
mockProvider.EXPECT().Metadata()
457457
mockProvider.EXPECT().Hooks().AnyTimes()
458458
mockProvider.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any()).
459459
Return(FloatResolutionDetail{ResolutionDetail: ResolutionDetail{Value: false}})
@@ -468,8 +468,8 @@ func TestClient_ProviderEvaluationReturnsUnexpectedType(t *testing.T) {
468468
defer t.Cleanup(initSingleton)
469469
ctrl := gomock.NewController(t)
470470
mockProvider := NewMockFeatureProvider(ctrl)
471+
mockProvider.EXPECT().Metadata().AnyTimes()
471472
SetProvider(mockProvider)
472-
mockProvider.EXPECT().Metadata()
473473
mockProvider.EXPECT().Hooks().AnyTimes()
474474
mockProvider.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any()).
475475
Return(IntResolutionDetail{ResolutionDetail: ResolutionDetail{Value: false}})

pkg/openfeature/evaluation_context.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ package openfeature
33
// EvaluationContext
44
// https://github.com/open-feature/spec/blob/main/specification/evaluation-context/evaluation-context.md
55
type EvaluationContext struct {
6-
TargetingKey string // uniquely identifying the subject (end-user, or client service) of a flag evaluation
7-
Attributes map[string]interface{}
6+
TargetingKey string `json:"targetingKey"` // uniquely identifying the subject (end-user, or client service) of a flag evaluation
7+
Attributes map[string]interface{} `json:"attributes"`
88
}

0 commit comments

Comments
 (0)