Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
/pkg/extensions/ @grafana/grafana-backend-group
/pkg/ifaces/ @grafana/grafana-backend-group
/pkg/infra/db/ @grafana/grafana-backend-group
/pkg/infra/featureflags/ @grafana/grafana-backend-group
/pkg/infra/localcache/ @grafana/grafana-backend-group
/pkg/infra/log/ @grafana/grafana-backend-group
/pkg/infra/metrics/ @grafana/grafana-backend-group
Expand Down
33 changes: 33 additions & 0 deletions pkg/infra/featureflags/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package featureflags

import (
"github.com/open-feature/go-sdk/openfeature"
"golang.org/x/net/context"
)

// FeatureFlags interface provides a convenient abstraction around OpenFeature SDK
// OpenFeature is an open standard and SDK framework for managing feature flags consistently across applications and platforms.
// Remote evaluation allows applications to determine the state of feature flags dynamically from a central service,
// enabling real-time feature control without redeploying code.
type FeatureFlags interface {

// IsEnabled checks if a feature is enabled for a given context.
// The settings may be per user, tenant, or globally set in the cloud.
//
// Always perform flag evaluation at runtime, not during service startup, to ensure correct and up-to-date flag values.
IsEnabled(context.Context, string) bool
}

type Client struct {
delegate *openfeature.Client
}

func NewClient() *Client {
return &Client{
delegate: openfeature.NewDefaultClient(),
}
}

func (ff *Client) IsEnabled(ctx context.Context, flag string) bool {
return ff.delegate.Boolean(ctx, flag, false, openfeature.TransactionContext(ctx))
}
89 changes: 89 additions & 0 deletions pkg/infra/featureflags/testing/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package testing

import (

Check failure on line 3 in pkg/infra/featureflags/testing/client.go

View workflow job for this annotation

GitHub Actions / lint-go

File is not properly formatted (goimports)
"github.com/open-feature/go-sdk/openfeature"
"github.com/open-feature/go-sdk/openfeature/memprovider"
"github.com/open-feature/go-sdk/openfeature/testing"
"golang.org/x/net/context"
"sync"
)

var (
// Use mutex to make sure that we initialise TestProvider only once
mutex sync.Mutex

// TestProvider should be a global instance shared between tests
// Under the hood it utilises a custom _goroutine local_ storage to manage state on a per-test basis
provider *testing.TestProvider
)

type FeatureFlag struct {
Name string
Value bool
}

func NewFeatureFlag(name string, value bool) *FeatureFlag {
return &FeatureFlag{
Name: name,
Value: value,
}
}

type TestClient struct {
delegate *openfeature.Client
test *testing.TestProvider
}

func NewTestClient() *TestClient {
mutex.Lock()
defer mutex.Unlock()

if provider == nil {
newProvider := testing.NewTestProvider()
provider = &newProvider
}

err := openfeature.SetProviderAndWait(provider)
if err != nil {
panic("unable to set test provider: " + err.Error())
}
return &TestClient{
delegate: openfeature.NewDefaultClient(),
test: provider,
}
}

func (c *TestClient) IsEnabled(ctx context.Context, flag string) bool {
return c.delegate.Boolean(ctx, flag, false, openfeature.TransactionContext(ctx))
}

// SetFeatureFlags sets flags for the scope of a test.
func (c *TestClient) SetFeatureFlags(t testing.TestFramework, flags ...FeatureFlag) {
featureFlags := make(map[string]memprovider.InMemoryFlag)

for _, flag := range flags {

Check failure on line 64 in pkg/infra/featureflags/testing/client.go

View workflow job for this annotation

GitHub Actions / lint-go

unnecessary leading newline (whitespace)

defaultVariant := "off"
if flag.Value {
defaultVariant = "on"
}

variants := map[string]any{
"on": true,
"off": false,
}

featureFlags[flag.Name] = memprovider.InMemoryFlag{
Key: flag.Name,
State: memprovider.Enabled,
DefaultVariant: defaultVariant,
Variants: variants,
}
}
provider.UsingFlags(t, featureFlags)
}

// Cleanup deletes the flags bound to the current test and should be executed after each test execution
func (c *TestClient) Cleanup() {
provider.Cleanup()
}
110 changes: 110 additions & 0 deletions pkg/infra/featureflags/testing/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package testing

import (
"golang.org/x/net/context"

Check failure on line 4 in pkg/infra/featureflags/testing/client_test.go

View workflow job for this annotation

GitHub Actions / lint-go

File is not properly formatted (goimports)
"sync"
"testing"
)

type Barrier struct {
count int
target int

mutex sync.Mutex
cond *sync.Cond
}

func NewBarrier(target int) *Barrier {
barrier := &Barrier{
target: target,
mutex: sync.Mutex{},
}

barrier.cond = sync.NewCond(&barrier.mutex)
return barrier
}

func (b *Barrier) Wait() {
b.mutex.Lock()
b.count++
if b.count == b.target {
b.cond.Broadcast()
} else {
b.cond.Wait()
}
b.mutex.Unlock()
}

var (
FlagFoo = FeatureFlag{Name: "foo", Value: true}
FlagBar = FeatureFlag{Name: "bar", Value: true}
FlagBaz = FeatureFlag{Name: "baz", Value: true}
FlagFoobar = FeatureFlag{Name: "foobar", Value: true}

barrier1 = NewBarrier(2)
barrier2 = NewBarrier(2)
)

// Tests in this package provide an example on how to write tests and serves as a proof that
// the provided api is thread-safe and can be used to write tests that expected to run in parallel
//
// Test method: ensure a happens-before relationship between goroutines using a barrier
// to guarantee the following execution order:
//
// Test_Parallel_GoroutineA: setup flag "foo"
// Test_Parallel_GoroutineB: setup flag "bar"
// Test_Parallel_GoroutineA: check if flag "foo" is enabled
// Test_Parallel_GoroutineB: check if flag "bar" is enabled
func Test_Parallel_GoroutineA(t *testing.T) {
t.Parallel()

client := NewTestClient()
client.SetFeatureFlags(t, FlagFoo)
defer client.Cleanup()

barrier1.Wait()
// proceed only when test-a finished the setup of the flag "bar"
barrier2.Wait()

// flag "foo" should be enabled
if !client.IsEnabled(context.Background(), FlagFoo.Name) {
t.Fatalf("expected %s to be enabled", FlagFoo.Name)
}
}

func Test_Parallel_GoroutineB(t *testing.T) {
t.Parallel()

// proceed only when test-a finished the setup of the flag "foo"
barrier1.Wait()

// Initialise the Test Client and set up the test flag
client := NewTestClient()
client.SetFeatureFlags(t, FlagBar)
defer client.Cleanup()

barrier2.Wait()

if !client.IsEnabled(context.Background(), FlagBar.Name) {
t.Fatalf("expected %s to be disabled", FlagBar.Name)
}
}

func Test_Sequential(t *testing.T) {
tests := []FeatureFlag{FlagBaz, FlagFoobar}

for _, flag := range tests {
executeTest(t, flag)
}
}

func executeTest(t *testing.T, flag FeatureFlag) {
client := NewTestClient()

client.SetFeatureFlags(t, flag)
defer client.Cleanup()

if !client.IsEnabled(context.Background(), flag.Name) {
t.Fatalf("expected %s to be disabled", flag.Name)
}
}
24 changes: 24 additions & 0 deletions pkg/services/featuremgmt/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package featuremgmt

import (
"github.com/open-feature/go-sdk/openfeature"
"golang.org/x/net/context"
)

type FeatureFlags interface {
IsEnabled(context.Context, string) bool
}

type FeatureFlagsClient struct {
delegate *openfeature.Client
}

func NewFeatureFlagsClient() *FeatureFlagsClient {
return &FeatureFlagsClient{
delegate: openfeature.NewDefaultClient(),
}
}

func (ff *FeatureFlagsClient) IsEnabled(ctx context.Context, flag string) bool {
return ff.delegate.Boolean(ctx, flag, false, openfeature.TransactionContext(ctx))
}
41 changes: 41 additions & 0 deletions pkg/services/featuremgmt/testing/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package testing

import (

Check failure on line 3 in pkg/services/featuremgmt/testing/client.go

View workflow job for this annotation

GitHub Actions / lint-go

File is not properly formatted (goimports)
"github.com/open-feature/go-sdk/openfeature"
"github.com/open-feature/go-sdk/openfeature/memprovider"
"github.com/open-feature/go-sdk/openfeature/testing"
"golang.org/x/net/context"
"sync"
)

var (
mutex sync.Mutex
provider *testing.TestProvider
)

type TestClient struct {
delegate *openfeature.Client
test *testing.TestProvider
}

func NewTestClient() *TestClient {
mutex.Lock()
defer mutex.Unlock()

err := openfeature.SetProviderAndWait(provider)
if err != nil {
panic("unable to set test provider: " + err.Error())
}
return &TestClient{
delegate: openfeature.NewDefaultClient(),
test: provider,
}
}

func (c *TestClient) IsEnabled(ctx context.Context, flag string) bool {
return c.delegate.Boolean(ctx, flag, false, openfeature.TransactionContext(ctx))
}

func (c *TestClient) SetFeatureFlags(t testing.TestFramework, flags map[string]memprovider.InMemoryFlag) {
provider.UsingFlags(t, flags)
}
Loading