Feature flagging (#1674)

* feat: introduce feature flagging via env variables
* refactor: enable sorting by default for users
This commit is contained in:
Vishal Sharma 2022-11-09 08:30:00 +05:30 committed by GitHub
parent 36315fcf9c
commit 674883cd18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 124 additions and 9 deletions

View File

@ -10,6 +10,8 @@ import (
"sync"
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
"go.signoz.io/signoz/ee/query-service/model"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
@ -92,6 +94,8 @@ func (lm *Manager) SetActive(l *model.License) {
lm.activeLicense = l
lm.activeFeatures = l.FeatureSet
// set default features
setDefaultFeatures(lm)
if !lm.validatorRunning {
// we want to make sure only one validator runs,
// we already have lock() so good to go
@ -101,7 +105,13 @@ func (lm *Manager) SetActive(l *model.License) {
}
// LoadActiveLicense loads the most recent active licenseex
func setDefaultFeatures(lm *Manager) {
for k, v := range baseconstants.DEFAULT_FEATURE_SET {
lm.activeFeatures[k] = v
}
}
// LoadActiveLicense loads the most recent active license
func (lm *Manager) LoadActiveLicense() error {
var err error
active, err := lm.repo.GetActiveLicense(context.Background())
@ -111,7 +121,10 @@ func (lm *Manager) LoadActiveLicense() error {
if active != nil {
lm.SetActive(active)
} else {
zap.S().Info("No active license found.")
zap.S().Info("No active license found, defaulting to basic plan")
// if no active license is found, we default to basic(free) plan with all default features
lm.activeFeatures = basemodel.BasicPlan
setDefaultFeatures(lm)
}
return nil
@ -278,8 +291,11 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
// CheckFeature will be internally used by backend routines
// for feature gating
func (lm *Manager) CheckFeature(featureKey string) error {
if _, ok := lm.activeFeatures[featureKey]; ok {
return nil
if value, ok := lm.activeFeatures[featureKey]; ok {
if value {
return nil
}
return basemodel.ErrFeatureUnavailable{Key: featureKey}
}
return basemodel.ErrFeatureUnavailable{Key: featureKey}
}

View File

@ -358,6 +358,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
router.HandleFunc("/api/v1/settings/ttl", ViewAccess(aH.getTTL)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/version", OpenAccess(aH.getVersion)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/featureFlags", OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/getSpanFilters", ViewAccess(aH.getSpanFilters)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/getTagFilters", ViewAccess(aH.getTagFilters)).Methods(http.MethodPost)
@ -1422,7 +1423,7 @@ func (aH *APIHandler) getSpanFilters(w http.ResponseWriter, r *http.Request) {
func (aH *APIHandler) getFilteredSpans(w http.ResponseWriter, r *http.Request) {
query, err := parseFilteredSpansRequest(r)
query, err := parseFilteredSpansRequest(r, aH)
if aH.HandleError(w, err, http.StatusBadRequest) {
return
}
@ -1533,6 +1534,20 @@ func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
aH.WriteJSON(w, r, map[string]string{"version": version, "ee": "N"})
}
func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
featureSet := aH.FF().GetFeatureFlags()
aH.Respond(w, featureSet)
}
func (aH *APIHandler) FF() interfaces.FeatureLookup {
return aH.featureFlags
}
func (aH *APIHandler) CheckFeature(f string) bool {
err := aH.FF().CheckFeature(f)
return err == nil
}
// inviteUser is used to invite a user. It is used by an admin api.
func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) {
req, err := parseInviteRequest(r)

View File

@ -255,7 +255,7 @@ func parseSpanFilterRequestBody(r *http.Request) (*model.SpanFilterParams, error
return postData, nil
}
func parseFilteredSpansRequest(r *http.Request) (*model.GetFilteredSpansParams, error) {
func parseFilteredSpansRequest(r *http.Request, aH *APIHandler) (*model.GetFilteredSpansParams, error) {
var postData *model.GetFilteredSpansParams
err := json.NewDecoder(r.Body).Decode(&postData)
@ -277,6 +277,20 @@ func parseFilteredSpansRequest(r *http.Request) (*model.GetFilteredSpansParams,
postData.Limit = 10
}
if len(postData.Order) != 0 {
if postData.Order != constants.Ascending && postData.Order != constants.Descending {
return nil, errors.New("order param is not in correct format")
}
if postData.OrderParam != constants.Duration && postData.OrderParam != constants.Timestamp {
return nil, errors.New("order param is not in correct format")
}
if postData.OrderParam == constants.Duration && !aH.CheckFeature(constants.DurationSort) {
return nil, model.ErrFeatureUnavailable{Key: constants.DurationSort}
} else if postData.OrderParam == constants.Timestamp && !aH.CheckFeature(constants.TimestampSort) {
return nil, model.ErrFeatureUnavailable{Key: constants.TimestampSort}
}
}
return postData, nil
}

View File

@ -19,6 +19,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/dao"
"go.signoz.io/signoz/pkg/query-service/featureManager"
"go.signoz.io/signoz/pkg/query-service/healthcheck"
am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
"go.signoz.io/signoz/pkg/query-service/interfaces"
@ -77,6 +78,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
localDB.SetMaxOpenConns(10)
// initiate feature manager
fm := featureManager.StartManager()
readerReady := make(chan bool)
var reader interfaces.Reader
@ -98,9 +103,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
telemetry.GetInstance().SetReader(reader)
apiHandler, err := NewAPIHandler(APIHandlerOpts{
Reader: reader,
AppDao: dao.DB(),
RuleManager: rm,
Reader: reader,
AppDao: dao.DB(),
RuleManager: rm,
FeatureFlags: fm,
})
if err != nil {
return nil, err

View File

@ -28,6 +28,9 @@ const TraceTTL = "traces"
const MetricsTTL = "metrics"
const LogsTTL = "logs"
const DurationSort = "DurationSort"
const TimestampSort = "TimestampSort"
func GetAlertManagerApiPrefix() string {
if os.Getenv("ALERTMANAGER_API_PREFIX") != "" {
return os.Getenv("ALERTMANAGER_API_PREFIX")
@ -40,6 +43,33 @@ var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/rout
var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db")
var DurationSortFeature = GetOrDefaultEnv("DURATION_SORT_FEATURE", "true")
var TimestampSortFeature = GetOrDefaultEnv("TIMESTAMP_SORT_FEATURE", "true")
func IsDurationSortFeatureEnabled() bool {
isDurationSortFeatureEnabledStr := DurationSortFeature
isDurationSortFeatureEnabledBool, err := strconv.ParseBool(isDurationSortFeatureEnabledStr)
if err != nil {
return false
}
return isDurationSortFeatureEnabledBool
}
func IsTimestampSortFeatureEnabled() bool {
isTimestampSortFeatureEnabledStr := TimestampSortFeature
isTimestampSortFeatureEnabledBool, err := strconv.ParseBool(isTimestampSortFeatureEnabledStr)
if err != nil {
return false
}
return isTimestampSortFeatureEnabledBool
}
var DEFAULT_FEATURE_SET = model.FeatureSet{
DurationSort: IsDurationSortFeatureEnabled(),
TimestampSort: IsTimestampSortFeatureEnabled(),
}
const (
TraceID = "traceID"
ServiceName = "serviceName"

View File

@ -0,0 +1,34 @@
package featureManager
import (
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/model"
)
type FeatureManager struct {
activeFeatures model.FeatureSet
}
func StartManager() *FeatureManager {
fM := &FeatureManager{
activeFeatures: constants.DEFAULT_FEATURE_SET,
}
return fM
}
// CheckFeature will be internally used by backend routines
// for feature gating
func (fm *FeatureManager) CheckFeature(featureKey string) error {
if value, ok := fm.activeFeatures[featureKey]; ok {
if value {
return nil
}
return model.ErrFeatureUnavailable{Key: featureKey}
}
return model.ErrFeatureUnavailable{Key: featureKey}
}
// GetFeatureFlags returns current active features
func (fm *FeatureManager) GetFeatureFlags() model.FeatureSet {
return fm.activeFeatures
}