Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: logging support via slog #64

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 73 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
# Confidence OpenFeature Go Provider
# Confidence Go SDK

This repo contains the OpenFeature Go flag provider for [Confidence](https://confidence.spotify.com/).

## OpenFeature

Before starting to use the provider, it can be helpful to read through the general [OpenFeature docs](https://docs.openfeature.dev/)
and get familiar with the concepts.
This repo contains the [Confidence](https://confidence.spotify.com/) Go SDK.

## Adding the dependency
<!---x-release-please-start-version-->
Expand All @@ -15,6 +10,74 @@ require (
)
```
<!---x-release-please-end-->

## Creating and using the SDK

Below is an example for how to create an instance of the Confidence SDK, and then resolve a flag with a boolean attribute.

The SDK is configured via `SetAPIConfig(...)` and `*c.NewAPIConfig(...)`, with which you can set the api key for authentication.
Optionally, a custom resolve API url can be configured if, for example, the resolver service is running on a locally deployed side-car (`NewAPIConfigWithUrl(...)`).


You can retrieve properties on the flag variant using property dot notation, meaning `test-flag.boolean-key` will retrieve the attribute `boolean-key` on the flag `test-flag`.

You can also use only the flag name `test-flag` and retrieve all values as a map with `GetObjectFlag()`.

The flag's schema is validated against the requested data type, and if it doesn't match it will fall back to the default value.

```go
import (
c "github.com/spotify/confidence-sdk-go/pkg/confidence"
)

confidenceSdk := c.NewConfidenceBuilder().SetAPIConfig(*c.NewAPIConfig("clientSecret")).Build()

confidence.PutContext("targeting_key", "Random_targeting_key")
flagValue := confidence.GetBoolFlag(context.Background(), "test-flag.boolean-key", false).Value
// we can also pull flag values using a Confidence instance with extra context
confidence.WithContext(map[string]interface{}{
"Something": 343,
}).GetBoolFlag(context.Background(), "test-flag.boolean-key", false).Value
```

The flag will be applied immediately, meaning that Confidence will count the targeted user as having received the treatment once they have have been evaluated.

### Tracking

Confidence support event tracking through the SDK. The `Track()` function accepts an en event name and a map of arbitrary data connected to the event.
The current context will also be appended to the event data.

```go
wg := confidence.Track(context.Background(), "checkout-complete", map[string]interface{}{
"orderId": 1234,
"total": 100.0,
"items": []string{"item1", "item2"},
})
wg.Wait()
```

## Logging

Unless specifically configured using the `ConfidenceBuilder` `setLogger()` function; Confidence uses the default instance of [slog](https://pkg.go.dev/log/slog) for logging valuable information during runtime.
When getting started with Confidence, we suggest you configure [slog](https://pkg.go.dev/log/slog) to emit debug level information:
```go
// Set up the logger with the debug log level
var programLevel = new(slog.LevelVar)
programLevel.Set(slog.LevelDebug)
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: programLevel})
slog.SetDefault(slog.New(h))
```

## Demo app

To run the demo app, replace the `CLIENT_SECRET` with client secret setup in the
[Confidence](https://confidence.spotify.com/) console, the flags with existing flags and execute
the app with `cd demo && go run GoDemoApp.go`.

## Confidence OpenFeature Go Provider

The SDK can be combined with the [OpenFeature Go SDK](https://github.com/open-feature/go-sdk), the repo also contains an OpenFeature Provider. Before starting to use the provider, it can be helpful to read through the general [OpenFeature docs](https://docs.openfeature.dev/)
and get familiar with the concepts.
It's also important to add the underlying OpenFeature SDK dependency:
```
require (
Expand All @@ -27,10 +90,8 @@ require (
Below is an example for how to create a OpenFeature client using the Confidence flag provider, and then resolve
a flag with a boolean attribute.

The provider is configured via `NewAPIConfig(...)`, with which you can set the api key for authentication.
Optionally, a custom resolve API url can be configured if, for example, the resolver service is running on a locally deployed side-car (`NewAPIConfigWithUrl(...)`).

The flag will be applied immediately, meaning that Confidence will count the targeted user as having received the treatment.
The Provider constructor accepts a confidence instance: `NewFlagProvider(confidenceSdk)`, please refer to the previous sections
of this readme for more detailed information on how to set that up.

You can retrieve attributes on the flag variant using property dot notation, meaning `test-flag.boolean-key` will retrieve
the attribute `boolean-key` on the flag `test-flag`.
Expand All @@ -46,7 +107,7 @@ import (
p "github.com/spotify/confidence-sdk-go/pkg/provider"
)

confidenceSdk := c.NewConfidenceBuilder().SetAPIConfig(c.APIConfig{APIKey: "clientSecret"}).Build()
confidenceSdk := c.NewConfidenceBuilder().SetAPIConfig(*c.NewAPIConfig("clientSecret")).Build()
confidenceProvider := p.NewFlagProvider(confidenceSdk)


Expand All @@ -61,8 +122,4 @@ attributes["user_id"] = "user1"
boolValue, error := client.BooleanValue(context.Background(), "test-flag.boolean-key", false,
o.NewEvaluationContext("", attributes))
```
## Demo app

To run the demo app, replace the `CLIENT_SECRET` with client secret setup in the
[Confidence](https://confidence.spotify.com/) console, the flags with existing flags and execute
the app with `cd demo && go run GoDemoApp.go`.
32 changes: 25 additions & 7 deletions demo/GoDemoApp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,35 @@ package main
import (
"context"
"fmt"
"log/slog"
"os"

c "github.com/spotify/confidence-sdk-go/pkg/confidence"
)

func main() {
fmt.Println("Fetching the flags...")

confidence := c.NewConfidenceBuilder().SetAPIConfig(*c.NewAPIConfig("API_KEY")).Build()
targetingKey := "Random_targeting_key"
confidence.PutContext("targeting_key", targetingKey)
// Set up the logger with the debug log level
var programLevel = new(slog.LevelVar)
programLevel.Set(slog.LevelDebug)
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: programLevel})
slog.SetDefault(slog.New(h))

colorValue := confidence.GetStringFlag(context.Background(), "hawkflag.color", "defaultValue").Value
messageValue := confidence.GetStringFlag(context.Background(), "hawkflag.message", "defaultValue").Value
confidence := c.NewConfidenceBuilder().SetAPIConfig(*c.NewAPIConfig("CLIENT_SECRET")).Build()
confidence.PutContext("targeting_key", "Random_targeting_key")
withAddedContext := confidence.WithContext(map[string]interface{}{
"Something": 343,
})

// we can pull flag values using a Confidence instance with extra context
fmt.Println("Fetching the flags...")
colorValue := withAddedContext.GetStringFlag(context.Background(), "hawkflag.color", "defaultValue").Value
messageValue := withAddedContext.GetStringFlag(context.Background(), "hawkflag.message", "defaultValue").Value

colorYellow := "\033[33m"
colorGreen := "\033[32m"
colorRed := "\033[31m"
colorDefault := "\033[0m"

fmt.Println(" Color --> " + colorValue)

Expand All @@ -30,8 +43,13 @@ func main() {
default:
fmt.Println(colorRed, "Message --> "+messageValue)
}
fmt.Print(colorDefault, "")

wg := confidence.Track(context.Background(), "page-viewed", map[string]interface{}{})
wg := confidence.Track(context.Background(), "checkout-complete", map[string]interface{}{
"orderId": 1234,
"total": 100.0,
"items": []string{"item1", "item2"},
})
wg.Wait()
fmt.Println("Event sent")
}
4 changes: 3 additions & 1 deletion demo/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ go 1.22.2

require github.com/spotify/confidence-sdk-go v0.2.3

replace github.com/spotify/confidence-sdk-go => ../
require golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect

replace github.com/spotify/confidence-sdk-go => ../
7 changes: 1 addition & 6 deletions demo/go.sum
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spotify/confidence-sdk-go v0.2.2-0.20240523155258-4bbc177010cc h1:CyQdom204c6w4UjbT63Otk3axZff4mEbeVDF/snvfas=
github.com/spotify/confidence-sdk-go v0.2.2-0.20240523155258-4bbc177010cc/go.mod h1:TYBqx3F0AZO7HZF8Nkf2dWnvVHH91ICo0W6e9wmLsk4=
github.com/spotify/confidence-sdk-go v0.2.3 h1:vJJjWJo6qgpgX+Lg/uiWrtzRw+qaJhwIQRkxwrBJUCA=
github.com/spotify/confidence-sdk-go v0.2.3/go.mod h1:3MInYY3UiHaNToPlL0mTgbWjcwMMGV/4OfbWAiJ2JC0=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ go 1.19
require (
github.com/open-feature/go-sdk v1.10.0
github.com/stretchr/testify v1.9.0
golang.org/x/text v0.14.0
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
7 changes: 7 additions & 0 deletions pkg/confidence/EventUploader.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"context"
"encoding/json"
"net/http"

"golang.org/x/exp/slog"
)

type EventUploader interface {
Expand All @@ -14,6 +16,7 @@ type EventUploader interface {
type HttpEventUploader struct {
Client *http.Client
Config APIConfig
Logger *slog.Logger
}

func (e HttpEventUploader) upload(ctx context.Context, request EventBatchRequest) {
Expand All @@ -31,7 +34,11 @@ func (e HttpEventUploader) upload(ctx context.Context, request EventBatchRequest

resp, err := e.Client.Do(req)
if err != nil {
e.Logger.Warn("Failed to perform upload request", "error", err)
return
}
if resp.StatusCode != http.StatusOK {
e.Logger.Warn("Failed to upload event", "status", resp.Status)
}
defer resp.Body.Close()
}
29 changes: 28 additions & 1 deletion pkg/confidence/confidence.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package confidence

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"reflect"
"strings"
"sync"
"time"

"golang.org/x/exp/slog"
)

type FlagResolver interface {
Expand All @@ -30,6 +34,7 @@ type Confidence struct {
contextMap map[string]interface{}
Config APIConfig
ResolveClient ResolveClient
Logger *slog.Logger
}

func (e Confidence) GetContext() map[string]interface{} {
Expand All @@ -55,6 +60,11 @@ type ConfidenceBuilder struct {
confidence Confidence
}

func (e ConfidenceBuilder) SetLogger(logger *slog.Logger) ConfidenceBuilder {
e.confidence.Logger = logger
return e
}

func (e ConfidenceBuilder) SetAPIConfig(config APIConfig) ConfidenceBuilder {
e.confidence.Config = config
if config.APIResolveBaseUrl == "" {
Expand All @@ -69,14 +79,18 @@ func (e ConfidenceBuilder) SetResolveClient(client ResolveClient) ConfidenceBuil
}

func (e ConfidenceBuilder) Build() Confidence {
if e.confidence.Logger == nil {
e.confidence.Logger = slog.Default()
}
if e.confidence.ResolveClient == nil {
e.confidence.ResolveClient = HttpResolveClient{Client: &http.Client{}, Config: e.confidence.Config}
}
if e.confidence.EventUploader == nil {
e.confidence.EventUploader = HttpEventUploader{Client: &http.Client{}, Config: e.confidence.Config}
e.confidence.EventUploader = HttpEventUploader{Client: &http.Client{}, Config: e.confidence.Config, Logger: e.confidence.Logger}
}

e.confidence.contextMap = make(map[string]interface{})
e.confidence.Logger.Info("Confidence created", "config", e.confidence.Config)
return e.confidence
}

Expand Down Expand Up @@ -117,8 +131,10 @@ func (e Confidence) Track(ctx context.Context, eventName string, data map[string
SendTime: iso8601Time,
Events: []Event{event},
}
e.Logger.Debug("EventUploading started", "eventName", eventName)
e.EventUploader.upload(ctx, batch)
wg.Done()
e.Logger.Debug("EventUploading completed", "eventName", eventName)
}()
return &wg
}
Expand All @@ -138,6 +154,7 @@ func (e Confidence) WithContext(context map[string]interface{}) Confidence {
contextMap: newMap,
Config: e.Config,
ResolveClient: e.ResolveClient,
Logger: e.Logger,
}
}

Expand Down Expand Up @@ -196,9 +213,18 @@ func (e Confidence) ResolveFlag(ctx context.Context, flag string, defaultValue i
Sdk: sdk{Id: SDK_ID, Version: SDK_VERSION}})

if err != nil {
slog.Warn("Error in resolving flag", "flag", flag, "error", err)
return processResolveError(err, defaultValue)
}
key := url.QueryEscape(e.Config.APIKey)
flagEncoded := url.QueryEscape(flag)
json, err := json.Marshal(e.GetContext())
if err == nil {
jsonContextEncoded := url.QueryEscape(string(json))
e.Logger.Debug("See resolves for " + flag + " in Confidence: https://app.confidence.spotify.com/flags/resolver-test?client-key=" + key + "&flag=flags/" + flagEncoded + "&context=" + jsonContextEncoded)
}
if len(resp.ResolvedFlags) == 0 {
slog.Debug("Flag not found", "flag", flag)
return InterfaceResolutionDetail{
Value: defaultValue,
ResolutionDetail: ResolutionDetail{
Expand All @@ -213,6 +239,7 @@ func (e Confidence) ResolveFlag(ctx context.Context, flag string, defaultValue i

resolvedFlag := resp.ResolvedFlags[0]
if resolvedFlag.Flag != requestFlagName {
slog.Warn("Unexpected flag from remote", "flag", resolvedFlag.Flag)
return InterfaceResolutionDetail{
Value: defaultValue,
ResolutionDetail: ResolutionDetail{
Expand Down
4 changes: 4 additions & 0 deletions pkg/confidence/confidence_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"reflect"
"testing"

"golang.org/x/exp/slog"

"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -102,6 +104,7 @@ func create_confidence(t *testing.T, response ResolveResponse) *Confidence {
Config: config,
ResolveClient: MockResolveClient{MockedResponse: response, MockedError: nil, TestingT: t},
contextMap: make(map[string]interface{}),
Logger: slog.Default(),
}
}

Expand All @@ -114,5 +117,6 @@ func createConfidenceWithUploader(t *testing.T, response ResolveResponse, upload
EventUploader: uploader,
ResolveClient: MockResolveClient{MockedResponse: response, MockedError: nil, TestingT: t},
contextMap: make(map[string]interface{}),
Logger: slog.Default(),
}
}
Loading
Loading