diff --git a/ee/query-service/license/manager.go b/ee/query-service/license/manager.go index 306fa5a8d1..a3e9ba0771 100644 --- a/ee/query-service/license/manager.go +++ b/ee/query-service/license/manager.go @@ -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} } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 1d648f4651..473d253585 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -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) diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index 2fed317973..bfd4042d22 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -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 } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 2b46ae8fed..cbb8a807fa 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -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 diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index 6e3af77a5e..46b3d4651f 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -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" diff --git a/pkg/query-service/featureManager/manager.go b/pkg/query-service/featureManager/manager.go new file mode 100644 index 0000000000..1c8c953982 --- /dev/null +++ b/pkg/query-service/featureManager/manager.go @@ -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 +}