Skip to content

Commit

Permalink
refactored and implemented feature flags per feature (frain-dev#2105)
Browse files Browse the repository at this point in the history
* refactored and feature flags per feature

* refactored search feature flag

* fixed tests

* updated feature flags
  • Loading branch information
mekilis authored Aug 14, 2024
1 parent d947f47 commit ed0b5bb
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 65 deletions.
2 changes: 1 addition & 1 deletion cmd/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func startServerComponent(ctx context.Context, a *cli.App) error {
a.Logger.WithError(err).Fatal("failed to initialize realm chain")
}

flag, err := fflag.NewFFlag()
flag, err := fflag.NewFFlag(&cfg)
if err != nil {
a.Logger.WithError(err).Fatal("failed to create fflag controller")
}
Expand Down
33 changes: 33 additions & 0 deletions cmd/ff/feature_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ff

import (
"github.com/frain-dev/convoy/config"
fflag2 "github.com/frain-dev/convoy/internal/pkg/fflag"
"github.com/frain-dev/convoy/pkg/log"
"github.com/spf13/cobra"
)

func AddFeatureFlagsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "feature-flags",
Short: "Print the list of feature flags",
Annotations: map[string]string{
"CheckMigration": "true",
"ShouldBootstrap": "false",
},
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Get()
if err != nil {
log.WithError(err).Fatalf("Error fetching the config.")
}
f, err := fflag2.NewFFlag(&cfg)
if err != nil {
return err
}
return f.ListFeatures()
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {},
}

return cmd
}
12 changes: 4 additions & 8 deletions cmd/hooks/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,15 +434,11 @@ func buildCliConfiguration(cmd *cobra.Command) (*config.Configuration, error) {
}

// Feature flags
fflag, err := cmd.Flags().GetString("feature-flag")
fflag, err := cmd.Flags().GetStringSlice("enable-feature-flag")
if err != nil {
return nil, err
}

switch fflag {
case config.Experimental:
c.FeatureFlag = config.ExperimentalFlagLevel
}
c.EnableFeatureFlag = fflag

// tracing
tracingProvider, err := cmd.Flags().GetString("tracer-type")
Expand Down Expand Up @@ -503,14 +499,14 @@ func buildCliConfiguration(cmd *cobra.Command) (*config.Configuration, error) {

}

flag, err := fflag2.NewFFlag()
flag, err := fflag2.NewFFlag(c)
if err != nil {
return nil, err
}
c.Metrics = config.MetricsConfiguration{
IsEnabled: false,
}
if flag.CanAccessFeature(fflag2.Prometheus, c) {
if flag.CanAccessFeature(fflag2.Prometheus) {
metricsBackend, err := cmd.Flags().GetString("metrics-backend")
if err != nil {
return nil, err
Expand Down
9 changes: 5 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"github.com/frain-dev/convoy/cmd/ff"
"os"
_ "time/tzdata"

Expand Down Expand Up @@ -47,7 +48,7 @@ func main() {
var dbPassword string
var dbDatabase string

var fflag string
var fflag []string
var enableProfiling bool

var redisPort int
Expand Down Expand Up @@ -96,8 +97,7 @@ func main() {
c.Flags().StringVar(&redisDatabase, "redis-database", "", "Redis database")
c.Flags().IntVar(&redisPort, "redis-port", 0, "Redis Port")

c.Flags().StringVar(&fflag, "feature-flag", "", "Enable feature flags (experimental)")

c.Flags().StringSliceVar(&fflag, "enable-feature-flag", []string{}, "List of feature flags to enable e.g. \"full-text-search,prometheus\"")
// tracing
c.Flags().StringVar(&tracerType, "tracer-type", "", "Tracer backend, e.g. sentry, datadog or otel")
c.Flags().StringVar(&sentryDSN, "sentry-dsn", "", "Sentry backend dsn")
Expand All @@ -107,7 +107,7 @@ func main() {
c.Flags().StringVar(&otelAuthHeaderValue, "otel-auth-header-value", "", "OTel backend auth header value")

// metrics
c.Flags().StringVar(&metricsBackend, "metrics-backend", "prometheus", "Metrics backend e.g. prometheus. ('experimental' feature flag level required")
c.Flags().StringVar(&metricsBackend, "metrics-backend", "prometheus", "Metrics backend e.g. prometheus. ('prometheus' feature flag required")
c.Flags().Uint64Var(&prometheusMetricsSampleTime, "metrics-prometheus-sample-time", 5, "Prometheus metrics sample time")

c.Flags().StringVar(&retentionPolicy, "retention-policy", "", "SMTP Port")
Expand All @@ -128,6 +128,7 @@ func main() {
c.AddCommand(ingest.AddIngestCommand(app))
c.AddCommand(bootstrap.AddBootstrapCommand(app))
c.AddCommand(agent.AddAgentCommand(app))
c.AddCommand(ff.AddFeatureFlagsCommand())

if err := c.Execute(); err != nil {
slog.Fatal(err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func startConvoyServer(a *cli.App) error {
a.Logger.WithError(err).Fatal("failed to initialize realm chain")
}

flag, err := fflag.NewFFlag()
flag, err := fflag.NewFFlag(&cfg)
if err != nil {
a.Logger.WithError(err).Fatal("failed to create fflag controller")
}
Expand Down
11 changes: 9 additions & 2 deletions cmd/worker/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/frain-dev/convoy/config"
"github.com/frain-dev/convoy/database/postgres"
"github.com/frain-dev/convoy/internal/pkg/cli"
fflag2 "github.com/frain-dev/convoy/internal/pkg/fflag"
"github.com/frain-dev/convoy/internal/pkg/limiter"
"github.com/frain-dev/convoy/internal/pkg/loader"
"github.com/frain-dev/convoy/internal/pkg/memorystore"
Expand Down Expand Up @@ -306,8 +307,14 @@ func StartWorker(ctx context.Context, a *cli.App, cfg config.Configuration, inte
consumer.RegisterHandlers(convoy.DailyAnalytics, task.PushDailyTelemetry(lo, a.DB, a.Cache, rd), nil)
consumer.RegisterHandlers(convoy.EmailProcessor, task.ProcessEmails(sc), nil)

consumer.RegisterHandlers(convoy.TokenizeSearch, task.GeneralTokenizerHandler(projectRepo, eventRepo, jobRepo, rd), nil)
consumer.RegisterHandlers(convoy.TokenizeSearchForProject, task.TokenizerHandler(eventRepo, jobRepo), nil)
fflag, err := fflag2.NewFFlag(&cfg)
if err != nil {
return nil
}
if fflag.CanAccessFeature(fflag2.FullTextSearch) {
consumer.RegisterHandlers(convoy.TokenizeSearch, task.GeneralTokenizerHandler(projectRepo, eventRepo, jobRepo, rd), nil)
consumer.RegisterHandlers(convoy.TokenizeSearchForProject, task.TokenizerHandler(eventRepo, jobRepo), nil)
}

consumer.RegisterHandlers(convoy.NotificationProcessor, task.ProcessNotifications(sc), nil)
consumer.RegisterHandlers(convoy.MetaEventProcessor, task.ProcessMetaEvent(projectRepo, metaEventRepo), nil)
Expand Down
30 changes: 2 additions & 28 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ type OnPremStorage struct {
}

type MetricsConfiguration struct {
IsEnabled bool `json:"metrics_enabled" envconfig:"CONVOY_METRICS_ENABLED"`
IsEnabled bool `json:"enabled" envconfig:"CONVOY_METRICS_ENABLED"`
Backend MetricsBackend `json:"metrics_backend" envconfig:"CONVOY_METRICS_BACKEND"`
Prometheus PrometheusMetricsConfiguration `json:"prometheus_metrics"`
}
Expand Down Expand Up @@ -324,39 +324,13 @@ type (
LimiterProvider string
DatabaseProvider string
SearchProvider string
FeatureFlagProvider string
MetricsBackend string
)

func (s SignatureHeaderProvider) String() string {
return string(s)
}

type FlagLevel int

const (
ExperimentalFlagLevel FlagLevel = iota + 1
)

const Experimental = "experimental"

func (ft *FlagLevel) UnmarshalJSON(v []byte) error {
switch string(v) {
case Experimental:
*ft = ExperimentalFlagLevel
}
return nil
}

func (ft FlagLevel) MarshalJSON() ([]byte, error) {
switch ft {
case ExperimentalFlagLevel:
return []byte(fmt.Sprintf(`"%s"`, []byte(Experimental))), nil
default:
return []byte(fmt.Sprintf(`"%s"`, []byte(Experimental))), nil
}
}

type ExecutionMode string

const (
Expand All @@ -381,7 +355,7 @@ type Configuration struct {
Host string `json:"host" envconfig:"CONVOY_HOST"`
Pyroscope PyroscopeConfiguration `json:"pyroscope"`
CustomDomainSuffix string `json:"custom_domain_suffix" envconfig:"CONVOY_CUSTOM_DOMAIN_SUFFIX"`
FeatureFlag FlagLevel `json:"feature_flag" envconfig:"CONVOY_FEATURE_FLAG"`
EnableFeatureFlag []string `json:"enable_feature_flag" envconfig:"CONVOY_ENABLE_FEATURE_FLAG"`
RetentionPolicy RetentionPolicyConfiguration `json:"retention_policy"`
Analytics AnalyticsConfiguration `json:"analytics"`
StoragePolicy StoragePolicyConfiguration `json:"storage_policy"`
Expand Down
9 changes: 6 additions & 3 deletions ee/cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"github.com/frain-dev/convoy/cmd/ff"
"os"

"github.com/frain-dev/convoy/cmd/bootstrap"
Expand Down Expand Up @@ -45,7 +46,7 @@ func main() {
var dbPassword string
var dbDatabase string

var fflag string
var fflag []string

var redisPort int
var redisHost string
Expand Down Expand Up @@ -91,7 +92,8 @@ func main() {
c.Flags().StringVar(&redisDatabase, "redis-database", "", "Redis database")
c.Flags().IntVar(&redisPort, "redis-port", 0, "Redis Port")

c.Flags().StringVar(&fflag, "feature-flag", "", "Enable feature flags (experimental)")
c.Flags().StringSliceVar(&fflag, "enable-feature-flag", []string{}, "List of feature flags to enable e.g. \"full-text-search,prometheus\"")

c.Flags().BoolVar(&enableProfiling, "enable-profiling", false, "Enable profiling")

// tracing
Expand All @@ -103,7 +105,7 @@ func main() {
c.Flags().StringVar(&otelAuthHeaderValue, "otel-auth-header-value", "", "OTel backend auth header value")

// metrics
c.Flags().StringVar(&metricsBackend, "metrics-backend", "prometheus", "Metrics backend e.g. prometheus. ('experimental' feature flag level required")
c.Flags().StringVar(&metricsBackend, "metrics-backend", "prometheus", "Metrics backend e.g. prometheus. ('prometheus' feature flag required")
c.Flags().Uint64Var(&prometheusMetricsSampleTime, "metrics-prometheus-sample-time", 5, "Prometheus metrics sample time")

c.Flags().Uint64Var(&maxRetrySeconds, "max-retry-seconds", 7200, "Max retry seconds exponential backoff")
Expand All @@ -122,6 +124,7 @@ func main() {
c.AddCommand(ingest.AddIngestCommand(app))
c.AddCommand(stream.AddStreamCommand(app))
c.AddCommand(bootstrap.AddBootstrapCommand(app))
c.AddCommand(ff.AddFeatureFlagsCommand())

if err := c.Execute(); err != nil {
slog.Fatal(err)
Expand Down
2 changes: 1 addition & 1 deletion ee/cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func StartConvoyServer(a *cli.App) error {
a.Logger.WithError(err).Fatal("failed to initialize realm chain")
}

flag, err := fflag.NewFFlag()
flag, err := fflag.NewFFlag(&cfg)
if err != nil {
a.Logger.WithError(err).Fatal("failed to create fflag controller")
}
Expand Down
87 changes: 78 additions & 9 deletions internal/pkg/fflag/fflag.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,102 @@
package fflag

import (
"errors"
"fmt"
"github.com/frain-dev/convoy/config"
"os"
"sort"
"text/tabwriter"
)

var ErrFeatureNotEnabled = errors.New("this feature is not enabled")

type (
FeatureFlagKey string
)

const (
Prometheus FeatureFlagKey = "prometheus"
Prometheus FeatureFlagKey = "prometheus"
FullTextSearch FeatureFlagKey = "full-text-search"
)

type (
FeatureFlagState bool
)

const (
enabled FeatureFlagState = true
disabled FeatureFlagState = false
)

var features = map[FeatureFlagKey]config.FlagLevel{
Prometheus: config.ExperimentalFlagLevel,
var DefaultFeaturesState = map[FeatureFlagKey]FeatureFlagState{
Prometheus: disabled,
FullTextSearch: disabled,
}

type FFlag struct{}
type FFlag struct {
Features map[FeatureFlagKey]FeatureFlagState
}

func NewFFlag() (*FFlag, error) {
return &FFlag{}, nil
func NewFFlag(c *config.Configuration) (*FFlag, error) {
f := &FFlag{
Features: clone(DefaultFeaturesState),
}
for _, flag := range c.EnableFeatureFlag {
switch flag {
case string(Prometheus):
f.Features[Prometheus] = enabled
case string(FullTextSearch):
f.Features[FullTextSearch] = enabled
}
}
return f, nil
}

func (c *FFlag) CanAccessFeature(key FeatureFlagKey, cfg *config.Configuration) bool {
func clone(src map[FeatureFlagKey]FeatureFlagState) map[FeatureFlagKey]FeatureFlagState {
dst := make(map[FeatureFlagKey]FeatureFlagState)
for k, v := range src {
dst[k] = v
}
return dst
}

func (c *FFlag) CanAccessFeature(key FeatureFlagKey) bool {
// check for this feature in our feature map
flagLevel, ok := features[key]
state, ok := c.Features[key]
if !ok {
return false
}

return flagLevel <= cfg.FeatureFlag // if the feature level is less than or equal to the cfg level, we can access the feature
return bool(state)
}

func (c *FFlag) ListFeatures() error {
keys := make([]string, 0, len(c.Features))

for k := range c.Features {
keys = append(keys, string(k))
}
sort.Strings(keys)

w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
_, err := fmt.Fprintln(w, "Features\tState")
if err != nil {
return err
}

for _, k := range keys {
stateBool := c.Features[FeatureFlagKey(k)]
state := "disabled"
if stateBool {
state = "enabled"
}

_, err := fmt.Fprintf(w, "%s\t%s\n", k, state)
if err != nil {
return err
}
}

return w.Flush()
}
Loading

0 comments on commit ed0b5bb

Please sign in to comment.