feature

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2024 License: MIT Imports: 6 Imported by: 0

README

feature Go Reference Lint Test

Package feature provides a simple, easy to use abstraction for working with feature flags in Go.

Examples

Defining and checking a flag

To define a flag use the global New function or, when using a custom Set, Set.New.

New takes a name for the flag and an optional description.

var newUIFlag = feature.New("new-ui", feature.WithDescription("enables the new UI"))

The status of the flag can be checked via the Enabled method which returns either true or false.

var tmpl *template.Template

if newUIFlag.Enabled(ctx) {
    tmpl = template.Must(template.Parse("new-ui/*.gotmpl")))
} else {
    tmpl = template.Must(template.Parse("old-ui/*.gotmpl")))
}

In order for this to work a Strategy must be configured first. Otherwise Enabled will panic.

Configuring a strategy

In order to use feature flags a Strategy must be created and associated with the Set using Set.SetStrategy or, if using the global set, the global SetStrategy function.

Example:

func main() {
    feature.SetStrategy(myCustomStrategy)
}

Or when using a custom Set:

func main() {
    mySet.SetStrategy(myCustomStrategy)
}
Using Switch to switch between code paths

A common use case for flags is switching between code paths, for example using a if/else combination.

The global Switch function provides an abstraction for this.

When using the provided Switch function both the decision making and the results of the call can be traced using the builtin tracing functionality.

To use Switch first define a flag:

var newUIFlag = feature.New("new-ui", feature.WithDescription("enables the new UI"))

Later in your code, just call Switch and pass the flag together with 2 callbacks, one for when the flag is enabled and one for when it is not.

tmpl, err := feature.Switch(ctx, newUIFlag, 
	func(context.Context) (*template.Template, error) { return template.Parse("new-ui/*.gotmpl") },
	func(context.Context) (*template.Template, error) { return template.Parse("old-ui/*.gotmpl") })
Running an experiment

In addition to simply switching between two functions using Switch, it is also possible to run both two functions concurrently and compare their results in order to compare code paths.

To do this use the global Experiment function and pass the feature flag, two functions that will be run and a callback to compare the results.

Calling Experiment will automatically run both functions and compare the results and pass the result of the comparison to the Tracer.Experiment function of the configured Tracer, if any.

The result of Experiment depends on the result of checking the flags status. If the flag is enabled, the results of the first function is returned. Otherwise the results of the second function are returned.

Example:

result, err := feature.Experiment(ctx, optimizationFlag, optimizedFunction, unoptimizedFunction, feature.Equals)
Using different sets of flags

All flags and cases created via New belong to a single global set of flags.

In some cases applications may want to have multiple sets, for example when extracting a piece of code into its own package and importing packages not wanting to clobber the global flag set with the imported, by not necessarily used flags.

For this and similar scenarios it is possible to create a custom Set which acts as its own separate namespace of flags.

When using a custom Set instead of using the New function, the Set.New method must be used.

Example:

var mySet feature.Set // zero value is valid

var optimizationFlag = mySet.New("new-ui", feature.WithDescription("enables the new UI"))
Changing strategies at runtime

The Strategy can be changed at any time during runtime. This can be useful for applications that keep cached states of all states in memory and periodically receive new states.

For cases like this the DecisionMap type can be useful, which returns a static boolean for each flag based on its name.

Example:

func main() {
	feature.SetStrategy(loadFlags())
	
	go func() {
		// Update flags every minute
		for range time.Ticker(time.Minute) {
			feature.SetStrategy(loadFlags())
		}
	}()
}

Tracing

It is possible to trace the use of Flags using the Tracer type.

In order to trace the use of this package simply use the global SetStrategy function to register a Tracer:

func main() {
    feature.SetTracer(myTracer)
}

Or when using a custom Set:

func main() {
    mySet.SetTracer(myCustomStrategy)
}
OpenTelemetry integration

The otelfeature package exposes a function that returns pre-configured Tracer that implements basic metrics and tracing for Flags as well as the global Switch and Experiment functions using OpenTelemetry.

In order to enable metrics collection and tracing use the global otelfeature.Tracer function to create a new feature.Tracer that can be passed to either the global SetTracer function or the Set.SetStrategy method.

func main() {
    tracer, err := otelfeature.Tracer(nil)
    if err != nil {
        // Handle the error
    }
    feature.SetTracer(tracer)
}

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

License

MIT

Documentation

Overview

Package feature provides a simple, easy to use abstraction for working with feature flags in Go.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Equals

func Equals[T comparable](a, b T) bool

Equals returns a function that compares to values of the same type using ==.

This can be used with Experiment when T is a comparable type.

func Experiment added in v0.3.0

func Experiment[T any](ctx context.Context, flag *Flag,
	experimental func(context.Context) (T, error),
	control func(context.Context) (T, error),
	equals func(new, old T) bool,
) (T, error)

Experiment runs both an experimental and a control function concurrently and compares their results using equals.

If the feature flag is enabled, the result of the experimental function will be returned, otherwise the result of the control function will be returned.

The given equals function is only called if there was no error.

When using values of a type that is comparable using ==, the global function Equals can be used to create the comparison function.

Example
optimizationFlag := feature.New("optimize-posts-loading",
	feature.WithDescription("enables new query for loading posts"))

// later

post, err := feature.Experiment(myCtx, optimizationFlag,
	func(ctx context.Context) (Post, error) { return loadPostOptimized(ctx, postId) },
	func(ctx context.Context) (Post, error) { return loadPost(ctx, postId) },
	feature.Equals[Post])
if err != nil {
	panic(err)
}

fmt.Println(post)
Output:

func SetStrategy

func SetStrategy(strategy Strategy)

SetStrategy sets the Strategy for the global Set.

Example
// Read initial configuration from local file
flags := readFlags("flags.json")

feature.SetStrategy(feature.DecisionMap(flags))

go func() {
	// Reload flags on SIGUSR1
	signals := make(chan os.Signal, 1)
	signal.Notify(signals, syscall.SIGUSR1)

	for range signals {
		flags := readFlags("flags.json")

		feature.SetStrategy(feature.DecisionMap(flags))
	}
}()

// Main logic...
Output:

func SetTracer

func SetTracer(tracer Tracer)

SetTracer sets the Tracer used for the global Set.

See Tracer for more information.

func Switch added in v0.3.0

func Switch[T any](ctx context.Context, flag *Flag,
	ifEnabled func(context.Context) (T, error),
	ifDisabled func(context.Context) (T, error),
) (T, error)

Switch checks if the associated flag is enabled and runs either ifEnabled or ifDisabled and returns their result.

Example
optimizationFlag := feature.New("optimize-posts-loading",
	feature.WithDescription("enables new query for loading posts"))

// later

post, err := feature.Switch(myCtx, optimizationFlag,
	func(ctx context.Context) (Post, error) { return loadPostOptimized(ctx, postId) },
	func(ctx context.Context) (Post, error) { return loadPost(ctx, postId) })
if err != nil {
	panic(err)
}

fmt.Println(post)
Output:

Types

type DecisionMap added in v0.3.0

type DecisionMap map[string]bool

DecisionMap implements a simple Strategy that returns a fixed value for each flag by its name.

Checking a flag that is not in the map will panic.

Example
package main

import (
	"encoding/json"
	"log"
	"os"

	"github.com/nussjustin/feature"
)

func main() {
	staticFlagsJSON, err := os.ReadFile("flags.json")
	if err != nil {
		log.Fatalf("failed to read flags JSON: %s", err)
	}

	var staticFlags map[string]bool
	if err := json.Unmarshal(staticFlagsJSON, &staticFlags); err != nil {
		log.Fatalf("failed to parse flags JSON: %s", err)
	}

	strategy := make(feature.DecisionMap, len(staticFlags))
	for name, enabled := range staticFlags {
		strategy[name] = enabled
	}

	feature.SetStrategy(strategy)
}
Output:

func (DecisionMap) Enabled added in v0.3.0

func (m DecisionMap) Enabled(_ context.Context, name string) bool

Enabled implements the Strategy interface.

If a feature with the given name is not found, Enabled will panic.

type Flag

type Flag struct {
	// contains filtered or unexported fields
}

Flag represents a feature flag that can be enabled or disabled (toggled) dynamically at runtime and used to control the behaviour of an application, for example by dynamically changing code paths (see Experiment and Switch).

A Flag must be obtained using either New or Set.New.

Example
package main

import (
	"html/template"
	"log"
	"net/http"

	"github.com/nussjustin/feature"
)

func main() {
	// Register flag. Most of the time this will be done globally.
	newUiFlag := feature.New("new-ui", feature.WithDescription("enables the new web ui"))

	// Load old and new UI templates
	oldUI := template.Must(template.ParseGlob("templates/old/*.gotmpl"))
	newUI := template.Must(template.ParseGlob("templates/new/*.gotmpl"))

	http.HandleFunc("/ui", func(w http.ResponseWriter, r *http.Request) {
		// Choose UI based on flag.
		if newUiFlag.Enabled(r.Context()) {
			_ = newUI.Execute(w, nil)
		} else {
			_ = oldUI.Execute(w, nil)
		}
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}
Output:

func Flags added in v0.3.0

func Flags() []*Flag

Flags returns a slice containing all flags registered with the global Set.

See Set.Flags for more information.

func New added in v0.3.0

func New(name string, opts ...FlagOpt) *Flag

New registers and returns a new Flag with the global Set.

See Set.New for more details.

func (*Flag) Description

func (f *Flag) Description() string

Description returns the description of the defined feature.

func (*Flag) Enabled

func (f *Flag) Enabled(ctx context.Context) bool

Enabled returns true if the feature is enabled for the given context.

Example:

if trackingFlag.Enabled(ctx) {
   trackUser(ctx, user)
}

func (*Flag) Labels added in v0.4.0

func (f *Flag) Labels() map[string]any

Labels returns a copy of the labels associated with this feature.

func (*Flag) Name

func (f *Flag) Name() string

Name returns the name of the feature flag.

type FlagOpt added in v0.5.0

type FlagOpt func(*Flag)

func WithDescription added in v0.5.0

func WithDescription(desc string) FlagOpt

WithDescription sets the description for a new flag.

func WithLabels added in v0.5.0

func WithLabels(l map[string]any) FlagOpt

WithLabels adds the given labels to a new flag.

type Set

type Set struct {
	// contains filtered or unexported fields
}

Set manages feature flags and provides a Strategy (using SetStrategy) for making dynamic decisions about a flags' status.

A Set with no associated Strategy is invalid and checking a flag will panic.

func (*Set) Flags added in v0.3.0

func (s *Set) Flags() []*Flag

Flags returns a slice containing all registered flags order by name.

func (*Set) New added in v0.3.0

func (s *Set) New(name string, opts ...FlagOpt) *Flag

New registers and returns a new Flag on s.

If the given name is empty or already registered, New will panic.

func (*Set) SetStrategy

func (s *Set) SetStrategy(strategy Strategy)

SetStrategy sets the Strategy used by s to make decisions.

func (*Set) SetTracer

func (s *Set) SetTracer(tracer Tracer)

SetTracer sets the Tracer used by the Set.

See Tracer for more information.

type Strategy

type Strategy interface {
	// Enabled takes the name of a feature flag and returns true if the feature is enabled or false otherwise.
	Enabled(ctx context.Context, name string) bool
}

Strategy defines an interface used for deciding on whether a feature is enabled or not.

A Strategy must be safe for concurrent use.

func FixedStrategy added in v0.3.0

func FixedStrategy(enabled bool) Strategy

FixedStrategy returns a Strategy that always returns the given boolean decision.

type StrategyFunc

type StrategyFunc func(ctx context.Context, name string) bool

StrategyFunc implements a Strategy by calling itself.

func (StrategyFunc) Enabled

func (f StrategyFunc) Enabled(ctx context.Context, name string) bool

Enabled implements the Strategy interface.

type Tracer

type Tracer struct {
	// Decision is called every time [Flag.Enabled] is called.
	Decision func(ctx context.Context, f *Flag, enabled bool)

	// Experiment is called at the beginning of every call to [Experiment].
	//
	// The returned function is called after both functions given to [Experiment] have returned and is passed
	// the values that will be returned as well as a boolean that indicates if the experiment was successful (the
	// results were equal and no errors occurred).
	//
	// The returned function can be nil.
	Experiment func(ctx context.Context, f *Flag, enabled bool) (context.Context, func(result any, err error, success bool))

	// ExperimentBranch is called for each called function during [Experiment] as well as for the function called by [Switch].
	//
	// The returned function is called after the called function has returned with the values returned by the function.
	//
	// The returned function can be nil.
	ExperimentBranch func(ctx context.Context, f *Flag, enabled bool) (context.Context, func(result any, err error))

	// Switch is called at the beginning of every call to [Switch].
	//
	// The returned function is called with the result that will be returned.
	//
	// The returned function can be nil.
	Switch func(ctx context.Context, f *Flag, enabled bool) (context.Context, func(result any, err error))
}

Tracer can be used to trace the use of calls to Flag.Enabled as well as the global helper functions Experiment and Switch.

See the documentation on each field for information on what can be traced.

All fields are optional.

A basic, pre-configured Tracer using OpenTelemetry can be found in the otelfeature subpackage.

Directories

Path Synopsis
Package otelfeature implements tracing and metric collection for feature flag usage using OpenTelemetry.
Package otelfeature implements tracing and metric collection for feature flag usage using OpenTelemetry.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL