ffclient

package module
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Mar 24, 2021 License: MIT Imports: 16 Imported by: 25

README ΒΆ

go-feature-flag logo

πŸŽ›οΈ go-feature-flag Tweet

Build Status Coverage Status Sonarcloud Status License FOSSA Status
Release version GoDoc Go version Mentioned in Awesome Go

A feature flag solution, with YAML file in the backend (S3, GitHub, HTTP, local file ...).
No server to install, just add a file in a central system (HTTP, S3, GitHub, ...) and all your services will react to the changes of this file.

If you are not familiar with feature flags also called feature Toggles you can read this article of Martin Fowler that explains why this is a great pattern.
I've also wrote an article that explains why feature flags can help you to iterate quickly.

Installation

go get github.com/thomaspoignant/go-feature-flag

Quickstart

First, you need to initialize the ffclient with the location of your backend file.

err := ffclient.Init(ffclient.Config{
    PollInterval: 3,
    Retriever: &ffclient.HTTPRetriever{
        URL:    "http://example.com/flag-config.yaml",
    },
})
defer ffclient.Close()

This example will load a file from an HTTP endpoint and will refresh the flags every 3 seconds (if you omit the PollInterval, the default value is 60 seconds).

Now you can evaluate your flags anywhere in your code.

user := ffuser.NewUser("user-unique-key")
hasFlag, _ := ffclient.BoolVariation("test-flag", user, false)
if hasFlag {
    // flag "test-flag" is true for the user
} else {
    // flag "test-flag" is false for the user
}

You can find more example programs in the examples/ directory.

Configuration

The configuration is set with ffclient.Config{} and you can give it to ffclient.Init() the initialization function.

Example:

ffclient.Init(ffclient.Config{ 
    PollInterval:   3,
    Logger:         log.New(file, "/tmp/log", 0),
    Context:        context.Background(),
    Retriever:      &ffclient.FileRetriever{Path: "testdata/flag-config.yaml"},
    FileFormat:     "yaml",
    Webhooks:       []ffclient.WebhookConfig{
        {
            PayloadURL: " https://example.com/hook",
            Secret:     "Secret",
            Meta: map[string]string{
                "app.name": "my app",
            },
        },
    },
})
Descriptions
PollInterval Number of seconds to wait before refreshing the flags.
The default value is 60 seconds.
Logger Logger used to log what go-feature-flag is doing.
If no logger is provided the module will not log anything.
Context The context used by the retriever.
The default value is context.Background().
FileFormat Format of your configuration file. Available formats are yaml, toml and json, if you omit the field it will try to unmarshal the file as a yaml file.
Retriever The configuration retriever you want to use to get your flag file (see Where do I store my flags file for the configuration details).
Webhooks List of webhooks to call when your flag file has changed (see webhook section for more details).

Where do I store my flags file

go-feature-flags support different ways of retrieving the flag file.
We can have only one source for the file, if you set multiple sources in your configuration, only one will be take in consideration.

From GitHub (click to see details)
err := ffclient.Init(ffclient.Config{
    PollInterval: 3,
    Retriever: &ffclient.GithubRetriever{
        RepositorySlug: "thomaspoignant/go-feature-flag",
        Branch: "main",
        FilePath: "testdata/flag-config.yaml",
        GithubToken: "XXXX",
        Timeout: 2 * time.Second,
    },
})
defer ffclient.Close()

To configure the access to your GitHub file:

  • RepositorySlug: your GitHub slug org/repo-name. MANDATORY
  • FilePath: the path of your file. MANDATORY
  • Branch: the branch where your file is (default is main).
  • GithubToken: Github token is used to access a private repository, you need the repo permission (how to create a GitHub token).
  • Timeout: Timeout for the HTTP call (default is 10 seconds).

⚠ GitHub has rate limits, so be sure to not reach them when setting your PollInterval.

From an HTTP endpoint (click to see details)
err := ffclient.Init(ffclient.Config{
    PollInterval: 3,
    Retriever: &ffclient.HTTPRetriever{
        URL:    "http://example.com/flag-config.yaml",
        Timeout: 2 * time.Second,
    },
})
defer ffclient.Close()

To configure your HTTP endpoint:

  • URL: location of your file. MANDATORY
  • Method: the HTTP method you want to use (default is GET).
  • Body: If you need a body to get the flags.
  • Header: Header you should pass while calling the endpoint (useful for authorization).
  • Timeout: Timeout for the HTTP call (default is 10 seconds).
From a S3 Bucket (click to see details)
err := ffclient.Init(ffclient.Config{
    PollInterval: 3,
    Retriever: &ffclient.S3Retriever{
        Bucket: "tpoi-test",
        Item:   "flag-config.yaml",
        AwsConfig: aws.Config{
            Region: aws.String("eu-west-1"),
        },
    },
})
defer ffclient.Close()

To configure your S3 file location:

  • Bucket: The name of your bucket. MANDATORY
  • Item: The location of your file in the bucket. MANDATORY
  • AwsConfig: An instance of aws.Config that configure your access to AWS (see this documentation for more info). MANDATORY
From a file (click to see details)
err := ffclient.Init(ffclient.Config{
    PollInterval: 3,
    Retriever: &ffclient.FileRetriever{
        Path: "file-example.yaml",
    },
})
defer ffclient.Close()

To configure your File retriever:

  • Path: location of your file. MANDATORY

I will not recommend using a file to store your flags except if it is in a shared folder for all your services.

Flags file format

go-feature-flag is to avoid to have to host a backend to manage your feature flags and to keep them centralized by using a file a source.
Your file should be a YAML file with a list of flags (see example).

A flag configuration looks like:

test-flag:
  percentage: 100
  rule: key eq "random-key"
  true: true
  false: false
  default: false
  disable: false
test-flag mandatory Name of the flag. It should be unique.
percentage optional Percentage of the users affect by the flag.
Default value is 0
rule optional This is the query use to select on which user the flag should apply.
Rule format is describe in the rule format section.
If no rule set, the flag apply to all users (percentage still apply).
true mandatory The value return by the flag if apply to the user (rule is evaluated to true) and user is in the active percentage.
false mandatory The value return by the flag if apply to the user (rule is evaluated to true) and user is not in the active percentage.
default mandatory The value return by the flag if not apply to the user (rule is evaluated to false).
disable optional True if the flag is disabled.

Rule format

The rule format is based on the nikunjy/rules library.

All the operations can be written capitalized or lowercase (ex: eq or EQ can be used).
Logical Operations supported are AND OR.

Compare Expression and their definitions (a|b means you can use either one of the two a or b):

eq|==: equals to 
ne|!=: not equals to
lt|<: less than 
gt|>: greater than
le|<=: less than equal to
ge|>=: greater than equal to 
co: contains 
sw: starts with 
ew: ends with
in: in a list
pr: present
not: not of a logical expression
Examples
  • Select a specific user: key eq "example@example.com"
  • Select all identified users: anonymous ne true
  • Select a user with a custom property: userId eq "12345"

Users

Feature flag targeting and rollouts are all determined by the user you pass to your Variation calls.
The SDK defines a User struct and a UserBuilder to make this easy.

Here's an example:

// User with only a key
user1 := ffuser.NewUser("user1-key")

// User with a key plus other attributes
user2 = ffuser.NewUserBuilder("user2-key").
 AddCustom("firstname", "John").
 AddCustom("lastname", "Doe").
 AddCustom("email", "john.doe@example.com").
 Build()

The most common attribute is the user's key. In this case we've used the strings "user1-key" and "user2-key".
The user key is the only mandatory user attribute. The key should also uniquely identify each user. You can use a primary key, an e-mail address, or a hash, as long as the same user always has the same key. We recommend using a hash if possible.

Custom attributes are one of the most powerful features. They let you have rules on these attributes and target users according to any data that you want.

Variation

The Variation methods determine whether a flag is enabled or not for a specific user. There is a Variation method for each type: BoolVariation, IntVariation, Float64Variation, StringVariation, JSONArrayVariation and JSONVariation.

result, _ := ffclient.BoolVariation("your.feature.key", user, false)

// result is now true or false depending on the setting of this boolean feature flag

Variation methods take the feature flag key, a User, and a default value.

The default value is return when an error is encountered (ffclient not initialized, variation with wrong type, flag does not exist ...).
In the example, if the flag your.feature.key does not exists, result will be false.
Not that you will always have a usable value in the result.

Webhook

If you want to be informed when a flag has changed outside of your app, you can configure a webhook.

ffclient.Config{ 
    // ...
    Webhooks: []ffclient.WebhookConfig{
        {
            PayloadURL: " https://example.com/hook",
            Secret:     "Secret",
            Meta: map[string]string{
                "app.name": "my app",
            },
        },
    },
}
PayloadURL mandatory The complete URL of your API (we will send a POST request to this URL, see format)
Secret optional A secret key you can share with your webhook. We will use this key to sign the request (see signature section for more details).
Meta optional A list of key value that will be add in your request, this is super usefull if you to add information on the current running instance of your app.
By default the hostname is always added in the meta informations.
Format

If you have configured a webhook, a POST request will be sent to the PayloadURL with a body in this format:

{
    "meta": {
        "hostname": "server01",
        // ...
    },
    "flags": {
        "deleted": {}, // map of your deleted flags
        "added": {}, // map of your added flags
        "updated": {
            "flag-name": { // an object that contains old and new value
                "old_value": {},
                "new_value": {}
            }
        }
    }
}
Example
{
  "meta":{
      "hostname": "server01"
  },
  "flags":{
      "deleted": {
          "test-flag": {
              "rule": "key eq \"random-key\"",
              "percentage": 100,
              "true": true,
              "false": false,
              "default": false
          }
      },
      "added": {
          "test-flag3": {
              "percentage": 5,
              "true": "test",
              "false": "false",
              "default": "default"
          }
      },
      "updated": {
          "test-flag2": {
              "old_value": {
                  "rule": "key eq \"not-a-key\"",
                  "percentage": 100,
                  "true": true,
                  "false": false,
                  "default": false
              },
              "new_value": {
                  "disable": true,
                  "rule": "key eq \"not-a-key\"",
                  "percentage": 100,
                  "true": true,
                  "false": false,
                  "default": false
              }
          }
      }
  }
}
Signature

This header X-Hub-Signature-256 is sent if the webhook is configured with a secret. This is the HMAC hex digest of the request body, and is generated using the SHA-256 hash function and the secret as the HMAC key.

⚠ The recommendation is to always use the Secret and on your API/webook always verify the signature key to be sure that you don't have a man in the middle attack.


Multiple flag configurations

go-feature-flag comes ready to use out of the box by calling the Init function and after that it will be available everywhere. Since most applications will want to use a single central flag configuration, the go-feature-flag package provides this. It is similar to a singleton.

In all of the examples above, they demonstrate using go-feature-flag in its singleton style approach.

Working with multiple go-feature-flag

You can also create many different go-feature-flag client for use in your application.
Each will have its own unique set of configurations and flags. Each can read from a different config file and from different places.
All of the functions that go-feature-flag package supports are mirrored as methods on a goFeatureFlag.

Example:
x, err := ffclient.New(Config{ Retriever: &ffclient.HTTPRetriever{{URL: "http://example.com/flag-config.yaml",}})
defer x.Close()

y, err := ffclient.New(Config{ Retriever: &ffclient.HTTPRetriever{{URL: "http://example.com/test2.yaml",}})
defer y.Close()

user := ffuser.NewUser("user-key")
x.BoolVariation("test-flag", user, false)
y.BoolVariation("test-flag", user, false)

// ...

When working with multiple go-feature-flag, it is up to the user to keep track of the different go-feature-flag instances.

How can I contribute?

This project is open for contribution, see the contributor's guide for some helpful tips.

Documentation ΒΆ

Overview ΒΆ

Package ffclient aids adding instrumentation to have feature flags in your app without any backend server.

Summary ΒΆ

This package and its subpackages contain bits of code to have an easy feature flag solution with no complex installation to do on your infrastructure and without using 3rd party vendor for this.

The ffclient package provides the entry point - initialization and the basic method to get your flags value.

Before using the module you need to initialized it this way:

func main() {
  err := ffclient.Init(ffclient.Config{
           PollInterval: 3,
           HTTPRetriever: &ffClient.HTTPRetriever{
             URL:    "http://example.com/flag-config.yaml",
           },
         })
  defer ffclient.Close()
  ...

This example will load a file from an HTTP endpoint and will refresh the flags every 3 seconds.

Now you can evalute your flags anywhere in your code.

func main() {
  ...
  user := ffuser.NewUser("user-unique-key")
  hasFlag, _ := ffclient.BoolVariation("test-flag", user, false)
  if hasFlag {
    //flag "test-flag" is true for the user
  } else {
    // flag "test-flag" is false for the user
  }
  ...

Index ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

This section is empty.

Functions ΒΆ

func BoolVariation ΒΆ

func BoolVariation(flagKey string, user ffuser.User, defaultValue bool) (bool, error)

BoolVariation return the value of the flag in boolean. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value.

func Close ΒΆ

func Close()

Close the component by stopping the background refresh and clean the cache.

func Float64Variation ΒΆ

func Float64Variation(flagKey string, user ffuser.User, defaultValue float64) (float64, error)

Float64Variation return the value of the flag in float64. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value.

func Init ΒΆ

func Init(config Config) error

Init the feature flag component with the configuration of ffclient.Config

func main() {
  err := ffclient.Init(ffclient.Config{
           PollInterval: 3,
           Retriever: &ffClient.HTTPRetriever{
             URL:    "http://example.com/flag-config.yaml",
           },
         })
  defer ffclient.Close()

func IntVariation ΒΆ

func IntVariation(flagKey string, user ffuser.User, defaultValue int) (int, error)

IntVariation return the value of the flag in int. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value.

func JSONArrayVariation ΒΆ

func JSONArrayVariation(flagKey string, user ffuser.User, defaultValue []interface{}) ([]interface{}, error)

JSONArrayVariation return the value of the flag in []interface{}. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value.

func JSONVariation ΒΆ

func JSONVariation(
	flagKey string, user ffuser.User, defaultValue map[string]interface{}) (map[string]interface{}, error)

JSONVariation return the value of the flag in map[string]interface{}. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value.

func StringVariation ΒΆ

func StringVariation(flagKey string, user ffuser.User, defaultValue string) (string, error)

StringVariation return the value of the flag in string. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value.

Types ΒΆ

type Config ΒΆ

type Config struct {
	PollInterval int // Poll every X seconds
	Logger       *log.Logger
	Context      context.Context // default is context.Background()
	Retriever    Retriever
	Webhooks     []WebhookConfig // webhooks we should call when a flag create/update/delete
	FileFormat   string
}

Config is the configuration of go-feature-flag. PollInterval is the interval in seconds where we gonna read the file to update the cache. You should also have a retriever to specify where to read the flags file.

func (*Config) GetRetriever ΒΆ

func (c *Config) GetRetriever() (retriever.FlagRetriever, error)

GetRetriever returns a retriever.FlagRetriever configure with the retriever available in the config.

type FileRetriever ΒΆ added in v0.5.0

type FileRetriever struct {
	Path string
}

FileRetriever is a configuration struct for a local flat file.

type GithubRetriever ΒΆ added in v0.4.0

type GithubRetriever struct {
	RepositorySlug string
	Branch         string // default is main
	FilePath       string
	GithubToken    string
	Timeout        time.Duration // default is 10 seconds
}

GithubRetriever is a configuration struct for a GitHub retriever.

type GoFeatureFlag ΒΆ added in v0.6.0

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

GoFeatureFlag is the main object of the library it contains the cache, the config and the update.

func New ΒΆ added in v0.6.0

func New(config Config) (*GoFeatureFlag, error)

New creates a new go-feature-flag instance that retrieve the config from a YAML file and return everything you need to manage your flags.

func (*GoFeatureFlag) BoolVariation ΒΆ added in v0.6.0

func (g *GoFeatureFlag) BoolVariation(flagKey string, user ffuser.User, defaultValue bool) (bool, error)

BoolVariation return the value of the flag in boolean. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value. Note: Use this function only if you are using multiple go-feature-flag instances.

func (*GoFeatureFlag) Close ΒΆ added in v0.6.0

func (g *GoFeatureFlag) Close()

Close wait until thread are done

func (*GoFeatureFlag) Float64Variation ΒΆ added in v0.6.0

func (g *GoFeatureFlag) Float64Variation(flagKey string, user ffuser.User, defaultValue float64) (float64, error)

Float64Variation return the value of the flag in float64. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value. Note: Use this function only if you are using multiple go-feature-flag instances.

func (*GoFeatureFlag) IntVariation ΒΆ added in v0.6.0

func (g *GoFeatureFlag) IntVariation(flagKey string, user ffuser.User, defaultValue int) (int, error)

IntVariation return the value of the flag in int. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value. Note: Use this function only if you are using multiple go-feature-flag instances.

func (*GoFeatureFlag) JSONArrayVariation ΒΆ added in v0.6.0

func (g *GoFeatureFlag) JSONArrayVariation(
	flagKey string, user ffuser.User, defaultValue []interface{}) ([]interface{}, error)

JSONArrayVariation return the value of the flag in []interface{}. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value. Note: Use this function only if you are using multiple go-feature-flag instances.

func (*GoFeatureFlag) JSONVariation ΒΆ added in v0.6.0

func (g *GoFeatureFlag) JSONVariation(
	flagKey string, user ffuser.User, defaultValue map[string]interface{}) (map[string]interface{}, error)

JSONVariation return the value of the flag in map[string]interface{}. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value. Note: Use this function only if you are using multiple go-feature-flag instances.

func (*GoFeatureFlag) StringVariation ΒΆ added in v0.6.0

func (g *GoFeatureFlag) StringVariation(flagKey string, user ffuser.User, defaultValue string) (string, error)

StringVariation return the value of the flag in string. An error is return if you don't have init the library before calling the function. If the key does not exist we return the default value. Note: Use this function only if you are using multiple go-feature-flag instances.

type HTTPRetriever ΒΆ

type HTTPRetriever struct {
	URL     string
	Method  string
	Body    string
	Header  http.Header
	Timeout time.Duration
}

HTTPRetriever is a configuration struct for an HTTP endpoint retriever.

type Retriever ΒΆ added in v0.5.0

type Retriever interface {
	// contains filtered or unexported methods
}

type S3Retriever ΒΆ

type S3Retriever struct {
	Bucket    string
	Item      string
	AwsConfig aws.Config
}

S3Retriever is a configuration struct for a S3 retriever.

type WebhookConfig ΒΆ added in v0.7.0

type WebhookConfig struct {
	PayloadURL string            // PayloadURL of your webhook
	Secret     string            // Secret used to sign your request body.
	Meta       map[string]string // Meta information that you want to send to your webhook (not mandatory)
}

WebhookConfig is the configuration of your webhook. we will call this URL with a POST request with the following format

{
 "meta":{
     "hostname": "server01"
 },
 "flags":{
     "deleted": {
         "test-flag": {
             "rule": "key eq \"random-key\"",
             "percentage": 100,
             "true": true,
             "false": false,
             "default": false
         }
     },
     "added": {
         "test-flag3": {
             "percentage": 5,
             "true": "test",
             "false": "false",
             "default": "default"
         }
     },
     "updated": {
         "test-flag2": {
             "old_value": {
                 "rule": "key eq \"not-a-key\"",
                 "percentage": 100,
                 "true": true,
                 "false": false,
                 "default": false
             },
             "new_value": {
                 "disable": true,
                 "rule": "key eq \"not-a-key\"",
                 "percentage": 100,
                 "true": true,
                 "false": false,
                 "default": false
             }
         }
     }
 }
}

Directories ΒΆ

Path Synopsis
examples
Package ffuser defines the go-feature-flag model for user properties.
Package ffuser defines the go-feature-flag model for user properties.

Jump to

Keyboard shortcuts

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