🎛️
go-feature-flag
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/test.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/test.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() . |
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/test.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).
PollInterval
.
From an HTTP endpoint (click to see details)
err := ffclient.Init(ffclient.Config{
PollInterval: 3,
Retriever: &ffclient.HTTPRetriever{
URL: "http://example.com/test.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: "test.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 |
Name of the flag. It should be unique. | |
percentage |
Percentage of the users affect by the flag. Default value is 0 |
|
rule |
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 |
The value return by the flag if apply to the user (rule is evaluated to true) and user is in the active percentage. | |
false |
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 |
The value return by the flag if not apply to the user (rule is evaluated to false). | |
disable |
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 "[email protected]"
- 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", "[email protected]").
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 |
The complete URL of your API (we will send a POST request to this URL, see format) | |
Secret |
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 |
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.
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/test.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.