Documentation
¶
Overview ¶
Package flags provides feature flags: typed values declared in code with a default, overridden by FLAG_<NAME> configuration and by values stored in the database (globally or for one scope, such as a user or a tenant).
var NewCheckout = flags.Bool("new_checkout", false).Describe("Serve the new checkout")
var AdminTwoFactor = flags.Bool("admin_two_factor", true).Lock()
app, err := vukem.New(vukem.WithFlags(NewCheckout, AdminTwoFactor))
ff, err := app.Flags()
if ff.Enabled(ctx, NewCheckout) { ... }
A value is resolved, lowest precedence first, from the code default, the FLAG_<NAME> variable, the stored global value and the stored value for the scope in the context (WithScope). Locked flags always have their code default. Reads fail closed: when the store or the cache errors, the default (the code default, or FLAG_<NAME> when set) is returned and a warning is logged; a read never fails a request. A kill switch should therefore default to its safe value in code, or be locked.
See https://github.com/AlexGalhardo/vukem/blob/main/docs/subsystems.md#feature-flags.
Example ¶
package main
import (
"context"
"fmt"
"github.com/AlexGalhardo/vukem/config"
"github.com/AlexGalhardo/vukem/flags"
)
func main() {
newCheckout := flags.Bool("new_checkout", false).Describe("Serve the new checkout")
adminMFA := flags.Bool("admin_two_factor", true).Lock()
uploadLimit := flags.Int("upload_limit_mb", 10)
// In an app: vukem.WithFlags(newCheckout, adminMFA, uploadLimit) and
// app.Flags(), which adds the database store and the app cache.
ff, err := flags.New(flags.Config{
Flags: []flags.AnyFlag{newCheckout, adminMFA, uploadLimit},
Env: config.MapSource(map[string]string{"FLAG_UPLOAD_LIMIT_MB": "25"}),
})
if err != nil {
fmt.Println(err)
return
}
ctx := context.Background()
fmt.Println(ff.Enabled(ctx, newCheckout), ff.Enabled(ctx, adminMFA), flags.Get(ctx, ff, uploadLimit))
fmt.Println(ff.Expose(ctx, newCheckout))
}
Output: false true 25 map[new_checkout:false]
Index ¶
- Constants
- Variables
- func Get[T Type](ctx context.Context, f *Flags, flag Flag[T]) T
- func Migration(id string) schema.Migration
- func WithScope(ctx context.Context, scope Scope) context.Context
- type AnyFlag
- type Change
- type Config
- type DatabaseStore
- type Flag
- type Flags
- func (f *Flags) All() []Info
- func (f *Flags) Enabled(ctx context.Context, flag Flag[bool]) bool
- func (f *Flags) Expose(ctx context.Context, allow ...AnyFlag) map[string]any
- func (f *Flags) Require(flag Flag[bool]) func(http.Handler) http.Handler
- func (f *Flags) Reset(ctx context.Context, scope Scope, name string) (Change, error)
- func (f *Flags) Set(ctx context.Context, scope Scope, name, value string) (Change, error)
- func (f *Flags) Share(allow ...AnyFlag) func(http.Handler) http.Handler
- func (f *Flags) States(ctx context.Context, scope Scope) ([]State, error)
- type Info
- type Kind
- type Scope
- type State
- type Store
- type Type
Examples ¶
Constants ¶
const DefaultTTL = 30 * time.Second
DefaultTTL is how long stored values stay cached when Config.TTL is zero. Writes invalidate the cached values, so a change shows at once on every instance sharing the cache; instances with their own memory cache see it within the TTL.
const EnvPrefix = "FLAG_"
EnvPrefix prefixes the configuration key that overrides a flag's code default: FLAG_ plus the upper-cased name (FLAG_NEW_CHECKOUT).
const Table = "feature_flags"
Table is the table holding stored flag values.
Variables ¶
var ( ErrUnknown = errors.New("flags: unknown flag") ErrLocked = errors.New("flags: flag is locked") ErrInvalid = errors.New("flags: invalid value") ErrNoStore = errors.New("flags: no store configured") )
Errors returned by Set, Put and Reset.
Functions ¶
func Get ¶
Get returns the value of flag for the scope in ctx. It never fails: on a store or cache error, or an invalid scope, it returns the default and logs a warning; for a flag that was not registered it returns the code default and logs an error (once per name).
func Migration ¶
Migration creates the flag table; register it with a timestamped id (vukem install:flags does):
flags.Migration("20261017120000_create_feature_flags_table")
func WithScope ¶
WithScope returns a context whose flag reads also apply the values stored for scope; stored scope values win over global ones. Set it once per request in middleware, after authentication, from an internal id ("user:" + u.AuthID()), never from raw input. A read with an invalid scope returns the default and logs a warning.
Types ¶
type AnyFlag ¶
type AnyFlag interface {
// contains filtered or unexported methods
}
AnyFlag is a Flag of any type, for registration and allowlists. Only Flag implements it.
type Change ¶
Change describes a write: the flag's resolved value in its scope before and after it. Sensitive is true for string flags, whose values may be anything: log or audit them only redacted (the admin panel records their length and a SHA-256 prefix).
type Config ¶
type Config struct {
// Flags are the registered flags; reading any other flag returns its
// code default and logs an error.
Flags []AnyFlag
// Store keeps the stored values. Nil uses code defaults and
// FLAG_<NAME> only, and writes fail with ErrNoStore.
Store Store
// Cache caches the stored values for TTL. Nil reads the store every
// time. Use a cache shared by every instance (database or Redis) when
// a flag is a kill switch that must apply at once.
Cache *cache.Cache
// TTL is how long stored values stay cached (DefaultTTL when zero).
TTL time.Duration
// Env resolves FLAG_<NAME> overrides (app.Source). When it also lists
// its keys (config.Source does, for .env files and maps), a FLAG_ key
// that names no flag is an error; process variables cannot be listed,
// so a typo there goes unnoticed.
Env config.Lookuper
// Log receives the warnings of failed reads (slog.Default when nil).
Log *slog.Logger
}
Config configures New.
type DatabaseStore ¶
type DatabaseStore struct {
// contains filtered or unexported fields
}
DatabaseStore keeps stored values in Table (SQLite, Postgres or MySQL).
func NewDatabaseStore ¶
func NewDatabaseStore(db *database.DB, now func() time.Time) *DatabaseStore
NewDatabaseStore returns a store over db; now stamps updated_at (time.Now when nil).
type Flag ¶
type Flag[T Type] struct { // contains filtered or unexported fields }
Flag is a typed feature flag. Declare it once (a package-level value is fine: it is immutable) and register it with vukem.WithFlags.
func (Flag[T]) Description ¶
Description returns the description.
type Flags ¶
type Flags struct {
// contains filtered or unexported fields
}
Flags resolves and changes feature flags. It is safe for concurrent use.
func New ¶
New validates cfg: flag names (lowercase letters, digits and underscores, starting with a letter), duplicates and FLAG_<NAME> values. Misconfiguration is returned here, at boot.
func (*Flags) Expose ¶
Expose returns the values of the allowlisted flags for the scope in ctx, keyed by name, for views and Inertia props. Only the flags passed are included: never expose every flag to the browser. Flags that were not registered are left out (and logged).
func (*Flags) Require ¶
Require serves next only while flag is on for the scope in the request context; otherwise it responds 404, as if the route did not exist. A flag that was not registered is logged when Require is called (at boot) and always responds 404.
func (*Flags) Reset ¶
Reset removes the stored value of the flag called name in scope, so it falls back to the global value (for a scope) or the default.
func (*Flags) Set ¶
Set stores the value of the flag called name in scope (Global for everyone). value is parsed by the flag's kind ("true", "42", "blue"). It fails with ErrUnknown, ErrLocked, ErrInvalid (also for an invalid scope) or ErrNoStore, or with the store's error. Readers sharing the cache see the new value at once.
type Info ¶
type Info struct {
Name string
Description string
Kind Kind
// Default is the encoded code default ("true", "10", "classic").
Default string
Locked bool
}
Info describes a registered flag.
type Scope ¶
type Scope string
Scope names who a stored value applies to: an internal identifier such as "user:42" or "tenant:7", never raw user input. It is 1 to 191 bytes of lowercase letters, digits and ":._-"; Global ("") applies to everyone.
const Global Scope = ""
Global is the scope of values that apply to everyone.
type State ¶
type State struct {
Info
// Value is the resolved value in the scope.
Value string
// Stored reports whether the scope has a stored value (ignored for
// locked flags).
Stored bool
}
State is a flag as the admin panel shows it. Its Default is the code default, or FLAG_<NAME> when set.
type Store ¶
type Store interface {
// Load returns the stored values of scope by flag name.
Load(ctx context.Context, scope Scope) (map[string]string, error)
// Save stores the value of name in scope.
Save(ctx context.Context, scope Scope, name, value string) error
// Delete removes the value of name in scope; a missing row is no error.
Delete(ctx context.Context, scope Scope, name string) error
}
Store keeps stored values by scope (Global is "").