OpenFeature Providers

OpenFeature Go Provider

ConfigDirector Provider for the OpenFeature Go SDK

Introduction

The OpenFeature Go Provider is intended to be used in combination with the OpenFeature Go SDK. The provider wraps the ConfigDirector Go SDK.

The minimum Go version supported is 1.26.

The provider is safe to share between goroutines. Register one instance per process when your application starts, and shut OpenFeature down when it stops. Evaluations read config state the provider already holds in memory, so they make no network calls on the request path.

Installation

The provider can be installed with go get. Its package reference is on pkg.go.dev: https://pkg.go.dev/github.com/ConfigDirector/go-sdks/configdirector-openfeature

The OpenFeature Go SDK (github.com/open-feature/go-sdk) and the ConfigDirector Go SDK are included as dependencies.

go get github.com/ConfigDirector/go-sdks/configdirector-openfeature@latest

Configure and initialize the client

  1. Create an instance of the provider using your server SDK key. You can retrieve a server SDK key for each environment under SDK Keys in the dashboard's navigation panel. The provider starts connecting as soon as it is created.
  2. Set the OpenFeature provider.
  3. Get a client instance from OpenFeature.
main.go
import (
    "log"

    "github.com/ConfigDirector/go-sdks/configdirector"
    configdirectoropenfeature "github.com/ConfigDirector/go-sdks/configdirector-openfeature"
    "github.com/open-feature/go-sdk/openfeature"
)

// IMPORTANT: Do not commit the server SDK key to your source code, it is a secret value.
provider, err := configdirectoropenfeature.NewProvider(configdirector.Options{SDKKey: "YOUR-SERVER-SDK-KEY"})
if err != nil {
    log.Fatal(err)
}

// Blocks until the provider is initialized or it times out.
// If initialization times out, the provider keeps connecting in the background.
if err := openfeature.SetProviderAndWait(provider); err != nil {
    log.Fatal(err)
}
defer openfeature.Shutdown()

client := openfeature.NewClient("my-app")
Server SDK keys are secret values. Do not commit them to your source code repository. Provide them at runtime via environment variables instead.

SetProviderAndWait blocks until the initial config state arrives or the configured timeout elapses, and it does not return an error on a connection failure. Until config state arrives, evaluations return the in-code default value with the PROVIDER_NOT_READY error code, and the provider continues to connect in the background. SetProviderWithContextAndWait takes a context that abandons the wait when it ends.

Additional configuration options

The provider takes the same configdirector.Options as the Go SDK client, so every option of the SDK is available on the provider.

For example, the Metadata can be provided like this:

main.go
provider, err := configdirectoropenfeature.NewProvider(configdirector.Options{
    SDKKey:   "YOUR-SERVER-SDK-KEY",
    Metadata: configdirector.Metadata{AppName: "YOUR-APP-NAME", AppVersion: "1.0.2"},
})

The provider accepts the same Metadata, Logger, Connection, Telemetry and Hooks options as the Go SDK client, refer to the additional configuration options section of the Go SDK for a full list. Handlers in Hooks run after the provider's own.

Shut down

Shutting OpenFeature down closes the provider, which closes its connections and reports any pending telemetry:

main.go
defer openfeature.Shutdown()

Retrieve config values

To retrieve config values, use the OpenFeature client:

Every getter takes a context.Context first, such as the request's context in an HTTP handler, and the evaluation context last:

main.go
booleanValue := client.Boolean(ctx, "my-config-key", false, openfeature.EvaluationContext{})

stringValue := client.String(ctx, "my-string-config-key", "Default", openfeature.EvaluationContext{})

Each OpenFeature getter maps to a ConfigDirector config type:

OpenFeature getterConfigDirector config value
BooleanBoolean
StringString or enum
Int, FloatNumber
ObjectJSON object or JSON array

Object returns the JSON document decoded the way encoding/json decodes into an any: a JSON object is a map[string]any, a JSON array a []any, and a number a float64:

main.go
settings := client.Object(ctx, "my-json-config-key", map[string]any{}, openfeature.EvaluationContext{})
theme := settings.(map[string]any)["theme"]

For additional information regarding the OpenFeature client refer to the OpenFeature Go SDK documentation.

Evaluation details

The detailed getters of the OpenFeature client, such as BooleanValueDetails, report why an evaluation produced the value that it did:

OutcomeReasonError code
A value was foundTARGETING_MATCH
The config carries no valueDEFAULT
The config key is unknownERRORFLAG_NOT_FOUND
No config state has arrived yetERRORPROVIDER_NOT_READY
The value does not match the requested typeERRORTYPE_MISMATCH

When a value was found, the Variant is ConfigDirector's identifier for that value. In every other case the in-code default value is returned.

main.go
details, err := client.BooleanValueDetails(ctx, "new-checkout", false, user)
if err != nil {
    log.Printf("new-checkout fell back to its in-code default value: %s", details.ErrorCode)
}

User context

The user context is the last argument of every getter of the OpenFeature client. The OpenFeature Go provider evaluates targeting rules locally without additional network calls for different contexts.

main.go
import "github.com/open-feature/go-sdk/openfeature"

user := openfeature.NewEvaluationContext(
    "12345", // In OpenFeature, the targeting key represents the context's user ID
    map[string]any{
        "name": "Example User",
        // Any arbitrary traits which can be referenced in targeting rules
        "traits": map[string]any{"region": "North America"},
    },
)

booleanValue := client.Boolean(ctx, "my-config-key", false, user)

The evaluation context maps onto the ConfigDirector user context as follows:

OpenFeature evaluation contextConfigDirector user context
The targeting key, or otherwise an id attributeID
The name attributeName
The traits attribute, a map[string]anyTraits
The anonymous attribute, a boolAnonymous

For additional information regarding the OpenFeature client refer to the OpenFeature Go SDK documentation.

Events

The provider emits PROVIDER_CONFIGURATION_CHANGED whenever configs are updated on the dashboard or via the admin API, carrying the keys of the configs in the update followed by the keys of the configs it removed:

main.go
logChangedFlags := func(details openfeature.EventDetails) {
    log.Printf("Configs updated: %v", details.FlagChanges)
}

client.AddHandler(openfeature.ProviderConfigChange, &logChangedFlags)

It emits PROVIDER_READY when the initial config state arrives after SetProviderAndWait has already returned.

Test your code

Code that reads flags through OpenFeature is tested by swapping the provider, so it never has to change for a test and nothing from ConfigDirector is involved. The OpenFeature Go SDK ships an in-memory provider for that, memprovider, which serves the values your test sets.

Each flag is an InMemoryFlag with its named variants and the name of the variant it resolves to:

checkout_test.go
import (
    "testing"

    "github.com/open-feature/go-sdk/openfeature"
    "github.com/open-feature/go-sdk/openfeature/memprovider"
    "github.com/stretchr/testify/require"
)

func TestShowsTheNewCheckout(t *testing.T) {
    provider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{
        "new-checkout": {Key: "new-checkout", State: memprovider.Enabled, DefaultVariant: "on", Variants: map[string]any{"on": true, "off": false}},
        "max-items":    {Key: "max-items", State: memprovider.Enabled, DefaultVariant: "default", Variants: map[string]any{"default": int64(20)}},
    })
    require.NoError(t, openfeature.SetProviderAndWait(provider))
    t.Cleanup(openfeature.Shutdown)
    client := openfeature.NewClient("checkout")

    require.True(t, client.Boolean(t.Context(), "new-checkout", false, openfeature.EvaluationContext{}))
    require.Equal(t, int64(20), client.Int(t.Context(), "max-items", 0, openfeature.EvaluationContext{}))
}

The in-memory provider has no method to change a flag, so a test that changes a value mid-way sets a new provider with SetProviderAndWait. A flag can also carry a ContextEvaluator, a pointer to a function from the flag and the evaluation context to the value and its resolution details, for a test of code that targets specific users. The OpenFeature API is one global, so shut it down after each test with openfeature.Shutdown, as the cleanup above does, which lets the next test register its own provider. The examples use require from the testify module; the standard testing package works just as well.