Skip to main content

3 posts tagged with "golang"

View All Tags

· 2 min read
Skye Gill

Logging

Logging is the act of keeping a log. A log (in this case) records events that occur in software.

Subject to many opinions and differing principles of best practice, the best thing we could do for the go-sdk was to create an implementation as open & configurable as possible. To achieve this, we've integrated logr, this allows the use of any logger that conforms to its API.

Applications may already have a chosen logging solution at the point of introducing openfeature. An integration with logr may already exist for their chosen solution (integrations exist for many of the popular logger packages in go). If not, they could write their own integration.

Objective

Configure the popular go logger zap with the go-sdk.

Prerequisites

  • Golang 1.17+

Scaffolding

  1. Go get the following dependencies

    go get github.com/open-feature/go-sdk
    go get go.uber.org/zap
    go get github.com/go-logr/logr
    go get github.com/go-logr/zapr # an integration of zap with logr's API
  2. Import all of the above into your main.go and create func main()

    package main

    import (
    "github.com/go-logr/logr"
    "github.com/go-logr/zapr"
    "github.com/open-feature/go-sdk/pkg/openfeature"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "context"
    "log"
    )

    func main() {

    }

Integrating the logger

  1. Create the zap logger with preset development config (for the sake of this tutorial)

    func main() {
    zc := zap.NewDevelopmentConfig()
    zc.Level = zap.NewAtomicLevelAt(zapcore.Level(-1)) // the level here decides the verbosity of our logs
    z, err := zc.Build()
    if err != nil {
    log.Fatal(err)
    }
    }
  2. Create the zapr logger (zap logger that conforms to logr's interface)

    l := zapr.NewLogger(z)
  3. Set the logger to the global openfeature singleton

    openfeature.SetLogger(l)
  4. Create an openfeature client and invoke a flag evaluation

    c := openfeature.NewClient("log")

    evalCtx := openfeature.NewEvaluationContext("foo", nil)

    c.BooleanValue(context.Background(), "bar", false, evalCtx)
  5. Check the result of go run main.go

    2022-09-02T14:22:31.109+0100    INFO    openfeature/openfeature.go:76   set global logger
    2022-09-02T14:22:31.110+0100 DEBUG openfeature/client.go:230 evaluating flag {"flag": "bar", "type": "bool", "defaultValue": false, "evaluationContext": {"targetingKey":"foo","attributes":null}, "evaluationOptions": {}}
    2022-09-02T14:22:31.110+0100 DEBUG openfeature/client.go:336 executing before hooks
    2022-09-02T14:22:31.110+0100 DEBUG openfeature/client.go:349 executed before hooks
    2022-09-02T14:22:31.110+0100 DEBUG openfeature/client.go:355 executing after hooks
    2022-09-02T14:22:31.110+0100 DEBUG openfeature/client.go:364 executed after hooks
    2022-09-02T14:22:31.110+0100 DEBUG openfeature/client.go:318 evaluated flag {"flag": "bar", "details": {"FlagKey":"bar","FlagType":0,"Value":false,"ErrorCode":"","Reason":"","Variant":""}, "type": "bool"}
    2022-09-02T14:22:31.110+0100 DEBUG openfeature/client.go:377 executing finally hooks
    2022-09-02T14:22:31.110+0100 DEBUG openfeature/client.go:383 executed finally hooks
  6. (optional) Tweak the level set in step 1 to decrease the verbosity

· 8 min read
James Milligan

Providers

A Provider is responsible for performing flag evaluation, they can be as simple as an interface for a key value store, or act as an abstraction layer for a more complex evaluation system. Only one Provider can be registered at a time, and OpenFeature will no-op if one has not been defined. Before writing your own Provider, it is strongly recommended to familiarize yourself with the OpenFeature spec.
In this tutorial I will demonstrate the steps taken to create a new Provider whilst conforming to the OpenFeature spec using a simple flag implementation. The flag evaluation will be handled by a simple JSON evaluator and flag configurations will be stored as environment variables.

The following section describes how the flag evaluator portion of this project has been constructed, to skip to the Provider specific implementations, click here.

Creating the flag evaluator

The core of any flag Provider is the evaluation logic used to provide the flag values from the provided metadata (referred to as the Evaluation Context). For this example I have put together a very simple json evaluator. Flags are configured using the structs described below, and are stored as environment variables:

type StoredFlag struct {
DefaultVariant string `json:"defaultVariant"`
Variants []Variant `json:"variant"`
}

type Variant struct {
Criteria []Criteria `json:"criteria"`
TargetingKey string `json:"targetingKey"`
Value interface{} `json:"value"`
Name string `json:"name"`
}

type Criteria struct {
Key string `json:"key"`
Value interface{} `json:"value"`
}

example JSON:

{
"defaultVariant":"not-yellow",
"variants": [
{
"name": "yellow-with-key",
"targetingKey":"user",
"criteria": [
{
"color":"yellow"
}
],
"value":true
},
{
"name": "yellow",
"targetingKey":"",
"criteria": [
{
"color":"yellow"
}
],
"value":true
},
{
"name": "not-yellow",
"targetingKey":"",
"criteria": [],
"value":false
}
]
}

Each flag value contains an array of Variants, each with their own array of Criteria. When a flag request needs to be evaluated, the Variants slice is iterated over, if the FlattenedContext matches all required Criteria for a specific Variant, the associated flag value will be returned from the evaluator. If a matching Variant is not found the DefaultVariant is returned in the response. The response also includes the the variant name, the reason for the resulting value (such as ERROR, STATIC or TARGETING_MATCH) and any associated error (such as PARSE_ERROR). These values form the type naive ResolutionDetails structure, which is then wrapped in a type specific parent for each response type, such as BoolResolutionDetail. This will be discussed in the Creating a Compliant Provider section.

import (
"encoding/json"
"errors"
"github.com/open-feature/go-sdk/pkg/openfeature"
"os"
)

func (f *StoredFlag) Evaluate(evalCtx map[string]interface{}) (string, openfeature.Reason, interface{}, error) {
var defaultVariant *Variant
for _, variant := range f.Variants {
if variant.Name == f.DefaultVariant {
defaultVariant = &variant
}
if variant.TargetingKey != "" && variant.TargetingKey != evalCtx["targetingKey"] {
continue
}
match := true
for _, criteria := range variant.Criteria {
val, ok := evalCtx[criteria.Key]
if !ok || val != criteria.Value {
match = false
break
}
}
if match {
return variant.Name, openfeature.TargetingMatchReason, variant.Value, nil
}
}
if defaultVariant == nil {
return "", openfeature.ErrorReason, nil, openfeature.NewParseErrorResolutionError("")
}
return defaultVariant.Name, openfeature.DefaultReason, defaultVariant.Value, nil
}

The above function demonstrates how this basic evaluator will work in this example, of course in other providers these can be far more complex, and the logic does not need to sit within the application.
This JSON evaluator can then be paired with a simple function for reading and parsing the StoredFlag values from environment variables (as seen in the example below), leaving only the integration with the go-sdk to go. (and some testing!)

func FetchStoredFlag(key string) (StoredFlag, error) {
v := StoredFlag{}
if val := os.Getenv(key); val != "" {
if err := json.Unmarshal([]byte(val), &v); err != nil {
return v, openfeature.NewParseErrorResolutionError(err.Error())
}
return v, nil
}
return v, openfeature.NewFlagNotFoundResolutionError("")
}

Creating a Compliant Provider

Repository Setup

Providers written for the go-sdk are all maintained in the go-sdk-contrib repository, containing both hooks and providers.
The following commands can be used to setup the go-sdk-contrib repository, they will clone the repository and set up your provider specific go module under /providers/MY-NEW-PROVIDER-NAME adding a go.mod and README.md file. Your module will them be referenced in the top level go.work file.

git clone https://github.com/open-feature/go-sdk-contrib.git
cd go-sdk-contrib
make PROVIDER=MY-NEW-PROVIDER-NAME new-provider
make workspace-init

Creating Your Provider

In order for your feature flag Provider to be compatible with the OpenFeature go-sdk, it will need to comply with the OpenFeature spec. For the go-sdk this means that it will need to fit to the following interface:

type FeatureProvider interface {
Metadata() Metadata
BooleanEvaluation(flagKey string, defaultValue bool, evalCtx FlattenedContext) BoolResolutionDetail
StringEvaluation(flagKey string, defaultValue string, evalCtx FlattenedContext) StringResolutionDetail
FloatEvaluation(flagKey string, defaultValue float64, evalCtx FlattenedContext) FloatResolutionDetail
IntEvaluation(flagKey string, defaultValue int64, evalCtx FlattenedContext) IntResolutionDetail
ObjectEvaluation(flagKey string, defaultValue interface{}, evalCtx FlattenedContext) InterfaceResolutionDetail
Hooks() []Hook
}
ArgumentDescription
flagKeyA string key representing the flag configuration used in this evaluation
defaultValueThe default response to be returned in the case of an error
evalCtxThe underlying type of FlattenedContext is map[string]interface{}, this provides ambient information for the purposes of flag evaluation. Effectively acting as metadata for a request
ProviderResolutionDetailThe provider response object from a flag evaluation, it contains the following fields: Variant (string), Reason (openfeature.Reason), ResolutionError (openfeature.ResolutionError)
XxxResolutionDetailThe type specific wrapper for the ProviderResolutionDetail struct. Contains two attributes: Value (type specific), ProviderResolutionDetail (ProviderResolutionDetail)

We can use our previously defined logic to build the Evaluation methods with ease, in the below example the core logic has been refactored into a separate function (resolveFlag()) to reduce code repetition, returning the ResolutionDetail structure directly. This means that the only type specific code required is a type cast the returned interface{} value, and for the type specific ResolutionDetail to be returned, e.g. BoolResolutionDetail.

type Provider struct {
EnvFetch func(key string) (StoredFlag, error)
}

func (p *Provider) resolveFlag(flagKey string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
// fetch the stored flag from environment variables
res, err := p.EnvFetch(flagKey)
if err != nil {
var e openfeature.ResolutionError
if !errors.As(err, &e) {
e = openfeature.NewGeneralResolutionError(err.Error())
}

return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
ResolutionError: e,
Reason: openfeature.ErrorReason,
},
}
}
// evaluate the stored flag to return the variant, reason, value and error
variant, reason, value, err := res.Evaluate(evalCtx)
if err != nil {
var e openfeature.ResolutionError
if !errors.As(err, &e) {
e = openfeature.NewGeneralResolutionError(err.Error())
}
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
ResolutionError: e,
Reason: openfeature.ErrorReason,
},
}
}

// return the type naive ResolutionDetail structure
return openfeature.InterfaceResolutionDetail{
Value: value,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Variant: variant,
Reason: reason,
},
}
}

func (p *Provider) BooleanEvaluation(flagKey string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
res := p.resolveFlag(flagKey, defaultValue, evalCtx)
// ensure the returned value is a bool
v, ok := res.Value.(bool)
if !ok {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewTypeMismatchResolutionError(""),
Reason: openfeature.ErrorReason,
},
}
}
// wrap the ResolutionDetail in a type specific parent
return openfeature.BoolResolutionDetail{
Value: v,
ProviderResolutionDetail: res.ProviderResolutionDetail,
}
}

Based upon this BooleanEvaluation method, the remaining Evaluation methods are simple to populate, leaving only 2 more methods, Metadata and Hooks.

the Metadata() method is very simple to implement, it simply needs to return a Metadata object, currently this object only requires one field - Name

func (p *Provider) Metadata() openfeature.Metadata {
return openfeature.Metadata{
Name: "environment-flag-evaluator",
}
}

The Hooks() method gives the go-sdk access to Provider hooks, this sits outside the scope of this tutorial, so for now we will just return an empty slice of hooks. Link to spec.

func (p *Provider) Hooks() []openfeature.Hook {
return []openfeature.Hook{}
}

Now that the Provider conforms to the OpenFeature spec, it can be registered to the OpenFeature go-sdk and used for flag evaluation.

Example usage

package main

import (
"context"
"encoding/json"
"fmt"
"os"

fromEnv "github.com/open-feature/go-sdk-contrib/providers/from-env/pkg"
"github.com/open-feature/go-sdk/pkg/openfeature"
)

// init function sets a bool flag environment variable called AM_I_YELLOW
func init() {
flagDefinition := fromEnv.StoredFlag{
DefaultVariant: "not-yellow",
Variants: []fromEnv.Variant{
{
Name: "yellow-with-targeting",
TargetingKey: "user",
Value: true,
Criteria: []fromEnv.Criteria{
{
Key: "color",
Value: "yellow",
},
},
},
{
Name: "yellow",
TargetingKey: "",
Value: true,
Criteria: []fromEnv.Criteria{
{
Key: "color",
Value: "yellow",
},
},
},
{
Name: "not-yellow",
TargetingKey: "",
Value: false,
Criteria: []fromEnv.Criteria{
{
Key: "color",
Value: "not yellow",
},
},
},
},
}
flagM, _ := json.Marshal(flagDefinition)
os.Setenv("AM_I_YELLOW", string(flagM))
}

func main() {
// create instance of the new provider
provider := fromEnv.FromEnvProvider{}
// register the provider against the go-sdk
openfeature.SetProvider(&provider)
// create a client from via the go-sdk
client := openfeature.NewClient("am-i-yellow-client")

// we are now able to evaluate our stored flags, providing different FlattenedContexts to manipulate the response
fmt.Println("I am yellow!")
boolRes, err := client.BooleanValueDetails(
context.Background(),
"AM_I_YELLOW",
false,
openfeature.NewEvaluationContext(
"",
map[string]interface{}{
"color": "yellow",
},
),
)
printResponse(boolRes.Value, boolRes.ResolutionDetail, err)

fmt.Println("I am yellow with targeting!")
boolRes, err = client.BooleanValueDetails(
context.Background(),
"AM_I_YELLOW",
false,
openfeature.NewEvaluationContext(
"user",
map[string]interface{}{
"color": "yellow",
},
),
)
printResponse(boolRes.Value, boolRes.ResolutionDetail, err)

fmt.Println("I am asking for a string!")
strRes, err := client.StringValueDetails(
context.Background(),
"AM_I_YELLOW",
"i am a default value",
openfeature.NewEvaluationContext(
"",
map[string]interface{}{
"color": "not yellow",
},
),
)
printResponse(strRes.Value, strRes.ResolutionDetail, err)
}

// simple response printing function
func printResponse(value interface{}, resDetail openfeature.ResolutionDetail, err error) {
fmt.Printf("value: %v\n", value)
if err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("variant: %v\n", resDetail.Variant)
fmt.Printf("reason: %v\n", resDetail.Reason)
}
fmt.Println("--------")
}

Console output:

I am yellow!
value: true
variant: yellow
reason: TARGETING_MATCH
--------
I am yellow with targeting!
value: true
variant: yellow-with-targeting
reason: TARGETING_MATCH
--------
I am asking for a string!
value: i am a default value
error: evaluate the flag: TYPE_MISMATCH
--------

The provider used in this example can be found here

· 3 min read
Skye Gill

Hooks

A Hook taps into one or more of the flag evaluation's lifecycle events (before/after/error/finally) to perform the same action at that point for every evaluation.

Objective

Create and integrate a spec compliant hook that validates that the result of a flag evaluation is a hex color.

Prerequisites

  • Golang 1.17+

Repository setup

Hooks written for the go-sdk are all maintained in the go-sdk-contrib repository, containing both hooks and providers. The following commands can be used to setup the go-sdk-contrib repository, this clones the repository and sets up your hook specific go module under /hooks/MY-NEW-HOOK-NAME adding a go.mod and README.md file. The module will then be referenced in the top level go.work file.

git clone https://github.com/open-feature/go-sdk-contrib.git
cd go-sdk-contrib
make HOOK=MY-NEW-HOOK-NAME new-hook
make workspace-init

Creating the hook

In order for the Hook to be compatible with the OpenFeature go-sdk, it will need to comply to the OpenFeature spec. For the go-sdk this means that it will need to conform to the following interface:

type Hook interface {
Before(hookContext HookContext, hookHints HookHints) (*EvaluationContext, error)
After(hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) error
Error(hookContext HookContext, err error, hookHints HookHints)
Finally(hookContext HookContext, hookHints HookHints)
}

In order to conform to the interface we are forced to implement all of these functions, despite only wanting to tap into the After lifecycle event. Let's leave the other functions empty to achieve this:

// Hook validates the flag evaluation details After flag resolution
type Hook struct {
Validator validator
}

func (h Hook) Before(hookContext of.HookContext, hookHints of.HookHints) (*of.EvaluationContext, error) {
return nil, nil
}

func (h Hook) After(hookContext of.HookContext, flagEvaluationDetails of.InterfaceEvaluationDetails, hookHints of.HookHints) error {
err := h.Validator.IsValid(flagEvaluationDetails)
if err != nil {
return err
}

return nil
}

func (h Hook) Error(hookContext of.HookContext, err error, hookHints of.HookHints) {}

func (h Hook) Finally(hookContext of.HookContext, hookHints of.HookHints) {}

Notice the Validator field of type validator in the Hook struct. This is defined as such:

type validator interface {
IsValid(flagEvaluationDetails of.InterfaceEvaluationDetails) error
}

This allows us to supply any validator that implements this function signature, you can either create your own validator or use one of the existing validators. This tutorial uses the existing hex regex validator.

Integrating the hook

  1. Install dependencies

    go get github.com/open-feature/go-sdk
    go get github.com/open-feature/go-sdk-contrib/hooks/validator
  2. Import the dependencies

    package main

    import (
    "context"
    "fmt"
    "github.com/open-feature/go-sdk-contrib/hooks/validator/pkg/regex"
    "github.com/open-feature/go-sdk-contrib/hooks/validator/pkg/validator"
    "github.com/open-feature/go-sdk/pkg/openfeature"
    "log"
    )
  3. Create an instance of the validator hook struct using the regex hex validator

    func main() {
    hexValidator, err := regex.Hex()
    if err != nil {
    log.Fatal(err)
    }
    v := validator.Hook{Validator: hexValidator}
    }
  4. Register the NoopProvider, this simply returns the given default value on flag evaluation.

    This step is optional, the sdk uses the NoopProvider by default but we're explicitly setting it for completeness

    openfeature.SetProvider(openfeature.NoopProvider{})
  5. Create the client, call the flag evaluation using the validator hook at the point of invocation

    client := openfeature.NewClient("foo")

    result, err := client.
    StringValueDetails(
    context.Background(),
    "blue",
    "invalidhex",
    openfeature.EvaluationContext{},
    openfeature.WithHooks(v),
    )
    if err != nil {
    fmt.Println("err:", err)
    }
    fmt.Println("result:", result)
  6. Check that the flag evaluation returns an error as invalidhex is not a valid hex color

    go run main.go
    err: execute after hook: regex doesn't match on flag value
    result {blue 1 {invalidhex }}

    Note that despite getting an error we still get a result.