Compare commits

...

27 Commits

Author SHA1 Message Date
grandwizard28
4af0503a89 chore(openfeature): integrate with openfeature 2025-04-07 01:35:16 +05:30
grandwizard28
fa2f63bc0d Merge branch 'remove-ff-unused' into licensing
# Conflicts:
#	ee/query-service/model/license.go
#	pkg/query-service/app/clickhouseReader/reader.go
#	pkg/query-service/app/querier/querier.go
#	pkg/query-service/constants/constants.go
#	pkg/query-service/featureManager/manager.go
#	pkg/query-service/model/featureSet.go
#	pkg/query-service/rules/manager.go
2025-04-06 21:11:24 +05:30
grandwizard28
084def0ba9 chore(ff): remove AWS_INTEGRATION 2025-04-06 17:03:42 +05:30
grandwizard28
b77bca7b71 Merge branch 'main' into remove-ff-unused 2025-04-05 23:38:34 +05:30
grandwizard28
a6bd5f9e33 Merge branch 'remove-ff-querier' into remove-ff-unused 2025-04-05 23:31:47 +05:30
Vibhu Pandey
2ae34cbcae Merge branch 'main' into remove-ff-querier 2025-04-05 23:30:15 +05:30
grandwizard28
46e8182ab1 Merge branch 'remove-ff-querier' into remove-ff-unused 2025-04-05 18:48:58 +05:30
grandwizard28
7bc2a614f9 Merge branch 'remove-ff-sort' into remove-ff-querier 2025-04-05 18:48:53 +05:30
grandwizard28
88227c6992 Merge branch 'main' into remove-ff-sort 2025-04-05 18:48:46 +05:30
grandwizard28
6aa8f18018 chore(ff): remove all unused ffs 2025-04-05 01:38:06 +05:30
grandwizard28
a389901b8d chore(ff): remove infra_metrics and custom_metrics 2025-04-05 00:34:49 +05:30
grandwizard28
30e2581bbc fix(querier): remove ff 2025-04-05 00:02:37 +05:30
grandwizard28
ff1e46766f Merge branch 'remove-ff-ruler' into remove-ff-sort 2025-04-04 23:47:15 +05:30
grandwizard28
8e48e58f9b Merge branch 'main' into remove-ff-ruler 2025-04-04 23:47:13 +05:30
grandwizard28
bb7301bc9f fix(duration|timestamp): remove duration/timestamp sort 2025-04-04 23:46:51 +05:30
grandwizard28
7b5eae84d5 feat(ruler): remove ff 2025-04-04 22:21:51 +05:30
grandwizard28
0aeb1009c6 Merge branch 'remove-prefer-rpm' into remove-ff-ruler 2025-04-04 22:18:56 +05:30
grandwizard28
ab3b250629 fix(ff): fix test cases 2025-04-04 20:44:24 +05:30
grandwizard28
765354b1ee Merge branch 'main' into remove-prefer-rpm 2025-04-04 20:29:16 +05:30
grandwizard28
a3daf43186 fix(ff): remove prefer rpm 2025-04-04 20:28:09 +05:30
grandwizard28
1649c0e26f chore(ff): remove from ruler 2025-04-04 19:55:47 +05:30
grandwizard28
142ad8adc4 Merge branch 'remove-ff-smart-detail' into remove-ff-ruler
# Conflicts:
#	pkg/query-service/rules/threshold_rule_test.go
2025-04-04 19:45:42 +05:30
grandwizard28
cda94d0325 chore(ff): remove unused SMART_TRACE_DETAIL feature flag 2025-04-04 19:34:20 +05:30
grandwizard28
51ae2df8d5 feat(ruler): remove ff 2025-04-04 19:17:58 +05:30
grandwizard28
ea4e9988a5 feat(licensing): second commit 2025-04-04 18:55:11 +05:30
grandwizard28
74489efeef feat(featuretypes): add feature types 2025-04-02 19:36:40 +05:30
grandwizard28
834d75fbbd feat(licensing): first commit 2025-04-02 19:35:06 +05:30
58 changed files with 1948 additions and 1087 deletions

1
.gitignore vendored
View File

@@ -83,3 +83,4 @@ queries.active
# .devenv tmp files
.devenv/**/tmp/**
.qodo

View File

@@ -0,0 +1,31 @@
package licensingserver
import (
"time"
"github.com/SigNoz/signoz/pkg/licensing"
)
type Config struct {
PollingConfig PollingConfig `mapstructure:"polling"`
}
type PollingConfig struct {
Interval time.Duration `mapstructure:"interval"`
}
func NewConfig() Config {
return Config{
PollingConfig: PollingConfig{
Interval: 24 * time.Hour,
},
}
}
func NewConfigFromLicensingConfig(config licensing.Config) Config {
return Config{
PollingConfig: PollingConfig{
Interval: config.PollingConfig.Interval,
},
}
}

View File

@@ -0,0 +1,72 @@
package licensingserver
import (
"context"
"log/slog"
"sync"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
)
type Server struct {
logger *slog.Logger
cfg Config
orgID valuer.UUID
zeus zeus.Zeus
store licensetypes.Store
license licensetypes.License
mtx sync.RWMutex
}
func NewServer(logger *slog.Logger, config Config, orgID valuer.UUID, zeus zeus.Zeus, store licensetypes.Store) *Server {
return &Server{
logger: logger,
cfg: config,
orgID: orgID,
zeus: zeus,
store: store,
license: licensetypes.NewNoop(),
}
}
func (server *Server) Fetch(ctx context.Context) error {
license, err := server.store.GetLatest(ctx, server.orgID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil
}
return err
}
fetchedLicense, err := server.zeus.GetLicense(ctx, license.Key())
if err != nil {
return err
}
return server.SetLicense(ctx, fetchedLicense)
}
func (server *Server) SetLicense(ctx context.Context, license licensetypes.License) error {
server.mtx.Lock()
defer server.mtx.Unlock()
server.license = license
return nil
}
func (server *Server) GetLicense(ctx context.Context) licensetypes.License {
server.mtx.RLock()
defer server.mtx.RUnlock()
return server.license
}

View File

@@ -0,0 +1,35 @@
package sqllicensingstore
import (
"context"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) licensetypes.Store {
return &store{
sqlstore: sqlstore,
}
}
func (store *store) Set(ctx context.Context, license licensetypes.License) error {
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID) ([]licensetypes.License, error) {
return nil, nil
}
func (store *store) GetLatest(ctx context.Context, orgID valuer.UUID) (licensetypes.License, error) {
return nil, nil
}
func (store *store) ListOrgs(ctx context.Context) ([]valuer.UUID, error) {
return nil, nil
}

View File

@@ -0,0 +1,109 @@
package pollinglicensing
import (
"context"
"time"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
)
type provider struct {
config licensing.Config
settings factory.ScopedProviderSettings
zeus zeus.Zeus
service *Service
store licensetypes.Store
stopC chan struct{}
}
func NewFactory(zeus zeus.Zeus, sqlstore sqlstore.SQLStore) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return factory.NewProviderFactory(factory.MustNewName("sql"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
return New(ctx, providerSettings, config, zeus, sqlstore)
})
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config, zeus zeus.Zeus, sqlstore sqlstore.SQLStore) (licensing.Licensing, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/licensing/pollinglicensing")
store := sqllicensingstore.NewStore(sqlstore)
return &provider{
config: config,
settings: settings,
zeus: zeus,
service: NewService(ctx, settings, config, store, zeus),
stopC: make(chan struct{}),
}, nil
}
func (provider *provider) Start(ctx context.Context) error {
if err := provider.service.SyncServers(ctx); err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to sync licensing servers", "error", err)
return err
}
ticker := time.NewTicker(provider.config.PollingConfig.Interval)
defer ticker.Stop()
for {
select {
case <-provider.stopC:
return nil
case <-ticker.C:
if err := provider.service.SyncServers(ctx); err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to sync licensing servers", "error", err)
}
}
}
}
func (provider *provider) GetLatestLicense(ctx context.Context, orgID valuer.UUID) (licensetypes.License, error) {
server, err := provider.service.getServer(orgID)
if err != nil {
return nil, err
}
return server.GetLicense(ctx), nil
}
func (provider *provider) GetLicenses(ctx context.Context, orgID valuer.UUID, params licensetypes.GettableLicenseParams) (licensetypes.GettableLicenses, error) {
if params.Active != nil {
if *params.Active {
license, err := provider.GetLatestLicense(ctx, orgID)
if err != nil {
return nil, err
}
return licensetypes.GettableLicenses{license}, nil
}
}
licenses, err := provider.store.Get(ctx, orgID)
if err != nil {
return nil, err
}
return licenses, nil
}
func (provider *provider) SetLicense(ctx context.Context, orgID valuer.UUID, key string) error {
license, err := provider.zeus.GetLicense(ctx, key)
if err != nil {
return err
}
if err := provider.store.Set(ctx, license); err != nil {
return err
}
return provider.service.SyncOrgServer(ctx, orgID)
}
func (provider *provider) Stop(ctx context.Context) error {
close(provider.stopC)
return nil
}

View File

@@ -0,0 +1,103 @@
package pollinglicensing
import (
"context"
"sync"
"github.com/SigNoz/signoz/ee/licensing/licensingserver"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
)
type Service struct {
// config is the config for the licensing service
config licensing.Config
// store is the store for the licensing service
store licensetypes.Store
// zeus
zeus zeus.Zeus
// settings is the settings for the licensing service
settings factory.ScopedProviderSettings
// Map of organization id to alertmanager server
servers map[valuer.UUID]*licensingserver.Server
// Mutex to protect the servers map
serversMtx sync.RWMutex
}
func NewService(ctx context.Context, settings factory.ScopedProviderSettings, config licensing.Config, store licensetypes.Store, zeus zeus.Zeus) *Service {
service := &Service{
config: config,
store: store,
zeus: zeus,
settings: settings,
servers: make(map[valuer.UUID]*licensingserver.Server),
serversMtx: sync.RWMutex{},
}
return service
}
func (service *Service) SyncServers(ctx context.Context) error {
orgIDs, err := service.store.ListOrgs(ctx)
if err != nil {
return err
}
service.serversMtx.Lock()
for _, orgID := range orgIDs {
// If the server is not present, create it and sync the config
if _, ok := service.servers[orgID]; !ok {
server := licensingserver.NewServer(service.settings.Logger(), licensingserver.NewConfigFromLicensingConfig(service.config), orgID, service.zeus, service.store)
service.servers[orgID] = server
}
err = service.servers[orgID].Fetch(ctx)
if err != nil {
service.settings.Logger().Error("failed to fetch license for licensing server", "orgID", orgID, "error", err)
continue
}
}
service.serversMtx.Unlock()
return nil
}
func (service *Service) SyncOrgServer(ctx context.Context, orgID valuer.UUID) error {
service.serversMtx.Lock()
defer service.serversMtx.Unlock()
_, ok := service.servers[orgID]
if !ok {
server := licensingserver.NewServer(service.settings.Logger(), licensingserver.NewConfigFromLicensingConfig(service.config), orgID, service.zeus, service.store)
service.servers[orgID] = server
}
err := service.servers[orgID].Fetch(ctx)
if err != nil {
service.settings.Logger().Error("failed to fetch license for licensing server", "orgID", orgID, "error", err)
return err
}
return nil
}
func (service *Service) getServer(orgID valuer.UUID) (*licensingserver.Server, error) {
service.serversMtx.RLock()
defer service.serversMtx.RUnlock()
server, ok := service.servers[orgID]
if !ok {
return nil, errors.Newf(errors.TypeNotFound, licensing.ErrCodeLicensingServerNotFound, "server not found for %s", orgID.StringValue())
}
return server, nil
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/SigNoz/signoz/ee/query-service/dao"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/ee/query-service/interfaces"
"github.com/SigNoz/signoz/ee/query-service/license"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
@@ -29,7 +28,7 @@ import (
)
type APIHandlerOptions struct {
DataConnector interfaces.DataConnector
DataConnector baseint.Reader
SkipConfig *basemodel.SkipConfig
PreferSpanMetrics bool
AppDao dao.ModelDao

View File

@@ -222,7 +222,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// start the usagemanager
usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.Config.TelemetryStore.Clickhouse.DSN)
usageManager, err := usage.New(lm.GetRepo(), serverOptions.SigNoz.TelemetryStore, serverOptions.SigNoz.Zeus)
if err != nil {
return nil, err
}

View File

@@ -1,11 +0,0 @@
package interfaces
import (
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
)
// Connector defines methods for interaction
// with o11y data. for example - clickhouse
type DataConnector interface {
baseint.Reader
}

View File

@@ -14,8 +14,8 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/zeus"
validate "github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
"github.com/SigNoz/signoz/ee/query-service/model"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
@@ -29,6 +29,7 @@ var validationFrequency = 24 * 60 * time.Minute
type Manager struct {
repo *Repo
zeus zeus.Zeus
mutex sync.Mutex
validatorRunning bool
// end the license validation, this is important to gracefully
@@ -45,7 +46,7 @@ type Manager struct {
activeFeatures basemodel.FeatureSet
}
func StartManager(db *sqlx.DB, store sqlstore.SQLStore, features ...basemodel.Feature) (*Manager, error) {
func StartManager(db *sqlx.DB, store sqlstore.SQLStore, zeus zeus.Zeus, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
@@ -53,6 +54,7 @@ func StartManager(db *sqlx.DB, store sqlstore.SQLStore, features ...basemodel.Fe
repo := NewLicenseRepo(db, store)
m := &Manager{
repo: &repo,
zeus: zeus,
}
if err := m.start(features...); err != nil {
return m, err
@@ -173,14 +175,12 @@ func (lm *Manager) ValidatorV3(ctx context.Context) {
}
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
if apiError != nil {
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError
license, err := lm.zeus.GetLicense(ctx, lm.activeLicenseV3.Key)
if err != nil {
return model.BadRequest(errors.Wrap(err, "failed to get license"))
}
err := lm.repo.UpdateLicenseV3(ctx, license)
err = lm.repo.UpdateLicenseV3(ctx, license)
if err != nil {
return model.BadRequest(errors.Wrap(err, "failed to update the new license"))
}
@@ -247,10 +247,9 @@ func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseRe
}
}()
license, apiError := validate.ValidateLicenseV3(licenseKey)
if apiError != nil {
zap.L().Error("failed to get the license", zap.Error(apiError.Err))
return nil, apiError
license, errv2 := lm.zeus.GetLicense(ctx, lm.activeLicenseV3.Key)
if errv2 != nil {
return nil, model.BadRequest(errors.Wrap(errv2, "failed to get license"))
}
// insert the new license to the sqlite db

View File

@@ -1,246 +0,0 @@
package model
import (
"encoding/json"
"fmt"
"reflect"
"time"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/pkg/errors"
)
type License struct {
Key string `json:"key" db:"key"`
ActivationId string `json:"activationId" db:"activationId"`
CreatedAt time.Time `db:"created_at"`
// PlanDetails contains the encrypted plan info
PlanDetails string `json:"planDetails" db:"planDetails"`
// stores parsed license details
LicensePlan
FeatureSet basemodel.FeatureSet
// populated in case license has any errors
ValidationMessage string `db:"validationMessage"`
// used only for sending details to front-end
IsCurrent bool `json:"isCurrent"`
}
func (l *License) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Key string `json:"key" db:"key"`
ActivationId string `json:"activationId" db:"activationId"`
ValidationMessage string `db:"validationMessage"`
IsCurrent bool `json:"isCurrent"`
PlanKey string `json:"planKey"`
ValidFrom time.Time `json:"ValidFrom"`
ValidUntil time.Time `json:"ValidUntil"`
Status string `json:"status"`
}{
Key: l.Key,
ActivationId: l.ActivationId,
IsCurrent: l.IsCurrent,
PlanKey: l.PlanKey,
ValidFrom: time.Unix(l.ValidFrom, 0),
ValidUntil: time.Unix(l.ValidUntil, 0),
Status: l.Status,
ValidationMessage: l.ValidationMessage,
})
}
type LicensePlan struct {
PlanKey string `json:"planKey"`
ValidFrom int64 `json:"validFrom"`
ValidUntil int64 `json:"validUntil"`
Status string `json:"status"`
}
type Licenses struct {
TrialStart int64 `json:"trialStart"`
TrialEnd int64 `json:"trialEnd"`
OnTrial bool `json:"onTrial"`
WorkSpaceBlock bool `json:"workSpaceBlock"`
TrialConvertedToSubscription bool `json:"trialConvertedToSubscription"`
GracePeriodEnd int64 `json:"gracePeriodEnd"`
Licenses []License `json:"licenses"`
}
type SubscriptionServerResp struct {
Status string `json:"status"`
Data Licenses `json:"data"`
}
type Plan struct {
Name string `json:"name"`
}
type LicenseDB struct {
ID string `json:"id"`
Key string `json:"key"`
Data string `json:"data"`
}
type LicenseV3 struct {
ID string
Key string
Data map[string]interface{}
PlanName string
Features basemodel.FeatureSet
Status string
IsCurrent bool
ValidFrom int64
ValidUntil int64
}
func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) {
var zeroValue T
if val, ok := data[key]; ok {
if value, ok := val.(T); ok {
return value, nil
}
return zeroValue, fmt.Errorf("%s key is not a valid %s", key, reflect.TypeOf(zeroValue))
}
return zeroValue, fmt.Errorf("%s key is missing", key)
}
func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
var features basemodel.FeatureSet
// extract id from data
licenseID, err := extractKeyFromMapStringInterface[string](data, "id")
if err != nil {
return nil, err
}
delete(data, "id")
// extract key from data
licenseKey, err := extractKeyFromMapStringInterface[string](data, "key")
if err != nil {
return nil, err
}
delete(data, "key")
// extract status from data
status, err := extractKeyFromMapStringInterface[string](data, "status")
if err != nil {
return nil, err
}
planMap, err := extractKeyFromMapStringInterface[map[string]any](data, "plan")
if err != nil {
return nil, err
}
planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
if err != nil {
return nil, err
}
// if license status is invalid then default it to basic
if status == LicenseStatusInvalid {
planName = PlanNameBasic
}
featuresFromZeus := basemodel.FeatureSet{}
if _features, ok := data["features"]; ok {
featuresData, err := json.Marshal(_features)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal features data")
}
if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal features data")
}
}
switch planName {
case PlanNameTeams:
features = append(features, ProPlan...)
case PlanNameEnterprise:
features = append(features, EnterprisePlan...)
case PlanNameBasic:
features = append(features, BasicPlan...)
default:
features = append(features, BasicPlan...)
}
if len(featuresFromZeus) > 0 {
for _, feature := range featuresFromZeus {
exists := false
for i, existingFeature := range features {
if existingFeature.Name == feature.Name {
features[i] = feature // Replace existing feature
exists = true
break
}
}
if !exists {
features = append(features, feature) // Append if it doesn't exist
}
}
}
data["features"] = features
_validFrom, err := extractKeyFromMapStringInterface[float64](data, "valid_from")
if err != nil {
_validFrom = 0
}
validFrom := int64(_validFrom)
_validUntil, err := extractKeyFromMapStringInterface[float64](data, "valid_until")
if err != nil {
_validUntil = 0
}
validUntil := int64(_validUntil)
return &LicenseV3{
ID: licenseID,
Key: licenseKey,
Data: data,
PlanName: planName,
Features: features,
ValidFrom: validFrom,
ValidUntil: validUntil,
Status: status,
}, nil
}
func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}) (*LicenseV3, error) {
licenseDataWithIdAndKey := data
licenseDataWithIdAndKey["id"] = id
licenseDataWithIdAndKey["key"] = key
return NewLicenseV3(licenseDataWithIdAndKey)
}
func ConvertLicenseV3ToLicenseV2(l *LicenseV3) *License {
planKeyFromPlanName, ok := MapOldPlanKeyToNewPlanName[l.PlanName]
if !ok {
planKeyFromPlanName = Basic
}
return &License{
Key: l.Key,
ActivationId: "",
PlanDetails: "",
FeatureSet: l.Features,
ValidationMessage: "",
IsCurrent: l.IsCurrent,
LicensePlan: LicensePlan{
PlanKey: planKeyFromPlanName,
ValidFrom: l.ValidFrom,
ValidUntil: l.ValidUntil,
Status: l.Status},
}
}
type CheckoutRequest struct {
SuccessURL string `json:"url"`
}
type PortalRequest struct {
SuccessURL string `json:"url"`
}

View File

@@ -1,170 +0,0 @@
package model
import (
"encoding/json"
"testing"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewLicenseV3(t *testing.T) {
testCases := []struct {
name string
data []byte
pass bool
expected *LicenseV3
error error
}{
{
name: "Error for missing license id",
data: []byte(`{}`),
pass: false,
error: errors.New("id key is missing"),
},
{
name: "Error for license id not being a valid string",
data: []byte(`{"id": 10}`),
pass: false,
error: errors.New("id key is not a valid string"),
},
{
name: "Error for missing license key",
data: []byte(`{"id":"does-not-matter"}`),
pass: false,
error: errors.New("key key is missing"),
},
{
name: "Error for invalid string license key",
data: []byte(`{"id":"does-not-matter","key":10}`),
pass: false,
error: errors.New("key key is not a valid string"),
},
{
name: "Error for missing license status",
data: []byte(`{"id":"does-not-matter", "key": "does-not-matter","category":"FREE"}`),
pass: false,
error: errors.New("status key is missing"),
},
{
name: "Error for invalid string license status",
data: []byte(`{"id":"does-not-matter","key": "does-not-matter", "category":"FREE", "status":10}`),
pass: false,
error: errors.New("status key is not a valid string"),
},
{
name: "Error for missing license plan",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE"}`),
pass: false,
error: errors.New("plan key is missing"),
},
{
name: "Error for invalid json license plan",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":10}`),
pass: false,
error: errors.New("plan key is not a valid map[string]interface {}"),
},
{
name: "Error for invalid license plan",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{}}`),
pass: false,
error: errors.New("name key is missing"),
},
{
name: "Parse the entire license properly",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"category": "FREE",
"status": "ACTIVE",
"valid_from": float64(1730899309),
"valid_until": float64(-1),
},
PlanName: PlanNameTeams,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "ACTIVE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
{
name: "Fallback to basic plan if license status is invalid",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"category": "FREE",
"status": "INVALID",
"valid_from": float64(1730899309),
"valid_until": float64(-1),
},
PlanName: PlanNameBasic,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "INVALID",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
{
name: "fallback states for validFrom and validUntil",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from":1234.456,"valid_until":5678.567}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"valid_from": 1234.456,
"valid_until": 5678.567,
"category": "FREE",
"status": "ACTIVE",
},
PlanName: PlanNameTeams,
ValidFrom: 1234,
ValidUntil: 5678,
Status: "ACTIVE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
}
for _, tc := range testCases {
var licensePayload map[string]interface{}
err := json.Unmarshal(tc.data, &licensePayload)
require.NoError(t, err)
license, err := NewLicenseV3(licensePayload)
if license != nil {
license.Features = make(model.FeatureSet, 0)
delete(license.Data, "features")
}
if tc.pass {
require.NoError(t, err)
require.NotNil(t, license)
assert.Equal(t, tc.expected, license)
} else {
require.Error(t, err)
assert.EqualError(t, err, tc.error.Error())
require.Nil(t, license)
}
}
}

View File

@@ -1,7 +1,6 @@
package model
import (
"github.com/SigNoz/signoz/pkg/query-service/constants"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
)
@@ -24,7 +23,6 @@ var (
LicenseStatusInvalid = "INVALID"
)
const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
@@ -38,83 +36,6 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.OSS,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: DisableUpsell,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.CustomMetricsFunction,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.QueryBuilderPanels,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.QueryBuilderAlerts,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelSlack,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelWebhook,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelPagerduty,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelOpsgenie,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelMsTeams,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.UseSpanMetrics,
Active: false,
@@ -143,135 +64,6 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.HostsInfraMonitoring,
Active: constants.EnableHostsInfraMonitoring(),
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.TraceFunnels,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
basemodel.Feature{
Name: SSO,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.OSS,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.CustomMetricsFunction,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.QueryBuilderPanels,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.QueryBuilderAlerts,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelSlack,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelWebhook,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelPagerduty,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelOpsgenie,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelMsTeams,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AnomalyDetection,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.HostsInfraMonitoring,
Active: constants.EnableHostsInfraMonitoring(),
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.TraceFunnels,
Active: false,
@@ -289,76 +81,6 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.OSS,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.CustomMetricsFunction,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.QueryBuilderPanels,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.QueryBuilderAlerts,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelSlack,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelWebhook,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelPagerduty,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelOpsgenie,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelMsTeams,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.UseSpanMetrics,
Active: false,
@@ -401,13 +123,6 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.HostsInfraMonitoring,
Active: constants.EnableHostsInfraMonitoring(),
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.TraceFunnels,
Active: false,

View File

@@ -4,22 +4,21 @@ import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"
"sync/atomic"
"time"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/go-co-op/gocron"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/SigNoz/signoz/ee/query-service/dao"
licenseserver "github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
"github.com/SigNoz/signoz/ee/query-service/license"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/utils/encryption"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/zeus"
)
const (
@@ -34,35 +33,20 @@ var (
)
type Manager struct {
clickhouseConn clickhouse.Conn
licenseRepo *license.Repo
scheduler *gocron.Scheduler
modelDao dao.ModelDao
tenantID string
telemetryStore telemetrystore.TelemetryStore
licenseRepo *license.Repo
scheduler *gocron.Scheduler
zeus zeus.Zeus
}
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn, chUrl string) (*Manager, error) {
hostNameRegex := regexp.MustCompile(`tcp://(?P<hostname>.*):`)
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(chUrl)
tenantID := ""
if len(hostNameRegexMatches) == 2 {
tenantID = hostNameRegexMatches[1]
tenantID = strings.TrimSuffix(tenantID, "-clickhouse")
}
func New(licenseRepo *license.Repo, telemetryStore telemetrystore.TelemetryStore, zeus zeus.Zeus) (*Manager, error) {
m := &Manager{
// repository: repo,
clickhouseConn: clickhouseConn,
licenseRepo: licenseRepo,
telemetryStore: telemetryStore,
zeus: zeus,
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
modelDao: modelDao,
tenantID: tenantID,
}
return m, nil
}
@@ -120,7 +104,7 @@ func (lm *Manager) UploadUsage() {
for _, db := range dbs {
dbusages := []model.UsageDB{}
err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
err := lm.telemetryStore.ClickhouseDB().Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err))
return
@@ -136,17 +120,6 @@ func (lm *Manager) UploadUsage() {
return
}
zap.L().Info("uploading usage data")
orgName := ""
orgNames, orgError := lm.modelDao.GetOrgs(ctx)
if orgError != nil {
zap.L().Error("failed to get org data: %v", zap.Error(orgError))
}
if len(orgNames) == 1 {
orgName = orgNames[0].Name
}
usagesPayload := []model.Usage{}
for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
@@ -166,8 +139,8 @@ func (lm *Manager) UploadUsage() {
usageData.ExporterID = usage.ExporterID
usageData.Type = usage.Type
usageData.Tenant = "default"
usageData.OrgName = orgName
usageData.TenantId = lm.tenantID
usageData.OrgName = "default"
usageData.TenantId = "default"
usagesPayload = append(usagesPayload, usageData)
}
@@ -176,6 +149,7 @@ func (lm *Manager) UploadUsage() {
LicenseKey: key,
Usage: usagesPayload,
}
lm.UploadUsageWithExponentalBackOff(ctx, payload)
}

View File

@@ -0,0 +1,33 @@
package featuretypes
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
var (
SingleSignOn = featuretypes.MustNewName("SingleSignOn")
)
func NewEnterpriseRegistry() (featuretypes.Registry, error) {
enterpriseRegistry, err := featuretypes.NewRegistry(
&featuretypes.Feature{
Name: SingleSignOn,
Kind: featuretypes.KindBoolean,
Description: "Enable single sign on.",
Stage: featuretypes.StageStable,
Default: true,
},
)
if err != nil {
return nil, err
}
return enterpriseRegistry.MergeOrOverride(featuretypes.MustNewCommunityRegistry()), nil
}
func MustNewEnterpriseRegistry() featuretypes.Registry {
enterpriseRegistry, err := NewEnterpriseRegistry()
if err != nil {
panic(err)
}
return enterpriseRegistry
}

View File

@@ -0,0 +1 @@
package licensetypes

View File

@@ -0,0 +1,245 @@
package licensetypes
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
type License struct {
ID string
Key string
Contents map[string]any
OrgFeatures []*featuretypes.StorableOrgFeature
}
// type License struct {
// Key string `json:"key" db:"key"`
// ActivationId string `json:"activationId" db:"activationId"`
// CreatedAt time.Time `db:"created_at"`
// // PlanDetails contains the encrypted plan info
// PlanDetails string `json:"planDetails" db:"planDetails"`
// // stores parsed license details
// LicensePlan
// FeatureSet basemodel.FeatureSet
// // populated in case license has any errors
// ValidationMessage string `db:"validationMessage"`
// // used only for sending details to front-end
// IsCurrent bool `json:"isCurrent"`
// }
// func (l *License) MarshalJSON() ([]byte, error) {
// return json.Marshal(&struct {
// Key string `json:"key" db:"key"`
// ActivationId string `json:"activationId" db:"activationId"`
// ValidationMessage string `db:"validationMessage"`
// IsCurrent bool `json:"isCurrent"`
// PlanKey string `json:"planKey"`
// ValidFrom time.Time `json:"ValidFrom"`
// ValidUntil time.Time `json:"ValidUntil"`
// Status string `json:"status"`
// }{
// Key: l.Key,
// ActivationId: l.ActivationId,
// IsCurrent: l.IsCurrent,
// PlanKey: l.PlanKey,
// ValidFrom: time.Unix(l.ValidFrom, 0),
// ValidUntil: time.Unix(l.ValidUntil, 0),
// Status: l.Status,
// ValidationMessage: l.ValidationMessage,
// })
// }
// type LicensePlan struct {
// PlanKey string `json:"planKey"`
// ValidFrom int64 `json:"validFrom"`
// ValidUntil int64 `json:"validUntil"`
// Status string `json:"status"`
// }
// type Licenses struct {
// TrialStart int64 `json:"trialStart"`
// TrialEnd int64 `json:"trialEnd"`
// OnTrial bool `json:"onTrial"`
// WorkSpaceBlock bool `json:"workSpaceBlock"`
// TrialConvertedToSubscription bool `json:"trialConvertedToSubscription"`
// GracePeriodEnd int64 `json:"gracePeriodEnd"`
// Licenses []License `json:"licenses"`
// }
// type SubscriptionServerResp struct {
// Status string `json:"status"`
// Data Licenses `json:"data"`
// }
// type Plan struct {
// Name string `json:"name"`
// }
// type LicenseDB struct {
// ID string `json:"id"`
// Key string `json:"key"`
// Data string `json:"data"`
// }
// type LicenseV3 struct {
// ID string
// Key string
// Data map[string]interface{}
// PlanName string
// Features basemodel.FeatureSet
// Status string
// IsCurrent bool
// ValidFrom int64
// ValidUntil int64
// }
// func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) {
// var zeroValue T
// if val, ok := data[key]; ok {
// if value, ok := val.(T); ok {
// return value, nil
// }
// return zeroValue, fmt.Errorf("%s key is not a valid %s", key, reflect.TypeOf(zeroValue))
// }
// return zeroValue, fmt.Errorf("%s key is missing", key)
// }
// func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
// var features basemodel.FeatureSet
// // extract id from data
// licenseID, err := extractKeyFromMapStringInterface[string](data, "id")
// if err != nil {
// return nil, err
// }
// delete(data, "id")
// // extract key from data
// licenseKey, err := extractKeyFromMapStringInterface[string](data, "key")
// if err != nil {
// return nil, err
// }
// delete(data, "key")
// // extract status from data
// status, err := extractKeyFromMapStringInterface[string](data, "status")
// if err != nil {
// return nil, err
// }
// planMap, err := extractKeyFromMapStringInterface[map[string]any](data, "plan")
// if err != nil {
// return nil, err
// }
// planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
// if err != nil {
// return nil, err
// }
// // if license status is invalid then default it to basic
// if status == LicenseStatusInvalid {
// planName = PlanNameBasic
// }
// featuresFromZeus := basemodel.FeatureSet{}
// if _features, ok := data["features"]; ok {
// featuresData, err := json.Marshal(_features)
// if err != nil {
// return nil, errors.Wrap(err, "failed to marshal features data")
// }
// if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
// return nil, errors.Wrap(err, "failed to unmarshal features data")
// }
// }
// switch planName {
// case PlanNameTeams:
// features = append(features, ProPlan...)
// case PlanNameEnterprise:
// features = append(features, EnterprisePlan...)
// case PlanNameBasic:
// features = append(features, BasicPlan...)
// default:
// features = append(features, BasicPlan...)
// }
// if len(featuresFromZeus) > 0 {
// for _, feature := range featuresFromZeus {
// exists := false
// for i, existingFeature := range features {
// if existingFeature.Name == feature.Name {
// features[i] = feature // Replace existing feature
// exists = true
// break
// }
// }
// if !exists {
// features = append(features, feature) // Append if it doesn't exist
// }
// }
// }
// data["features"] = features
// _validFrom, err := extractKeyFromMapStringInterface[float64](data, "valid_from")
// if err != nil {
// _validFrom = 0
// }
// validFrom := int64(_validFrom)
// _validUntil, err := extractKeyFromMapStringInterface[float64](data, "valid_until")
// if err != nil {
// _validUntil = 0
// }
// validUntil := int64(_validUntil)
// return &LicenseV3{
// ID: licenseID,
// Key: licenseKey,
// Data: data,
// PlanName: planName,
// Features: features,
// ValidFrom: validFrom,
// ValidUntil: validUntil,
// Status: status,
// }, nil
// }
// func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}) (*LicenseV3, error) {
// licenseDataWithIdAndKey := data
// licenseDataWithIdAndKey["id"] = id
// licenseDataWithIdAndKey["key"] = key
// return NewLicenseV3(licenseDataWithIdAndKey)
// }
// func ConvertLicenseV3ToLicenseV2(l *LicenseV3) *License {
// planKeyFromPlanName, ok := MapOldPlanKeyToNewPlanName[l.PlanName]
// if !ok {
// planKeyFromPlanName = Basic
// }
// return &License{
// Key: l.Key,
// ActivationId: "",
// PlanDetails: "",
// FeatureSet: l.Features,
// ValidationMessage: "",
// IsCurrent: l.IsCurrent,
// LicensePlan: LicensePlan{
// PlanKey: planKeyFromPlanName,
// ValidFrom: l.ValidFrom,
// ValidUntil: l.ValidUntil,
// Status: l.Status},
// }
// }
// type CheckoutRequest struct {
// SuccessURL string `json:"url"`
// }
// type PortalRequest struct {
// SuccessURL string `json:"url"`
// }

View File

@@ -0,0 +1,160 @@
package licensetypes
// func TestNewLicenseV3(t *testing.T) {
// testCases := []struct {
// name string
// data []byte
// pass bool
// expected *LicenseV3
// error error
// }{
// {
// name: "Error for missing license id",
// data: []byte(`{}`),
// pass: false,
// error: errors.New("id key is missing"),
// },
// {
// name: "Error for license id not being a valid string",
// data: []byte(`{"id": 10}`),
// pass: false,
// error: errors.New("id key is not a valid string"),
// },
// {
// name: "Error for missing license key",
// data: []byte(`{"id":"does-not-matter"}`),
// pass: false,
// error: errors.New("key key is missing"),
// },
// {
// name: "Error for invalid string license key",
// data: []byte(`{"id":"does-not-matter","key":10}`),
// pass: false,
// error: errors.New("key key is not a valid string"),
// },
// {
// name: "Error for missing license status",
// data: []byte(`{"id":"does-not-matter", "key": "does-not-matter","category":"FREE"}`),
// pass: false,
// error: errors.New("status key is missing"),
// },
// {
// name: "Error for invalid string license status",
// data: []byte(`{"id":"does-not-matter","key": "does-not-matter", "category":"FREE", "status":10}`),
// pass: false,
// error: errors.New("status key is not a valid string"),
// },
// {
// name: "Error for missing license plan",
// data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE"}`),
// pass: false,
// error: errors.New("plan key is missing"),
// },
// {
// name: "Error for invalid json license plan",
// data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":10}`),
// pass: false,
// error: errors.New("plan key is not a valid map[string]interface {}"),
// },
// {
// name: "Error for invalid license plan",
// data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{}}`),
// pass: false,
// error: errors.New("name key is missing"),
// },
// {
// name: "Parse the entire license properly",
// data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
// pass: true,
// expected: &LicenseV3{
// ID: "does-not-matter",
// Key: "does-not-matter-key",
// Data: map[string]interface{}{
// "plan": map[string]interface{}{
// "name": "TEAMS",
// },
// "category": "FREE",
// "status": "ACTIVE",
// "valid_from": float64(1730899309),
// "valid_until": float64(-1),
// },
// PlanName: PlanNameTeams,
// ValidFrom: 1730899309,
// ValidUntil: -1,
// Status: "ACTIVE",
// IsCurrent: false,
// Features: model.FeatureSet{},
// },
// },
// {
// name: "Fallback to basic plan if license status is invalid",
// data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
// pass: true,
// expected: &LicenseV3{
// ID: "does-not-matter",
// Key: "does-not-matter-key",
// Data: map[string]interface{}{
// "plan": map[string]interface{}{
// "name": "TEAMS",
// },
// "category": "FREE",
// "status": "INVALID",
// "valid_from": float64(1730899309),
// "valid_until": float64(-1),
// },
// PlanName: PlanNameBasic,
// ValidFrom: 1730899309,
// ValidUntil: -1,
// Status: "INVALID",
// IsCurrent: false,
// Features: model.FeatureSet{},
// },
// },
// {
// name: "fallback states for validFrom and validUntil",
// data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from":1234.456,"valid_until":5678.567}`),
// pass: true,
// expected: &LicenseV3{
// ID: "does-not-matter",
// Key: "does-not-matter-key",
// Data: map[string]interface{}{
// "plan": map[string]interface{}{
// "name": "TEAMS",
// },
// "valid_from": 1234.456,
// "valid_until": 5678.567,
// "category": "FREE",
// "status": "ACTIVE",
// },
// PlanName: PlanNameTeams,
// ValidFrom: 1234,
// ValidUntil: 5678,
// Status: "ACTIVE",
// IsCurrent: false,
// Features: model.FeatureSet{},
// },
// },
// }
// for _, tc := range testCases {
// var licensePayload map[string]interface{}
// err := json.Unmarshal(tc.data, &licensePayload)
// require.NoError(t, err)
// license, err := NewLicenseV3(licensePayload)
// if license != nil {
// license.Features = make(model.FeatureSet, 0)
// delete(license.Data, "features")
// }
// if tc.pass {
// require.NoError(t, err)
// require.NotNil(t, license)
// assert.Equal(t, tc.expected, license)
// } else {
// require.Error(t, err)
// assert.EqualError(t, err, tc.error.Error())
// require.Nil(t, license)
// }
// }
// }

31
ee/zeus/config.go Normal file
View File

@@ -0,0 +1,31 @@
package zeus
import (
"fmt"
neturl "net/url"
"sync"
"github.com/SigNoz/signoz/pkg/zeus"
)
// This will be set via ldflags at build time.
var (
url string = "<unset>"
once sync.Once
GlobalConfig zeus.Config
)
// init initializes and validates the Zeus configuration
func init() {
once.Do(func() {
parsedURL, err := neturl.Parse(url)
if err != nil {
panic(fmt.Errorf("invalid zeus URL: %w", err))
}
GlobalConfig = zeus.Config{URL: parsedURL}
if err := GlobalConfig.Validate(); err != nil {
panic(fmt.Errorf("invalid zeus config: %w", err))
}
})
}

View File

@@ -0,0 +1,61 @@
package implzeus
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/client"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/metertypes"
"github.com/SigNoz/signoz/pkg/zeus"
)
type Provider struct {
settings factory.ScopedProviderSettings
config zeus.Config
client *client.Client
}
func NewProviderFactory() factory.ProviderFactory[zeus.Zeus, zeus.Config] {
return factory.NewProviderFactory(factory.MustNewName("impl"), func(ctx context.Context, providerSettings factory.ProviderSettings, config zeus.Config) (zeus.Zeus, error) {
return New(ctx, providerSettings, config)
})
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config zeus.Config) (zeus.Zeus, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/zeus/implzeus")
httpClient := client.New(
settings.Logger(),
providerSettings.TracerProvider,
providerSettings.MeterProvider,
client.WithRequestResponseLog(true),
client.WithRetryCount(3),
)
return &Provider{
settings: settings,
config: config,
client: httpClient,
}, nil
}
func (provider *Provider) GetLicense(ctx context.Context, key string) (*licensetypes.License, error) {
return nil, nil
}
func (provider *Provider) GetCheckoutURL(ctx context.Context, key string) (string, error) {
return "", nil
}
func (provider *Provider) GetPortalURL(ctx context.Context, key string) (string, error) {
return "", nil
}
func (provider *Provider) GetDeployment(ctx context.Context, key string) ([]byte, error) {
return nil, nil
}
func (provider *Provider) PutMeters(ctx context.Context, key string, meters metertypes.Meters) error {
return nil
}

View File

@@ -1,26 +1,12 @@
// keep this consistent with backend constants.go
export enum FeatureKeys {
SSO = 'SSO',
ENTERPRISE_PLAN = 'ENTERPRISE_PLAN',
BASIC_PLAN = 'BASIC_PLAN',
ALERT_CHANNEL_SLACK = 'ALERT_CHANNEL_SLACK',
ALERT_CHANNEL_WEBHOOK = 'ALERT_CHANNEL_WEBHOOK',
ALERT_CHANNEL_PAGERDUTY = 'ALERT_CHANNEL_PAGERDUTY',
ALERT_CHANNEL_OPSGENIE = 'ALERT_CHANNEL_OPSGENIE',
ALERT_CHANNEL_MSTEAMS = 'ALERT_CHANNEL_MSTEAMS',
CUSTOM_METRICS_FUNCTION = 'CUSTOM_METRICS_FUNCTION',
QUERY_BUILDER_PANELS = 'QUERY_BUILDER_PANELS',
QUERY_BUILDER_ALERTS = 'QUERY_BUILDER_ALERTS',
DISABLE_UPSELL = 'DISABLE_UPSELL',
USE_SPAN_METRICS = 'USE_SPAN_METRICS',
OSS = 'OSS',
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
AWS_INTEGRATION = 'AWS_INTEGRATION',
ONBOARDING_V3 = 'ONBOARDING_V3',
THIRD_PARTY_API = 'THIRD_PARTY_API',
TRACE_FUNNELS = 'TRACE_FUNNELS',

View File

@@ -1,6 +1,5 @@
import { Form, FormInstance, Input, Select, Switch, Typography } from 'antd';
import { Store } from 'antd/lib/form/interface';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import {
ChannelType,
@@ -11,11 +10,8 @@ import {
WebhookChannel,
} from 'container/CreateAlertChannels/config';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
import { isFeatureKeys } from 'utils/app';
import EmailSettings from './Settings/Email';
import MsTeamsSettings from './Settings/MsTeams';
@@ -39,17 +35,6 @@ function FormAlertChannels({
editing = false,
}: FormAlertChannelsProps): JSX.Element {
const { t } = useTranslation('channels');
const { featureFlags } = useAppContext();
const feature = `ALERT_CHANNEL_${type.toUpperCase()}`;
const featureKey = isFeatureKeys(feature)
? feature
: FeatureKeys.ALERT_CHANNEL_SLACK;
const hasFeature = featureFlags?.find(
(flag: FeatureFlagProps) => flag.name === featureKey,
);
const renderSettings = (): ReactElement | null => {
switch (type) {
@@ -146,7 +131,7 @@ function FormAlertChannels({
<Form.Item>
<Button
disabled={savingState || !hasFeature}
disabled={savingState}
loading={savingState}
type="primary"
onClick={(): void => onSaveHandler(type)}
@@ -154,7 +139,7 @@ function FormAlertChannels({
{t('button_save_channel')}
</Button>
<Button
disabled={testingState || !hasFeature}
disabled={testingState}
loading={testingState}
onClick={(): void => onTestHandler(type)}
>

View File

@@ -467,10 +467,6 @@ function FormAlertRules({
panelType,
]);
const isAlertAvailable =
!featureFlags?.find((flag) => flag.name === FeatureKeys.QUERY_BUILDER_ALERTS)
?.active || false;
const saveRule = useCallback(async () => {
if (!isFormValid()) {
return;
@@ -689,7 +685,6 @@ function FormAlertRules({
const isAlertNameMissing = !formInstance.getFieldValue('alert');
const isAlertAvailableToSave =
isAlertAvailable &&
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
alertType !== AlertTypes.METRICS_BASED_ALERT;

View File

@@ -5,7 +5,6 @@ import { WarningOutlined } from '@ant-design/icons';
import { Button, Flex, Modal, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import {
initialQueriesMap,
@@ -27,7 +26,6 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
getNextWidgets,
@@ -79,8 +77,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const { t } = useTranslation(['dashboard']);
const { featureFlags } = useAppContext();
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const {
@@ -566,12 +562,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isQueryBuilderActive =
!featureFlags?.find((flag) => flag.name === FeatureKeys.QUERY_BUILDER_PANELS)
?.active || false;
const isNewTraceLogsAvailable =
isQueryBuilderActive &&
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
currentQuery.builder.queryData.find(
(query) => query.dataSource !== DataSource.METRICS,

View File

@@ -13,11 +13,7 @@ function OrganizationSettings(): JSX.Element {
const isNotSSO =
!featureFlags?.find((flag) => flag.name === FeatureKeys.SSO)?.active || false;
const isNoUpSell =
!featureFlags?.find((flag) => flag.name === FeatureKeys.DISABLE_UPSELL)
?.active || false;
const isAuthDomain = !isNoUpSell || (isNoUpSell && !isNotSSO);
const isAuthDomain = !isNotSSO;
if (!org) {
return <div />;

View File

@@ -186,76 +186,6 @@ export function getAppContextMock(
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.OSS,
active: false,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.DISABLE_UPSELL,
active: false,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.CUSTOM_METRICS_FUNCTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.QUERY_BUILDER_PANELS,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.QUERY_BUILDER_ALERTS,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.ALERT_CHANNEL_SLACK,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.ALERT_CHANNEL_WEBHOOK,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.ALERT_CHANNEL_PAGERDUTY,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.ALERT_CHANNEL_OPSGENIE,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.ALERT_CHANNEL_MSTEAMS,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.USE_SPAN_METRICS,
active: false,

1
go.mod
View File

@@ -181,6 +181,7 @@ require (
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/open-feature/go-sdk v1.14.1 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.111.0 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect

2
go.sum
View File

@@ -717,6 +717,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo=
github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg=
github.com/open-telemetry/opamp-go v0.5.0 h1:2YFbb6G4qBkq3yTRdVb5Nfz9hKHW/ldUyex352e1J7g=
github.com/open-telemetry/opamp-go v0.5.0/go.mod h1:IMdeuHGVc5CjKSu5/oNV0o+UmiXuahoHvoZ4GOmAI9M=
github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.111.0 h1:n1p2DedLvPEN1XEx26s1PR1PCuXTgCY4Eo+kDTq7q0s=

1
pkg/flagger/api.go Normal file
View File

@@ -0,0 +1 @@
package flagger

35
pkg/flagger/config.go Normal file
View File

@@ -0,0 +1,35 @@
package flagger
import "github.com/SigNoz/signoz/pkg/factory"
var _ factory.Config = Config{}
type Config struct {
Provider string `json:"provider"`
Memory Memory `json:"memory"`
}
type Memory struct {
Boolean Boolean `json:"boolean"`
}
type Boolean struct {
Enabled []string `json:"enabled"`
Disabled []string `json:"disabled"`
}
func NewConfigFactory() factory.ConfigFactory {
return factory.NewConfigFactory(factory.MustNewName("flagger"), newConfig)
}
func newConfig() factory.Config {
return &Config{
Provider: "memory",
Memory: Memory{},
}
}
func (c Config) Validate() error {
return nil
}

27
pkg/flagger/flagger.go Normal file
View File

@@ -0,0 +1,27 @@
package flagger
import (
"github.com/open-feature/go-sdk/openfeature"
)
type Provider = openfeature.FeatureProvider
type FlaggerHook = openfeature.Hook
type Flagger = openfeature.IClient
type flagger struct {
*openfeature.Client
}
func New(provider Provider, hooks ...FlaggerHook) (Flagger, error) {
client := openfeature.NewClient("signoz")
if err := openfeature.SetNamedProviderAndWait("signoz", provider); err != nil {
return nil, err
}
client.AddHooks(hooks...)
return &flagger{Client: client}, nil
}

View File

@@ -0,0 +1,241 @@
package memoryprovider
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/open-feature/go-sdk/openfeature"
)
type provider struct {
config flagger.Config
settings factory.ScopedProviderSettings
featureValues map[featuretypes.Name]featuretypes.FeatureValue
registry featuretypes.Registry
}
func NewFactory(registry featuretypes.Registry) factory.ProviderFactory[flagger.Provider, flagger.Config] {
return factory.NewProviderFactory(factory.MustNewName("memory"), func(ctx context.Context, providerSettings factory.ProviderSettings, config flagger.Config) (flagger.Provider, error) {
return New(ctx, providerSettings, config, registry)
})
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config flagger.Config, registry featuretypes.Registry) (flagger.Provider, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/flagger/memoryprovider")
featureValues := make(map[featuretypes.Name]featuretypes.FeatureValue)
for _, flag := range config.Memory.Boolean.Enabled {
name, err := featuretypes.NewName(flag)
if err != nil {
settings.Logger().Error("invalid flag name encountered", "flag", flag, "error", err)
continue
}
featureValues[name] = featuretypes.FeatureValue{
Name: name,
Variant: featuretypes.KindBooleanVariantEnabled,
}
}
for _, flag := range config.Memory.Boolean.Disabled {
name, err := featuretypes.NewName(flag)
if err != nil {
settings.Logger().Error("invalid flag name encountered", "flag", flag, "error", err)
continue
}
if _, ok := featureValues[name]; ok {
settings.Logger().Error("flag already exists and has been enabled", "flag", flag)
continue
}
featureValues[name] = featuretypes.FeatureValue{
Name: name,
Variant: featuretypes.KindBooleanVariantDisabled,
}
}
return &provider{
config: config,
settings: settings,
featureValues: featureValues,
registry: registry,
}, nil
}
func (provider *provider) Metadata() openfeature.Metadata {
return openfeature.Metadata{
Name: "memory",
}
}
func (provider *provider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
feature, detail, err := provider.registry.Get(flag)
if err != nil {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureValues[feature.Name]; ok {
value, detail, err := featuretypes.GetVariantValue[bool](feature, featureValue.Variant)
if err != nil {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.BoolResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
feature, detail, err := provider.registry.Get(flag)
if err != nil {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureValues[feature.Name]; ok {
value, detail, err := featuretypes.GetVariantValue[string](feature, featureValue.Variant)
if err != nil {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.StringResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
feature, detail, err := provider.registry.Get(flag)
if err != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureValues[feature.Name]; ok {
value, detail, err := featuretypes.GetVariantValue[float64](feature, featureValue.Variant)
if err != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.FloatResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
feature, detail, err := provider.registry.Get(flag)
if err != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureValues[feature.Name]; ok {
value, detail, err := featuretypes.GetVariantValue[int64](feature, featureValue.Variant)
if err != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.IntResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
feature, detail, err := provider.registry.Get(flag)
if err != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureValues[feature.Name]; ok {
value, detail, err := featuretypes.GetVariantValue[interface{}](feature, featureValue.Variant)
if err != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.InterfaceResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) Hooks() []openfeature.Hook {
return []openfeature.Hook{}
}

34
pkg/flagger/registry.go Normal file
View File

@@ -0,0 +1,34 @@
package flagger
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
var (
FeatureUseTracesNewSchema = featuretypes.MustNewName("UseTracesNewSchema")
FeatureUseLogsNewSchema = featuretypes.MustNewName("UseLogsNewSchema")
)
func MustNewRegistry() featuretypes.Registry {
registry, err := featuretypes.NewRegistry(
&featuretypes.Feature{
Name: FeatureUseTracesNewSchema,
Kind: featuretypes.KindBoolean,
Description: "Use new traces schema.",
Stage: featuretypes.StageStable,
DefaultVariant: featuretypes.KindBooleanVariantDisabled,
Variants: featuretypes.NewBooleanFeatureVariants(),
},
&featuretypes.Feature{
Name: FeatureUseLogsNewSchema,
Kind: featuretypes.KindBoolean,
Description: "Use new logs schema.",
Stage: featuretypes.StageStable,
DefaultVariant: featuretypes.KindBooleanVariantDisabled,
Variants: featuretypes.NewBooleanFeatureVariants(),
},
)
if err != nil {
panic(err)
}
return registry
}

View File

@@ -17,7 +17,7 @@ type Client struct {
netc *http.Client
}
func New(logger *slog.Logger, tracerProvider trace.TracerProvider, meterProvider metric.MeterProvider, opts ...Option) (*Client, error) {
func New(logger *slog.Logger, tracerProvider trace.TracerProvider, meterProvider metric.MeterProvider, opts ...Option) *Client {
clientOpts := options{
retryCount: 3,
requestResponseLog: false,
@@ -46,7 +46,7 @@ func New(logger *slog.Logger, tracerProvider trace.TracerProvider, meterProvider
return &Client{
netc: netc,
c: c,
}, nil
}
}
func (c *Client) Do(request *http.Request) (*http.Response, error) {

16
pkg/licensing/api.go Normal file
View File

@@ -0,0 +1,16 @@
package licensing
// type API interface {
// GetLicenses(context.Context, valuer.UUID, licensetypes.GettableLicenseParams) (licensetypes.GettableLicenses, error)
// }
// func Get(){
// //
// return 501
// }
// func Put() {
// //400 bad request
// //501
// }

17
pkg/licensing/config.go Normal file
View File

@@ -0,0 +1,17 @@
package licensing
import "time"
type Config struct {
Provider string `mapstructure:"provider"`
PollingConfig PollingConfig `mapstructure:"polling"`
}
type PollingConfig struct {
Interval time.Duration `mapstructure:"interval"`
}
func (c Config) Validate() error {
return nil
}

View File

@@ -0,0 +1,27 @@
package licensing
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
ErrCodeLicensingServerNotFound = errors.MustNewCode("licensing_server_not_found")
)
type Licensing interface {
factory.Service
// GetLicenses gets the licenses for the organization.
GetLicenses(context.Context, valuer.UUID, licensetypes.GettableLicenseParams) (licensetypes.GettableLicenses, error)
// GetLatestLicense gets the latest license for the organization.
GetLatestLicense(context.Context, valuer.UUID) (licensetypes.License, error)
// SetLicense sets the license for the organization.
SetLicense(context.Context, valuer.UUID, string) error
}

View File

@@ -0,0 +1,50 @@
package nooplicensing
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type provider struct {
stopC chan struct{}
license licensetypes.License
}
func NewFactory() factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return factory.NewProviderFactory(factory.MustNewName("noop"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
return New(ctx, providerSettings, config)
})
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
return &provider{
stopC: make(chan struct{}),
license: licensetypes.NewNoop(),
}, nil
}
func (provider *provider) Start(ctx context.Context) error {
<-provider.stopC
return nil
}
func (provider *provider) GetLicenses(ctx context.Context, orgID valuer.UUID, params licensetypes.GettableLicenseParams) (licensetypes.GettableLicenses, error) {
return licensetypes.GettableLicenses{provider.license}, nil
}
func (provider *provider) GetLatestLicense(ctx context.Context, orgID valuer.UUID) (licensetypes.License, error) {
return provider.license, nil
}
func (provider *provider) SetLicense(ctx context.Context, orgID valuer.UUID, key string) error {
return nil
}
func (provider *provider) Stop(ctx context.Context) error {
close(provider.stopC)
return nil
}

View File

@@ -33,6 +33,7 @@ import (
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"go.uber.org/zap"
@@ -143,9 +144,6 @@ type ClickHouseReader struct {
liveTailRefreshSeconds int
cluster string
useLogsNewSchema bool
useTraceNewSchema bool
logsTableName string
logsLocalTableName string
@@ -166,8 +164,6 @@ func NewReader(
telemetryStore telemetrystore.TelemetryStore,
prometheus prometheus.Prometheus,
cluster string,
useLogsNewSchema bool,
useTraceNewSchema bool,
fluxIntervalForTraceDetail time.Duration,
cache cache.Cache,
) *ClickHouseReader {
@@ -181,13 +177,13 @@ func NewReaderFromClickhouseConnection(
telemetryStore telemetrystore.TelemetryStore,
prometheus prometheus.Prometheus,
cluster string,
useLogsNewSchema bool,
useTraceNewSchema bool,
fluxIntervalForTraceDetail time.Duration,
cache cache.Cache,
) *ClickHouseReader {
logsTableName := options.primary.LogsTable
logsLocalTableName := options.primary.LogsLocalTable
useLogsNewSchema, _ := featureControl.Boolean(context.Background(), valuer.UUID{}, featuretypes.UseLogsNewSchema)
if useLogsNewSchema {
logsTableName = options.primary.LogsTableV2
logsLocalTableName = options.primary.LogsLocalTableV2
@@ -195,6 +191,7 @@ func NewReaderFromClickhouseConnection(
traceTableName := options.primary.IndexTable
traceLocalTableName := options.primary.LocalIndexTable
useTraceNewSchema, _ := featureControl.Boolean(context.Background(), valuer.UUID{}, featuretypes.UseTracesNewSchema)
if useTraceNewSchema {
traceTableName = options.primary.TraceIndexTableV3
traceLocalTableName = options.primary.TraceLocalTableNameV3
@@ -290,7 +287,7 @@ func (r *ClickHouseReader) GetServicesList(ctx context.Context) (*[]string, erro
services := []string{}
query := fmt.Sprintf(`SELECT DISTINCT serviceName FROM %s.%s WHERE toDate(timestamp) > now() - INTERVAL 1 DAY`, r.TraceDB, r.traceTableName)
if r.useTraceNewSchema {
if ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseTracesNewSchema); ok {
query = fmt.Sprintf(`SELECT DISTINCT serviceName FROM %s.%s WHERE ts_bucket_start > (toUnixTimestamp(now() - INTERVAL 1 DAY) - 1800) AND toDate(timestamp) > now() - INTERVAL 1 DAY`, r.TraceDB, r.traceTableName)
}
@@ -538,7 +535,7 @@ func (r *ClickHouseReader) GetServicesV2(ctx context.Context, queryParams *model
}
func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.GetServicesParams, skipConfig *model.SkipConfig) (*[]model.ServiceItem, *model.ApiError) {
if r.useTraceNewSchema {
if ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseTracesNewSchema); ok {
return r.GetServicesV2(ctx, queryParams, skipConfig)
}
@@ -892,7 +889,7 @@ func (r *ClickHouseReader) GetTopOperationsV2(ctx context.Context, queryParams *
func (r *ClickHouseReader) GetTopOperations(ctx context.Context, queryParams *model.GetTopOperationsParams) (*[]model.TopOperationsItem, *model.ApiError) {
if r.useTraceNewSchema {
if ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseTracesNewSchema); ok {
return r.GetTopOperationsV2(ctx, queryParams)
}
@@ -2680,7 +2677,7 @@ func (r *ClickHouseReader) GetSpansInLastHeartBeatInterval(ctx context.Context,
var spansInLastHeartBeatInterval uint64
queryStr := fmt.Sprintf("SELECT count() from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d));", signozTraceDBName, signozSpansTable, int(interval.Minutes()))
if r.useTraceNewSchema {
if ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseTracesNewSchema); ok {
queryStr = fmt.Sprintf("SELECT count() from %s.%s where ts_bucket_start >= toUInt64(toUnixTimestamp(now() - toIntervalMinute(%d))) - 1800 and timestamp > toUnixTimestamp(now()-toIntervalMinute(%d));", signozTraceDBName, r.traceTableName, int(interval.Minutes()), int(interval.Minutes()))
}
r.db.QueryRow(ctx, queryStr).Scan(&spansInLastHeartBeatInterval)
@@ -2820,7 +2817,7 @@ func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Contex
where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d))
group by serviceName, env, language;`, r.TraceDB, r.traceTableName, int(interval.Minutes()))
if r.useTraceNewSchema {
if ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseTracesNewSchema); ok {
queryStr = fmt.Sprintf(`select serviceName, resources_string['deployment.environment'] as env,
resources_string['telemetry.sdk.language'] as language from %s.%s
where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d))
@@ -2925,8 +2922,9 @@ func (r *ClickHouseReader) extractSelectedAndInterestingFields(tableStatement st
if overrideFieldType != "" {
field.Type = overrideFieldType
}
ok, _ := r.featureControl.Boolean(context.Background(), valuer.UUID{}, featuretypes.UseLogsNewSchema)
// all static fields are assumed to be selected as we don't allow changing them
if isColumn(r.useLogsNewSchema, tableStatement, field.Type, field.Name, field.DataType) {
if isColumn(ok, tableStatement, field.Type, field.Name, field.DataType) {
response.Selected = append(response.Selected, field)
} else {
response.Interesting = append(response.Interesting, field)
@@ -3007,7 +3005,8 @@ func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.Upda
return &model.ApiError{Err: err, Typ: model.ErrorBadData}
}
if r.useLogsNewSchema {
ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseLogsNewSchema)
if ok {
return r.UpdateLogFieldV2(ctx, field)
}
@@ -3896,6 +3895,7 @@ func (r *ClickHouseReader) GetLogAggregateAttributes(ctx context.Context, req *v
var tagKey string
var dataType string
var attType string
ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseLogsNewSchema)
for rows.Next() {
if err := rows.Scan(&tagKey, &attType, &dataType); err != nil {
return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
@@ -3904,7 +3904,7 @@ func (r *ClickHouseReader) GetLogAggregateAttributes(ctx context.Context, req *v
Key: tagKey,
DataType: v3.AttributeKeyDataType(dataType),
Type: v3.AttributeKeyType(attType),
IsColumn: isColumn(r.useLogsNewSchema, statements[0].Statement, attType, tagKey, dataType),
IsColumn: isColumn(ok, statements[0].Statement, attType, tagKey, dataType),
}
response.AttributeKeys = append(response.AttributeKeys, key)
}
@@ -3950,6 +3950,7 @@ func (r *ClickHouseReader) GetLogAttributeKeys(ctx context.Context, req *v3.Filt
var attributeKey string
var attributeDataType string
var tagType string
ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseLogsNewSchema)
for rows.Next() {
if err := rows.Scan(&attributeKey, &tagType, &attributeDataType); err != nil {
return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
@@ -3959,7 +3960,7 @@ func (r *ClickHouseReader) GetLogAttributeKeys(ctx context.Context, req *v3.Filt
Key: attributeKey,
DataType: v3.AttributeKeyDataType(attributeDataType),
Type: v3.AttributeKeyType(tagType),
IsColumn: isColumn(r.useLogsNewSchema, statements[0].Statement, tagType, attributeKey, attributeDataType),
IsColumn: isColumn(ok, statements[0].Statement, tagType, attributeKey, attributeDataType),
}
response.AttributeKeys = append(response.AttributeKeys, key)
@@ -4690,7 +4691,8 @@ func (r *ClickHouseReader) GetTraceAggregateAttributes(ctx context.Context, req
}
fields := constants.NewStaticFieldsTraces
if !r.useTraceNewSchema {
ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseTracesNewSchema)
if !ok {
fields = constants.DeprecatedStaticFieldsTraces
}
@@ -4754,7 +4756,8 @@ func (r *ClickHouseReader) GetTraceAttributeKeys(ctx context.Context, req *v3.Fi
// remove this later just to have NewStaticFieldsTraces in the response
fields := constants.NewStaticFieldsTraces
if !r.useTraceNewSchema {
ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseTracesNewSchema)
if !ok {
fields = constants.DeprecatedStaticFieldsTraces
}
@@ -4818,7 +4821,7 @@ func (r *ClickHouseReader) GetTraceAttributeValues(ctx context.Context, req *v3.
// TODO(nitya): remove 24 hour limit in future after checking the perf/resource implications
where := "timestamp >= toDateTime64(now() - INTERVAL 48 HOUR, 9)"
if r.useTraceNewSchema {
if ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseTracesNewSchema); ok {
where += " AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 48 HOUR))"
}
query = fmt.Sprintf("SELECT DISTINCT %s FROM %s.%s WHERE %s AND %s ILIKE $1 LIMIT $2", selectKey, r.TraceDB, r.traceTableName, where, filterValueColumnWhere)
@@ -4916,7 +4919,8 @@ func (r *ClickHouseReader) GetSpanAttributeKeysV2(ctx context.Context) (map[stri
}
func (r *ClickHouseReader) GetSpanAttributeKeys(ctx context.Context) (map[string]v3.AttributeKey, error) {
if r.useTraceNewSchema {
ok, _ := r.featureControl.Boolean(ctx, valuer.UUID{}, featuretypes.UseTracesNewSchema)
if ok {
return r.GetSpanAttributeKeysV2(ctx)
}
var query string

View File

@@ -43,7 +43,6 @@ type querier struct {
fluxInterval time.Duration
builder *queryBuilder.QueryBuilder
// used for testing
// TODO(srikanthccv): remove this once we have a proper mock
testingMode bool

View File

@@ -8,7 +8,6 @@ import (
metricsV3 "github.com/SigNoz/signoz/pkg/query-service/app/metrics/v3"
"github.com/SigNoz/signoz/pkg/query-service/cache"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"go.uber.org/zap"
)
@@ -46,8 +45,7 @@ type prepareLogsQueryFunc func(start, end int64, queryType v3.QueryType, panelTy
type prepareMetricQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery, options metricsV3.Options) (string, error)
type QueryBuilder struct {
options QueryBuilderOptions
featureFlags interfaces.FeatureLookup
options QueryBuilderOptions
}
type QueryBuilderOptions struct {

View File

@@ -68,10 +68,6 @@ func UseMetricsPreAggregation() bool {
return GetOrDefaultEnv("USE_METRICS_PRE_AGGREGATION", "true") == "true"
}
func EnableHostsInfraMonitoring() bool {
return GetOrDefaultEnv("ENABLE_INFRA_METRICS", "true") == "true"
}
var KafkaSpanEval = GetOrDefaultEnv("KAFKA_SPAN_EVAL", "false")
var DEFAULT_FEATURE_SET = model.FeatureSet{

View File

@@ -1,66 +0,0 @@
package featureManager
import (
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
type FeatureManager struct {
}
func StartManager() *FeatureManager {
fM := &FeatureManager{}
return fM
}
// CheckFeature will be internally used by backend routines
// for feature gating
func (fm *FeatureManager) CheckFeature(featureKey string) error {
feature, err := fm.GetFeatureFlag(featureKey)
if err != nil {
return err
}
if feature.Active {
return nil
}
return model.ErrFeatureUnavailable{Key: featureKey}
}
// GetFeatureFlags returns current features
func (fm *FeatureManager) GetFeatureFlags() (model.FeatureSet, error) {
features := append(constants.DEFAULT_FEATURE_SET, model.Feature{
Name: model.OSS,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
})
return features, nil
}
func (fm *FeatureManager) InitFeatures(req model.FeatureSet) error {
zap.L().Error("InitFeatures not implemented in OSS")
return nil
}
func (fm *FeatureManager) UpdateFeatureFlag(req model.Feature) error {
zap.L().Error("UpdateFeatureFlag not implemented in OSS")
return nil
}
func (fm *FeatureManager) GetFeatureFlag(key string) (model.Feature, error) {
features, err := fm.GetFeatureFlags()
if err != nil {
return model.Feature{}, err
}
for _, feature := range features {
if feature.Name == key {
return feature, nil
}
}
return model.Feature{}, model.ErrFeatureUnavailable{Key: key}
}

View File

@@ -1,13 +0,0 @@
package interfaces
import (
"github.com/SigNoz/signoz/pkg/query-service/model"
)
type FeatureLookup interface {
CheckFeature(f string) error
GetFeatureFlags() (model.FeatureSet, error)
GetFeatureFlag(f string) (model.Feature, error)
UpdateFeatureFlag(features model.Feature) error
InitFeatures(features model.FeatureSet) error
}

View File

@@ -9,58 +9,11 @@ type Feature struct {
Route string `db:"route" json:"route"`
}
const CustomMetricsFunction = "CUSTOM_METRICS_FUNCTION"
const DisableUpsell = "DISABLE_UPSELL"
const OSS = "OSS"
const QueryBuilderPanels = "QUERY_BUILDER_PANELS"
const QueryBuilderAlerts = "QUERY_BUILDER_ALERTS"
const UseSpanMetrics = "USE_SPAN_METRICS"
const AlertChannelSlack = "ALERT_CHANNEL_SLACK"
const AlertChannelWebhook = "ALERT_CHANNEL_WEBHOOK"
const AlertChannelPagerduty = "ALERT_CHANNEL_PAGERDUTY"
const AlertChannelMsTeams = "ALERT_CHANNEL_MSTEAMS"
const AlertChannelOpsgenie = "ALERT_CHANNEL_OPSGENIE"
const AlertChannelEmail = "ALERT_CHANNEL_EMAIL"
const AnomalyDetection = "ANOMALY_DETECTION"
const HostsInfraMonitoring = "HOSTS_INFRA_MONITORING"
const TraceFunnels = "TRACE_FUNNELS"
var BasicPlan = FeatureSet{
Feature{
Name: OSS,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: DisableUpsell,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: CustomMetricsFunction,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: QueryBuilderPanels,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: QueryBuilderAlerts,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: UseSpanMetrics,
Active: false,
@@ -68,48 +21,6 @@ var BasicPlan = FeatureSet{
UsageLimit: -1,
Route: "",
},
Feature{
Name: AlertChannelSlack,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: AlertChannelWebhook,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: AlertChannelPagerduty,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: AlertChannelOpsgenie,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: AlertChannelMsTeams,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: AnomalyDetection,
Active: false,

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/featurecontrol"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/sqlmigration"
@@ -13,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/SigNoz/signoz/pkg/web"
)
@@ -25,6 +27,8 @@ type SigNoz struct {
TelemetryStore telemetrystore.TelemetryStore
Prometheus prometheus.Prometheus
Alertmanager alertmanager.Alertmanager
Zeus zeus.Zeus
FeatureControl featurecontrol.FeatureControl
}
func New(

View File

@@ -66,7 +66,7 @@ func (migration *updatePatAndOrgDomains) Up(ctx context.Context, db *bun.DB) err
}
}
if err := updateOrgId(ctx, tx, "org_domains"); err != nil {
if err := updateOrgId(ctx, tx); err != nil {
return err
}
@@ -96,7 +96,7 @@ func (migration *updatePatAndOrgDomains) Down(ctx context.Context, db *bun.DB) e
return nil
}
func updateOrgId(ctx context.Context, tx bun.Tx, table string) error {
func updateOrgId(ctx context.Context, tx bun.Tx) error {
if _, err := tx.NewCreateTable().
Model(&struct {
bun.BaseModel `bun:"table:org_domains_new"`

View File

@@ -0,0 +1,35 @@
package featuretypes
import "github.com/SigNoz/signoz/pkg/valuer"
const (
KindBooleanVariantEnabled = "enabled"
KindBooleanVariantDisabled = "disabled"
)
var (
KindBoolean Kind = Kind{valuer.NewString("bool")}
KindString Kind = Kind{valuer.NewString("string")}
KindInt Kind = Kind{valuer.NewString("int")}
KindFloat Kind = Kind{valuer.NewString("float")}
KindObject Kind = Kind{valuer.NewString("object")}
)
// Kind is the kind of the feature flag. Inspired from https://github.com/open-feature/go-sdk/blob/main/openfeature/interfaces.go
type Kind struct{ valuer.String }
var (
// Is the feature experimental?
StageExperimental = Stage{valuer.NewString("experimental")}
// Does the feature work but is not ready for production?
StagePreview = Stage{valuer.NewString("preview")}
// Is the feature stable and ready for production?
StageStable = Stage{valuer.NewString("stable")}
// Is the feature deprecated and will be removed in the future?
StageDeprecated = Stage{valuer.NewString("deprecated")}
)
type Stage struct{ valuer.String }

View File

@@ -0,0 +1,78 @@
package featuretypes
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/open-feature/go-sdk/openfeature"
)
var (
ErrCodeFeatureNotFound = errors.MustNewCode("feature_not_found")
ErrCodeFeatureKindMismatch = errors.MustNewCode("feature_kind_mismatch")
ErrCodeFeatureVariantNotFound = errors.MustNewCode("feature_variant_not_found")
)
type Feature struct {
// Name is the name of the feature flag.
Name Name `json:"name"`
// Kind is the kind of the feature flag.
Kind Kind `json:"kind"`
// Description is the description of the feature flag.
Description string `json:"description"`
// Stage is the stage of the feature flag.
Stage Stage `json:"stage"`
// DefaultVariant is the default variant of the feature flag.
DefaultVariant string `json:"default_variant"`
// Variants is the variants of the feature flag.
Variants map[string]any `json:"variants"`
}
type FeatureValue struct {
// Name is the name of the feature flag.
Name Name `json:"name"`
// Value is the value of the feature flag.
Variant string `json:"variant"`
}
func NewBooleanFeatureVariants() map[string]any {
return map[string]any{
KindBooleanVariantEnabled: true,
KindBooleanVariantDisabled: false,
}
}
func GetVariantValue[T any](feature *Feature, variant string) (t T, detail openfeature.ProviderResolutionDetail, err error) {
value, ok := feature.Variants[variant]
if !ok {
err = errors.Newf(errors.TypeNotFound, ErrCodeFeatureVariantNotFound, "variant %s not found for feature %s", variant, feature.Name.String())
detail = openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant,
}
return
}
t, ok = value.(T)
if !ok {
err = errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureKindMismatch, "variant %s has type %T, expected %T", variant, value, t)
detail = openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewTypeMismatchResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant,
}
return
}
detail = openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: variant,
}
return
}

View File

@@ -0,0 +1,35 @@
package featuretypes
import (
"fmt"
"regexp"
)
var (
nameRegex = regexp.MustCompile(`^[A-Z][a-zA-Z]+$`)
)
type Name struct {
s string
}
func NewName(s string) (Name, error) {
if !nameRegex.MatchString(s) {
return Name{}, fmt.Errorf("invalid feature name: %s", s)
}
return Name{s: s}, nil
}
func MustNewName(s string) Name {
name, err := NewName(s)
if err != nil {
panic(err)
}
return name
}
func (n Name) String() string {
return n.s
}

View File

@@ -0,0 +1,134 @@
package featuretypes
import (
"fmt"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/open-feature/go-sdk/openfeature"
)
type Registry interface {
// Merge merges the given registry with the current registry and returns a new registry.
// The input registry takes precedence over the current registry.
MergeOrOverride(Registry) Registry
// Get returns the feature with the given name.
Get(string) (*Feature, openfeature.ProviderResolutionDetail, error)
// List returns all the features in the registry.
List() []*Feature
}
type registry struct {
features map[Name]*Feature
}
func NewRegistry(features ...*Feature) (Registry, error) {
registry := &registry{features: make(map[Name]*Feature)}
for _, feature := range features {
// Name must be unique
if _, ok := registry.features[feature.Name]; ok {
return nil, fmt.Errorf("cannot build registry, duplicate name %q found", feature.Name.String())
}
// Default variant must be present in the variants
if _, ok := feature.Variants[feature.DefaultVariant]; !ok {
return nil, fmt.Errorf("cannot build registry, default variant %q not found in variants %v", feature.DefaultVariant, feature.Variants)
}
// Check that the type of the default variant and the variants match the kind of the feature
switch feature.Kind {
case KindBoolean:
if _, ok := feature.Variants[feature.DefaultVariant].(bool); !ok {
return nil, fmt.Errorf("cannot build registry, default variant %q is not a boolean", feature.DefaultVariant)
}
for variant, value := range feature.Variants {
if _, ok := value.(bool); !ok {
return nil, fmt.Errorf("cannot build registry, variant %q is not a boolean", variant)
}
}
case KindString:
if _, ok := feature.Variants[feature.DefaultVariant].(string); !ok {
return nil, fmt.Errorf("cannot build registry, default variant %q is not a string", feature.DefaultVariant)
}
for variant, value := range feature.Variants {
if _, ok := value.(string); !ok {
return nil, fmt.Errorf("cannot build registry, variant %q is not a string", variant)
}
}
case KindInt:
if _, ok := feature.Variants[feature.DefaultVariant].(int); !ok {
return nil, fmt.Errorf("cannot build registry, default variant %q is not an int", feature.DefaultVariant)
}
for variant, value := range feature.Variants {
if _, ok := value.(int); !ok {
return nil, fmt.Errorf("cannot build registry, variant %q is not an int", variant)
}
}
case KindFloat:
if _, ok := feature.Variants[feature.DefaultVariant].(float64); !ok {
return nil, fmt.Errorf("cannot build registry, default variant %q is not a float", feature.DefaultVariant)
}
for variant, value := range feature.Variants {
if _, ok := value.(float64); !ok {
return nil, fmt.Errorf("cannot build registry, variant %q is not a float", variant)
}
}
case KindObject:
if _, ok := feature.Variants[feature.DefaultVariant].(map[string]any); !ok {
return nil, fmt.Errorf("cannot build registry, default variant %q is not an object", feature.DefaultVariant)
}
for variant, value := range feature.Variants {
if _, ok := value.(map[string]any); !ok {
return nil, fmt.Errorf("cannot build registry, variant %q is not an object", variant)
}
}
}
registry.features[feature.Name] = feature
}
return registry, nil
}
func (registry *registry) MergeOrOverride(other Registry) Registry {
for _, feature := range other.List() {
if _, ok := registry.features[feature.Name]; ok {
registry.features[feature.Name] = feature
}
registry.features[feature.Name] = feature
}
return registry
}
func (registry *registry) Get(flag string) (*Feature, openfeature.ProviderResolutionDetail, error) {
name, err := NewName(flag)
if err != nil {
return nil, openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewFlagNotFoundResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
}, err
}
feature, ok := registry.features[name]
if !ok {
return nil, openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewFlagNotFoundResolutionError(errors.Newf(errors.TypeNotFound, ErrCodeFeatureNotFound, "feature %s not found in registry", name.String()).Error()),
Reason: openfeature.ErrorReason,
}, err
}
return feature, openfeature.ProviderResolutionDetail{}, nil
}
func (registry *registry) List() []*Feature {
features := make([]*Feature, 0, len(registry.features))
for _, feature := range registry.features {
features = append(features, feature)
}
return features
}

View File

@@ -0,0 +1,73 @@
package licensetypes
import (
"context"
"net/http"
"strconv"
"time"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type GettableLicenseParams struct {
Active *bool
}
type GettableLicenses = []License
type License interface {
// ID returns the unique identifier for the license
ID() valuer.UUID
// OrgID returns the organization ID for the license
OrgID() valuer.UUID
// Contents returns the raw data for the license
Contents() []byte
// Key returns the key for the license
Key() string
// CreatedAt returns the creation time for the license
CreatedAt() time.Time
// UpdatedAt returns the last update time for the license
UpdatedAt() time.Time
// FeatureValues returns the feature values for the license
FeatureValues() []*featuretypes.FeatureValue
}
func NewGettableLicenseParams(req *http.Request) (GettableLicenseParams, error) {
params := GettableLicenseParams{
Active: nil,
}
queryValues := req.URL.Query()
if active := queryValues.Get("active"); active != "" {
activeBool, err := strconv.ParseBool(active)
if err != nil {
return GettableLicenseParams{}, err
}
params.Active = &activeBool
}
return params, nil
}
type Store interface {
// Set creates or updates a license.
Set(context.Context, License) error
// Get returns the license for the given orgID
Get(context.Context, valuer.UUID) ([]License, error)
// GetLatest returns the latest license for the given orgID
GetLatest(context.Context, valuer.UUID) (License, error)
// ListOrgs returns the list of orgs
ListOrgs(context.Context) ([]valuer.UUID, error)
}

View File

@@ -0,0 +1,42 @@
package licensetypes
import (
"time"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Noop struct{}
func NewNoop() License {
return &Noop{}
}
func (*Noop) ID() valuer.UUID {
return valuer.UUID{}
}
func (*Noop) OrgID() valuer.UUID {
return valuer.UUID{}
}
func (*Noop) Contents() []byte {
return []byte{}
}
func (*Noop) Key() string {
return ""
}
func (*Noop) CreatedAt() time.Time {
return time.Time{}
}
func (*Noop) UpdatedAt() time.Time {
return time.Time{}
}
func (*Noop) FeatureValues() []*featuretypes.FeatureValue {
return []*featuretypes.FeatureValue{}
}

View File

@@ -0,0 +1,5 @@
package metertypes
type Meter struct{}
type Meters = []*Meter

65
pkg/valuer/string.go Normal file
View File

@@ -0,0 +1,65 @@
package valuer
import (
"database/sql/driver"
"encoding/json"
"fmt"
"reflect"
"strings"
)
var _ Valuer = (*String)(nil)
type String struct {
val string
}
func NewString(val string) String {
return String{val: strings.ToLower(strings.TrimSpace(val))}
}
func (enum String) IsZero() bool {
return enum.val == ""
}
func (enum String) StringValue() string {
return enum.val
}
func (enum String) MarshalJSON() ([]byte, error) {
return json.Marshal(enum.StringValue())
}
func (enum *String) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
*enum = NewString(str)
return nil
}
func (enum String) Value() (driver.Value, error) {
return enum.StringValue(), nil
}
func (enum *String) Scan(val interface{}) error {
if enum == nil {
return fmt.Errorf("string: (nil \"%s\")", reflect.TypeOf(enum).String())
}
if val == nil {
// Return an empty string
*enum = NewString("")
return nil
}
str, ok := val.(string)
if !ok {
return fmt.Errorf("string: (non-string \"%s\")", reflect.TypeOf(val).String())
}
*enum = NewString(str)
return nil
}

17
pkg/zeus/config.go Normal file
View File

@@ -0,0 +1,17 @@
package zeus
import (
"net/url"
"github.com/SigNoz/signoz/pkg/factory"
)
var _ factory.Config = (*Config)(nil)
type Config struct {
URL *url.URL `mapstructure:"url"`
}
func (c Config) Validate() error {
return nil
}

View File

@@ -0,0 +1,43 @@
package noopzeus
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/metertypes"
"github.com/SigNoz/signoz/pkg/zeus"
)
type Provider struct{}
func NewProviderFactory() factory.ProviderFactory[zeus.Zeus, zeus.Config] {
return factory.NewProviderFactory(factory.MustNewName("noop"), func(ctx context.Context, providerSettings factory.ProviderSettings, config zeus.Config) (zeus.Zeus, error) {
return New(ctx, providerSettings, config)
})
}
func New(_ context.Context, _ factory.ProviderSettings, _ zeus.Config) (zeus.Zeus, error) {
return &Provider{}, nil
}
func (provider *Provider) GetLicense(_ context.Context, _ string) (*licensetypes.License, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "this operation is not supported")
}
func (provider *Provider) GetCheckoutURL(ctx context.Context, key string) (string, error) {
return "", errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "this operation is not supported")
}
func (provider *Provider) GetPortalURL(ctx context.Context, key string) (string, error) {
return "", errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "this operation is not supported")
}
func (provider *Provider) GetDeployment(ctx context.Context, key string) ([]byte, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "this operation is not supported")
}
func (provider *Provider) PutMeters(ctx context.Context, key string, meters metertypes.Meters) error {
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "this operation is not supported")
}

25
pkg/zeus/zeus.go Normal file
View File

@@ -0,0 +1,25 @@
package zeus
import (
"context"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/metertypes"
)
type Zeus interface {
// Returns the license for the given key.
GetLicense(context.Context, string) (licensetypes.License, error)
// Returns the checkout URL for the given license key.
GetCheckoutURL(context.Context, string) (string, error)
// Returns the portal URL for the given license key.
GetPortalURL(context.Context, string) (string, error)
// Returns the deployment for the given license key.
GetDeployment(context.Context, string) ([]byte, error)
// Puts the usage for the given license key.
PutMeters(context.Context, string, metertypes.Meters) error
}