feat(license): build license service (#7969)

* feat(license): base setup for license service

* feat(license): delete old manager and import to new

* feat(license): deal with features

* feat(license): complete the license service in ee

* feat(license): add sqlmigration for licenses

* feat(license): remove feature flags

* feat(license): refactor into provider pattern

* feat(license): remove the ff lookup interface

* feat(license): add logging to the validator functions

* feat(license): implement features for OSS build

* feat(license): fix the OSS build

* feat(license): lets blast frontend

* feat(license): fix the EE OSS build without license

* feat(license): remove the hardcoded testing configs

* feat(license): upgrade migration to 34

* feat(license): better naming and structure

* feat(license): better naming and structure

* feat(license): better naming and structure

* feat(license): better naming and structure

* feat(license): better naming and structure

* feat(license): better naming and structure

* feat(license): better naming and structure

* feat(license): integration tests

* feat(license): integration tests

* feat(license): refactor frontend

* feat(license): make frontend api structure changes

* feat(license): fix integration tests

* feat(license): revert hardcoded configs

* feat(license): fix integration tests

* feat(license): address review comments

* feat(license): address review comments

* feat(license): address review comments

* feat(license): address review comments

* feat(license): update migration

* feat(license): update migration

* feat(license): update migration

* feat(license): fixed logging

* feat(license): use the unmarshaller for postable subscription

* feat(license): correct the error message

* feat(license): fix license test

* feat(license): fix lint issues

* feat(user): do not kill the service if upstream is down
This commit is contained in:
Vikrant Gupta 2025-05-24 19:14:29 +05:30 committed by GitHub
parent 7feb94e5eb
commit b1c78c2f12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
88 changed files with 2328 additions and 2172 deletions

26
ee/licensing/config.go Normal file
View File

@ -0,0 +1,26 @@
package licensing
import (
"fmt"
"sync"
"time"
"github.com/SigNoz/signoz/pkg/licensing"
)
var (
config licensing.Config
once sync.Once
)
// initializes the licensing configuration
func Config(pollInterval time.Duration, failureThreshold int) licensing.Config {
once.Do(func() {
config = licensing.Config{PollInterval: pollInterval, FailureThreshold: failureThreshold}
if err := config.Validate(); err != nil {
panic(fmt.Errorf("invalid licensing config: %w", err))
}
})
return config
}

View File

@ -0,0 +1,168 @@
package httplicensing
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type licensingAPI struct {
licensing licensing.Licensing
}
func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
return &licensingAPI{licensing: licensing}
}
func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
req := new(licensetypes.PostableLicense)
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
return
}
err = api.licensing.Activate(r.Context(), orgID, req.Key)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusAccepted, nil)
}
func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
license, err := api.licensing.GetActive(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
}
gettableLicense := licensetypes.NewGettableLicense(license.Data, license.Key)
render.Success(rw, http.StatusOK, gettableLicense)
}
func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
err = api.licensing.Refresh(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
req := new(licensetypes.PostableSubscription)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
render.Error(rw, err)
return
}
gettableSubscription, err := api.licensing.Checkout(ctx, orgID, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, gettableSubscription)
}
func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
req := new(licensetypes.PostableSubscription)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
render.Error(rw, err)
return
}
gettableSubscription, err := api.licensing.Portal(ctx, orgID, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, gettableSubscription)
}

View File

@ -0,0 +1,285 @@
package httplicensing
import (
"context"
"encoding/json"
"time"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/tidwall/gjson"
)
type provider struct {
store licensetypes.Store
zeus zeus.Zeus
config licensing.Config
settings factory.ScopedProviderSettings
stopChan chan struct{}
}
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
return New(ctx, providerSettings, config, store, zeus)
})
}
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus) (licensing.Licensing, error) {
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
licensestore := sqllicensingstore.New(sqlstore)
return &provider{store: licensestore, zeus: zeus, config: config, settings: settings, stopChan: make(chan struct{})}, nil
}
func (provider *provider) Start(ctx context.Context) error {
tick := time.NewTicker(provider.config.PollInterval)
defer tick.Stop()
err := provider.Validate(ctx)
if err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err)
}
for {
select {
case <-provider.stopChan:
return nil
case <-tick.C:
err := provider.Validate(ctx)
if err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err)
}
}
}
}
func (provider *provider) Stop(ctx context.Context) error {
provider.settings.Logger().DebugContext(ctx, "license validation stopped")
close(provider.stopChan)
return nil
}
func (provider *provider) Validate(ctx context.Context) error {
organizations, err := provider.store.ListOrganizations(ctx)
if err != nil {
return err
}
for _, organizationID := range organizations {
err := provider.Refresh(ctx, organizationID)
if err != nil {
return err
}
}
if len(organizations) == 0 {
provider.settings.Logger().DebugContext(ctx, "no organizations found, defaulting to basic plan")
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
}
return nil
}
func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) error {
data, err := provider.zeus.GetLicense(ctx, key)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
}
license, err := licensetypes.NewLicense(data, organizationID)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
}
storableLicense := licensetypes.NewStorableLicenseFromLicense(license)
err = provider.store.Create(ctx, storableLicense)
if err != nil {
return err
}
err = provider.InitFeatures(ctx, license.Features)
if err != nil {
return err
}
return nil
}
func (provider *provider) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) {
storableLicenses, err := provider.store.GetAll(ctx, organizationID)
if err != nil {
return nil, err
}
activeLicense, err := licensetypes.GetActiveLicenseFromStorableLicenses(storableLicenses, organizationID)
if err != nil {
return nil, err
}
return activeLicense, nil
}
func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error {
provider.settings.Logger().DebugContext(ctx, "license validation started for organizationID", "organizationID", organizationID.StringValue())
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
provider.settings.Logger().ErrorContext(ctx, "license validation failed", "organizationID", organizationID.StringValue())
return err
}
if err != nil && errors.Ast(err, errors.TypeNotFound) {
provider.settings.Logger().DebugContext(ctx, "no active license found, defaulting to basic plan", "organizationID", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
return nil
}
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
if err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to validate the license with upstream server", "licenseID", activeLicense.Key, "organizationID", organizationID.StringValue())
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
provider.settings.Logger().ErrorContext(ctx, "license validation failed for consecutive poll intervals. defaulting to basic plan", "failureThreshold", provider.config.FailureThreshold, "licenseID", activeLicense.ID.StringValue(), "organizationID", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
return nil
}
return err
}
err = activeLicense.Update(data)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity from license data")
}
provider.settings.Logger().DebugContext(ctx, "license validation completed successfully", "licenseID", activeLicense.ID, "organizationID", organizationID.StringValue())
updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
if err != nil {
return err
}
return nil
}
func (provider *provider) Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil {
return nil, err
}
body, err := json.Marshal(postableSubscription)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal checkout payload")
}
response, err := provider.zeus.GetCheckoutURL(ctx, activeLicense.Key, body)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate checkout session")
}
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil {
return nil, err
}
body, err := json.Marshal(postableSubscription)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal portal payload")
}
response, err := provider.zeus.GetPortalURL(ctx, activeLicense.Key, body)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate portal session")
}
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
// feature surrogate
func (provider *provider) CheckFeature(ctx context.Context, key string) error {
feature, err := provider.store.GetFeature(ctx, key)
if err != nil {
return err
}
if feature.Active {
return nil
}
return errors.Newf(errors.TypeUnsupported, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key)
}
func (provider *provider) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) {
featureStatus, err := provider.store.GetFeature(ctx, key)
if err != nil {
return nil, err
}
return &featuretypes.GettableFeature{
Name: featureStatus.Name,
Active: featureStatus.Active,
Usage: int64(featureStatus.Usage),
UsageLimit: int64(featureStatus.UsageLimit),
Route: featureStatus.Route,
}, nil
}
func (provider *provider) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
storableFeatures, err := provider.store.GetAllFeatures(ctx)
if err != nil {
return nil, err
}
gettableFeatures := make([]*featuretypes.GettableFeature, len(storableFeatures))
for idx, gettableFeature := range storableFeatures {
gettableFeatures[idx] = &featuretypes.GettableFeature{
Name: gettableFeature.Name,
Active: gettableFeature.Active,
Usage: int64(gettableFeature.Usage),
UsageLimit: int64(gettableFeature.UsageLimit),
Route: gettableFeature.Route,
}
}
return gettableFeatures, nil
}
func (provider *provider) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error {
featureStatus := make([]*featuretypes.StorableFeature, len(features))
for i, f := range features {
featureStatus[i] = &featuretypes.StorableFeature{
Name: f.Name,
Active: f.Active,
Usage: int(f.Usage),
UsageLimit: int(f.UsageLimit),
Route: f.Route,
}
}
return provider.store.InitFeatures(ctx, featureStatus)
}
func (provider *provider) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error {
return provider.store.UpdateFeature(ctx, &featuretypes.StorableFeature{
Name: feature.Name,
Active: feature.Active,
Usage: int(feature.Usage),
UsageLimit: int(feature.UsageLimit),
Route: feature.Route,
})
}

View File

@ -0,0 +1,186 @@
package sqllicensingstore
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func New(sqlstore sqlstore.SQLStore) licensetypes.Store {
return &store{sqlstore}
}
func (store *store) Create(ctx context.Context, storableLicense *licensetypes.StorableLicense) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(storableLicense).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "license with ID: %s already exists", storableLicense.ID)
}
return nil
}
func (store *store) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.StorableLicense, error) {
storableLicense := new(licensetypes.StorableLicense)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storableLicense).
Where("org_id = ?", organizationID).
Where("id = ?", licenseID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "license with ID: %s does not exist", licenseID)
}
return storableLicense, nil
}
func (store *store) GetAll(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.StorableLicense, error) {
storableLicenses := make([]*licensetypes.StorableLicense, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&storableLicenses).
Where("org_id = ?", organizationID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "licenses for organizationID: %s does not exists", organizationID)
}
return storableLicenses, nil
}
func (store *store) Update(ctx context.Context, organizationID valuer.UUID, storableLicense *licensetypes.StorableLicense) error {
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(storableLicense).
WherePK().
Where("org_id = ?", organizationID).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update license with ID: %s", storableLicense.ID)
}
return nil
}
func (store *store) ListOrganizations(ctx context.Context) ([]valuer.UUID, error) {
orgIDStrs := make([]string, 0)
err := store.sqlstore.
BunDB().
NewSelect().
Model(new(types.Organization)).
Column("id").
Scan(ctx, &orgIDStrs)
if err != nil {
return nil, err
}
orgIDs := make([]valuer.UUID, len(orgIDStrs))
for idx, orgIDStr := range orgIDStrs {
orgID, err := valuer.NewUUID(orgIDStr)
if err != nil {
return nil, err
}
orgIDs[idx] = orgID
}
return orgIDs, nil
}
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(storableFeature).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "feature with name:%s already exists", storableFeature.Name)
}
return nil
}
func (store *store) GetFeature(ctx context.Context, key string) (*featuretypes.StorableFeature, error) {
storableFeature := new(featuretypes.StorableFeature)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storableFeature).
Where("name = ?", key).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "feature with name:%s does not exist", key)
}
return storableFeature, nil
}
func (store *store) GetAllFeatures(ctx context.Context) ([]*featuretypes.StorableFeature, error) {
storableFeatures := make([]*featuretypes.StorableFeature, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&storableFeatures).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "features do not exist")
}
return storableFeatures, nil
}
func (store *store) InitFeatures(ctx context.Context, storableFeatures []*featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(&storableFeatures).
On("CONFLICT (name) DO UPDATE").
Set("active = EXCLUDED.active").
Set("usage = EXCLUDED.usage").
Set("usage_limit = EXCLUDED.usage_limit").
Set("route = EXCLUDED.route").
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to initialise features")
}
return nil
}
func (store *store) UpdateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(storableFeature).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update feature with key: %s", storableFeature.Name)
}
return nil
}

View File

@ -6,29 +6,29 @@ import (
"net/http/httputil"
"time"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"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/model"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/gorilla/mux"
"go.uber.org/zap"
@ -40,8 +40,6 @@ type APIHandlerOptions struct {
AppDao dao.ModelDao
RulesManager *rules.Manager
UsageManager *usage.Manager
FeatureFlags baseint.FeatureLookup
LicenseManager *license.Manager
IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
@ -67,12 +65,12 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
Reader: opts.DataConnector,
PreferSpanMetrics: opts.PreferSpanMetrics,
RuleManager: opts.RulesManager,
FeatureFlags: opts.FeatureFlags,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
FieldsAPI: fields.NewAPI(signoz.TelemetryStore),
Signoz: signoz,
QuickFilters: quickFilter,
@ -90,18 +88,10 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
return ah, nil
}
func (ah *APIHandler) FF() baseint.FeatureLookup {
return ah.opts.FeatureFlags
}
func (ah *APIHandler) RM() *rules.Manager {
return ah.opts.RulesManager
}
func (ah *APIHandler) LM() *license.Manager {
return ah.opts.LicenseManager
}
func (ah *APIHandler) UM() *usage.Manager {
return ah.opts.UsageManager
}
@ -114,8 +104,8 @@ func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
return ah.opts.Gateway
}
func (ah *APIHandler) CheckFeature(f string) bool {
err := ah.FF().CheckFeature(f)
func (ah *APIHandler) CheckFeature(ctx context.Context, key string) bool {
err := ah.Signoz.Licensing.CheckFeature(ctx, key)
return err == nil
}
@ -151,18 +141,17 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.UpdateAPIKey)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.RevokeAPIKey)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingAPI.Checkout)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.portalSession)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v3
router.HandleFunc("/api/v3/licenses", am.ViewAccess(ah.listLicensesV3)).Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.applyLicenseV3)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.refreshLicensesV3)).Methods(http.MethodPut)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.getActiveLicenseV3)).Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.LicensingAPI.GetActive)).Methods(http.MethodGet)
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
@ -175,18 +164,14 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
}
// TODO(nitya): remove this once we know how to get the FF's
func (ah *APIHandler) updateRequestContext(w http.ResponseWriter, r *http.Request) (*http.Request, error) {
func (ah *APIHandler) updateRequestContext(_ http.ResponseWriter, r *http.Request) (*http.Request, error) {
ssoAvailable := true
err := ah.FF().CheckFeature(model.SSO)
if err != nil {
switch err.(type) {
case basemodel.ErrFeatureUnavailable:
// do nothing, just skip sso
ssoAvailable = false
default:
zap.L().Error("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err))
return r, errors.New(errors.TypeInternal, errors.CodeInternal, "error checking SSO feature")
}
err := ah.Signoz.Licensing.CheckFeature(r.Context(), licensetypes.SSO)
if err != nil && errors.Asc(err, licensing.ErrCodeFeatureUnavailable) {
ssoAvailable = false
} else if err != nil {
zap.L().Error("feature check failed", zap.String("featureKey", licensetypes.SSO), zap.Error(err))
return r, errors.New(errors.TypeInternal, errors.CodeInternal, "error checking SSO feature")
}
ctx := context.WithValue(r.Context(), types.SSOAvailable, ssoAvailable)
return r.WithContext(ctx), nil
@ -199,7 +184,6 @@ func (ah *APIHandler) loginPrecheck(w http.ResponseWriter, r *http.Request) {
return
}
ah.Signoz.Handlers.User.LoginPrecheck(w, r)
return
}
func (ah *APIHandler) acceptInvite(w http.ResponseWriter, r *http.Request) {
@ -209,7 +193,6 @@ func (ah *APIHandler) acceptInvite(w http.ResponseWriter, r *http.Request) {
return
}
ah.Signoz.Handlers.User.AcceptInvite(w, r)
return
}
func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
@ -219,7 +202,7 @@ func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
return
}
ah.Signoz.Handlers.User.GetInvite(w, r)
return
}
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {

View File

@ -12,8 +12,8 @@ import (
"go.uber.org/zap"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
)
func parseRequest(r *http.Request, req interface{}) error {
@ -35,7 +35,6 @@ func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
return
}
ah.Signoz.Handlers.User.Login(w, r)
return
}
func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) {
@ -52,7 +51,7 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
redirectUri := constants.GetDefaultSiteURL()
ctx := context.Background()
if !ah.CheckFeature(model.SSO) {
if !ah.CheckFeature(r.Context(), licensetypes.SSO) {
zap.L().Error("[receiveGoogleAuth] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return
@ -118,7 +117,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
redirectUri := constants.GetDefaultSiteURL()
ctx := context.Background()
if !ah.CheckFeature(model.SSO) {
if !ah.CheckFeature(r.Context(), licensetypes.SSO) {
zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return

View File

@ -36,6 +36,12 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
cloudProvider := mux.Vars(r)["cloudProvider"]
if cloudProvider != "aws" {
RespondError(w, basemodel.BadRequest(fmt.Errorf(
@ -56,11 +62,9 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
SigNozAPIKey: apiKey,
}
license, apiErr := ah.LM().GetRepo().GetActiveLicense(r.Context())
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't look for active license",
), nil)
license, err := ah.Signoz.Licensing.GetActive(r.Context(), orgID)
if err != nil {
render.Error(w, err)
return
}

View File

@ -9,13 +9,29 @@ import (
"time"
"github.com/SigNoz/signoz/ee/query-service/constants"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
pkgError "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
featureSet, err := ah.FF().GetFeatureFlags()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, pkgError.Newf(pkgError.TypeInvalidInput, pkgError.CodeInvalidInput, "orgId is invalid"))
return
}
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context())
if err != nil {
ah.HandleError(w, err, http.StatusInternalServerError)
return
@ -23,7 +39,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
if constants.FetchFeatures == "true" {
zap.L().Debug("fetching license")
license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
if err != nil {
zap.L().Error("failed to fetch license", zap.Error(err))
} else if license == nil {
@ -44,9 +60,8 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
}
if ah.opts.PreferSpanMetrics {
for idx := range featureSet {
feature := &featureSet[idx]
if feature.Name == basemodel.UseSpanMetrics {
for idx, feature := range featureSet {
if feature.Name == featuretypes.UseSpanMetrics {
featureSet[idx].Active = true
}
}
@ -57,7 +72,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
// and returns the FeatureSet.
func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature, error) {
// Check if the URL is empty
if url == "" {
return nil, fmt.Errorf("url is empty")
@ -116,14 +131,14 @@ func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
}
type ZeusFeaturesResponse struct {
Status string `json:"status"`
Data basemodel.FeatureSet `json:"data"`
Status string `json:"status"`
Data []*featuretypes.GettableFeature `json:"data"`
}
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basemodel.FeatureSet {
func MergeFeatureSets(zeusFeatures, internalFeatures []*featuretypes.GettableFeature) []*featuretypes.GettableFeature {
// Create a map to store the merged features
featureMap := make(map[string]basemodel.Feature)
featureMap := make(map[string]*featuretypes.GettableFeature)
// Add all features from the otherFeatures set to the map
for _, feature := range internalFeatures {
@ -137,7 +152,7 @@ func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basem
}
// Convert the map back to a FeatureSet slice
var mergedFeatures basemodel.FeatureSet
var mergedFeatures []*featuretypes.GettableFeature
for _, feature := range featureMap {
mergedFeatures = append(mergedFeatures, feature)
}

View File

@ -3,58 +3,58 @@ package api
import (
"testing"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/stretchr/testify/assert"
)
func TestMergeFeatureSets(t *testing.T) {
tests := []struct {
name string
zeusFeatures basemodel.FeatureSet
internalFeatures basemodel.FeatureSet
expected basemodel.FeatureSet
zeusFeatures []*featuretypes.GettableFeature
internalFeatures []*featuretypes.GettableFeature
expected []*featuretypes.GettableFeature
}{
{
name: "empty zeusFeatures and internalFeatures",
zeusFeatures: basemodel.FeatureSet{},
internalFeatures: basemodel.FeatureSet{},
expected: basemodel.FeatureSet{},
zeusFeatures: []*featuretypes.GettableFeature{},
internalFeatures: []*featuretypes.GettableFeature{},
expected: []*featuretypes.GettableFeature{},
},
{
name: "non-empty zeusFeatures and empty internalFeatures",
zeusFeatures: basemodel.FeatureSet{
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: basemodel.FeatureSet{},
expected: basemodel.FeatureSet{
internalFeatures: []*featuretypes.GettableFeature{},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "empty zeusFeatures and non-empty internalFeatures",
zeusFeatures: basemodel.FeatureSet{},
internalFeatures: basemodel.FeatureSet{
zeusFeatures: []*featuretypes.GettableFeature{},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
expected: basemodel.FeatureSet{
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
zeusFeatures: basemodel.FeatureSet{
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature3", Active: false},
},
internalFeatures: basemodel.FeatureSet{
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature2", Active: true},
{Name: "Feature4", Active: false},
},
expected: basemodel.FeatureSet{
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: true},
{Name: "Feature3", Active: false},
@ -63,15 +63,15 @@ func TestMergeFeatureSets(t *testing.T) {
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
zeusFeatures: basemodel.FeatureSet{
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: basemodel.FeatureSet{
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: false},
{Name: "Feature3", Active: true},
},
expected: basemodel.FeatureSet{
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
{Name: "Feature3", Active: true},

View File

@ -5,10 +5,26 @@ import (
"strings"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
validPath := false
for _, allowedPrefix := range gateway.AllowedPrefix {
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
@ -22,9 +38,9 @@ func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request
return
}
license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
if err != nil {
RespondError(rw, err, nil)
render.Error(rw, err)
return
}

View File

@ -6,11 +6,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
"github.com/SigNoz/signoz/pkg/types/authtypes"
)
type DayWiseBreakdown struct {
@ -49,10 +45,6 @@ type details struct {
BillTotal float64 `json:"billTotal"`
}
type Redirect struct {
RedirectURL string `json:"redirectURL"`
}
type billingDetails struct {
Status string `json:"status"`
Data struct {
@ -64,97 +56,6 @@ type billingDetails struct {
} `json:"data"`
}
type ApplyLicenseRequest struct {
LicenseKey string `json:"key"`
}
func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) {
ah.listLicensesV2(w, r)
}
func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) {
activeLicense, err := ah.LM().GetRepo().GetActiveLicenseV3(r.Context())
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
// return 404 not found if there is no active license
if activeLicense == nil {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no active license found")}, nil)
return
}
// TODO deprecate this when we move away from key for stripe
activeLicense.Data["key"] = activeLicense.Key
render.Success(w, http.StatusOK, activeLicense.Data)
}
// this function is called by zeus when inserting licenses in the query-service
func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
var licenseKey ApplyLicenseRequest
if err := json.NewDecoder(r.Body).Decode(&licenseKey); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
if licenseKey.LicenseKey == "" {
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
return
}
_, err = ah.LM().ActivateV3(r.Context(), licenseKey.LicenseKey)
if err != nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED, map[string]interface{}{"err": err.Error()}, claims.Email, true, false)
render.Error(w, err)
return
}
render.Success(w, http.StatusAccepted, nil)
}
func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request) {
err := ah.LM().RefreshLicense(r.Context())
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func getCheckoutPortalResponse(redirectURL string) *Redirect {
return &Redirect{RedirectURL: redirectURL}
}
func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
checkoutRequest := &model.CheckoutRequest{}
if err := json.NewDecoder(r.Body).Decode(checkoutRequest); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
license := ah.LM().GetActiveLicense()
if license == nil {
RespondError(w, model.BadRequestStr("cannot proceed with checkout without license key"), nil)
return
}
redirectUrl, err := signozio.CheckoutSession(r.Context(), checkoutRequest, license.Key, ah.Signoz.Zeus)
if err != nil {
render.Error(w, err)
return
}
ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
}
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
licenseKey := r.URL.Query().Get("licenseKey")
@ -188,71 +89,3 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
// TODO(srikanthccv):Fetch the current day usage and add it to the response
ah.Respond(w, billingResponse.Data)
}
func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License {
licensesV2 := []model.License{}
for _, l := range licenses {
planKeyFromPlanName, ok := model.MapOldPlanKeyToNewPlanName[l.PlanName]
if !ok {
planKeyFromPlanName = model.Basic
}
licenseV2 := model.License{
Key: l.Key,
ActivationId: "",
PlanDetails: "",
FeatureSet: l.Features,
ValidationMessage: "",
IsCurrent: l.IsCurrent,
LicensePlan: model.LicensePlan{
PlanKey: planKeyFromPlanName,
ValidFrom: l.ValidFrom,
ValidUntil: l.ValidUntil,
Status: l.Status},
}
licensesV2 = append(licensesV2, licenseV2)
}
return licensesV2
}
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
licensesV3, apierr := ah.LM().GetLicensesV3(r.Context())
if apierr != nil {
RespondError(w, apierr, nil)
return
}
licenses := convertLicenseV3ToLicenseV2(licensesV3)
resp := model.Licenses{
TrialStart: -1,
TrialEnd: -1,
OnTrial: false,
WorkSpaceBlock: false,
TrialConvertedToSubscription: false,
GracePeriodEnd: -1,
Licenses: licenses,
}
ah.Respond(w, resp)
}
func (ah *APIHandler) portalSession(w http.ResponseWriter, r *http.Request) {
portalRequest := &model.PortalRequest{}
if err := json.NewDecoder(r.Body).Decode(portalRequest); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
license := ah.LM().GetActiveLicense()
if license == nil {
RespondError(w, model.BadRequestStr("cannot request the portal session without license key"), nil)
return
}
redirectUrl, err := signozio.PortalSession(r.Context(), portalRequest, license.Key, ah.Signoz.Zeus)
if err != nil {
render.Error(w, err)
return
}
ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
}

View File

@ -18,6 +18,7 @@ import (
"github.com/SigNoz/signoz/ee/query-service/dao/sqlite"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/ee/query-service/rules"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
@ -30,9 +31,6 @@ import (
"github.com/rs/cors"
"github.com/soheilhy/cmux"
licensepkg "github.com/SigNoz/signoz/ee/query-service/license"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
@ -96,12 +94,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
// initiate license manager
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB(), serverOptions.SigNoz.SQLStore, serverOptions.SigNoz.Zeus)
if err != nil {
return nil, err
}
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
if err != nil {
return nil, err
@ -168,11 +160,11 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// start the usagemanager
usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus)
usageManager, err := usage.New(modelDao, serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.Organization)
if err != nil {
return nil, err
}
err = usageManager.Start()
err = usageManager.Start(context.Background())
if err != nil {
return nil, err
}
@ -197,8 +189,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
AppDao: modelDao,
RulesManager: rm,
UsageManager: usageManager,
FeatureFlags: lm,
LicenseManager: lm,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
@ -431,15 +421,15 @@ func (s *Server) Start(ctx context.Context) error {
return nil
}
func (s *Server) Stop() error {
func (s *Server) Stop(ctx context.Context) error {
if s.httpServer != nil {
if err := s.httpServer.Shutdown(context.Background()); err != nil {
if err := s.httpServer.Shutdown(ctx); err != nil {
return err
}
}
if s.privateHTTP != nil {
if err := s.privateHTTP.Shutdown(context.Background()); err != nil {
if err := s.privateHTTP.Shutdown(ctx); err != nil {
return err
}
}
@ -447,11 +437,11 @@ func (s *Server) Stop() error {
s.opampServer.Stop()
if s.ruleManager != nil {
s.ruleManager.Stop(context.Background())
s.ruleManager.Stop(ctx)
}
// stop usage manager
s.usageManager.Stop()
s.usageManager.Stop(ctx)
return nil
}

View File

@ -1,67 +0,0 @@
package signozio
import (
"context"
"encoding/json"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/tidwall/gjson"
)
func ValidateLicenseV3(ctx context.Context, licenseKey string, zeus zeus.Zeus) (*model.LicenseV3, error) {
data, err := zeus.GetLicense(ctx, licenseKey)
if err != nil {
return nil, err
}
var m map[string]any
if err = json.Unmarshal(data, &m); err != nil {
return nil, err
}
license, err := model.NewLicenseV3(m)
if err != nil {
return nil, err
}
return license, nil
}
// SendUsage reports the usage of signoz to license server
func SendUsage(ctx context.Context, usage model.UsagePayload, zeus zeus.Zeus) error {
body, err := json.Marshal(usage)
if err != nil {
return err
}
return zeus.PutMeters(ctx, usage.LicenseKey.String(), body)
}
func CheckoutSession(ctx context.Context, checkoutRequest *model.CheckoutRequest, licenseKey string, zeus zeus.Zeus) (string, error) {
body, err := json.Marshal(checkoutRequest)
if err != nil {
return "", err
}
response, err := zeus.GetCheckoutURL(ctx, licenseKey, body)
if err != nil {
return "", err
}
return gjson.GetBytes(response, "url").String(), nil
}
func PortalSession(ctx context.Context, portalRequest *model.PortalRequest, licenseKey string, zeus zeus.Zeus) (string, error) {
body, err := json.Marshal(portalRequest)
if err != nil {
return "", err
}
response, err := zeus.GetPortalURL(ctx, licenseKey, body)
if err != nil {
return "", err
}
return gjson.GetBytes(response, "url").String(), nil
}

View File

@ -1,248 +0,0 @@
package license
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"github.com/SigNoz/signoz/ee/query-service/model"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"go.uber.org/zap"
)
// Repo is license repo. stores license keys in a secured DB
type Repo struct {
db *sqlx.DB
store sqlstore.SQLStore
}
// NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB, store sqlstore.SQLStore) Repo {
return Repo{
db: db,
store: store,
}
}
func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
licensesData := []model.LicenseDB{}
licenseV3Data := []*model.LicenseV3{}
query := "SELECT id,key,data FROM licenses_v3"
err := r.db.Select(&licensesData, query)
if err != nil {
return nil, fmt.Errorf("failed to get licenses from db: %v", err)
}
for _, l := range licensesData {
var licenseData map[string]interface{}
err := json.Unmarshal([]byte(l.Data), &licenseData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
}
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
if err != nil {
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
}
licenseV3Data = append(licenseV3Data, license)
}
return licenseV3Data, nil
}
// GetActiveLicense fetches the latest active license from DB.
// If the license is not present, expect a nil license and a nil error in the output.
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
if activeLicenseV3 == nil {
return nil, nil
}
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
return activeLicenseV2, nil
}
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
var err error
licenses := []model.LicenseDB{}
query := "SELECT id,key,data FROM licenses_v3"
err = r.db.Select(&licenses, query)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
var active *model.LicenseV3
for _, l := range licenses {
var licenseData map[string]interface{}
err := json.Unmarshal([]byte(l.Data), &licenseData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
}
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
if err != nil {
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
}
if active == nil &&
(license.ValidFrom != 0) &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
active = license
}
if active != nil &&
license.ValidFrom > active.ValidFrom &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
active = license
}
}
return active, nil
}
// InsertLicenseV3 inserts a new license v3 in db
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
// licsense is the entity of zeus so putting the entire license here without defining schema
licenseData, err := json.Marshal(l.Data)
if err != nil {
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
}
_, err = r.db.ExecContext(ctx,
query,
l.ID,
l.Key,
string(licenseData),
)
if err != nil {
if sqliteErr, ok := err.(sqlite3.Error); ok {
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
}
}
zap.L().Error("error in inserting license data: ", zap.Error(err))
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
}
return nil
}
// UpdateLicenseV3 updates a new license v3 in db
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
// the key and id for the license can't change so only update the data here!
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
license, err := json.Marshal(l.Data)
if err != nil {
return fmt.Errorf("insert license failed: license marshal error")
}
_, err = r.db.ExecContext(ctx,
query,
license,
l.ID,
)
if err != nil {
zap.L().Error("error in updating license data: ", zap.Error(err))
return fmt.Errorf("failed to update license in db: %v", err)
}
return nil
}
func (r *Repo) CreateFeature(req *types.FeatureStatus) *basemodel.ApiError {
_, err := r.store.BunDB().NewInsert().
Model(req).
Exec(context.Background())
if err != nil {
return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err}
}
return nil
}
func (r *Repo) GetFeature(featureName string) (types.FeatureStatus, error) {
var feature types.FeatureStatus
err := r.store.BunDB().NewSelect().
Model(&feature).
Where("name = ?", featureName).
Scan(context.Background())
if err != nil {
return feature, err
}
if feature.Name == "" {
return feature, basemodel.ErrFeatureUnavailable{Key: featureName}
}
return feature, nil
}
func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) {
var feature []basemodel.Feature
err := r.db.Select(&feature,
`SELECT * FROM feature_status;`)
if err != nil {
return feature, err
}
return feature, nil
}
func (r *Repo) UpdateFeature(req types.FeatureStatus) error {
_, err := r.store.BunDB().NewUpdate().
Model(&req).
Where("name = ?", req.Name).
Exec(context.Background())
if err != nil {
return err
}
return nil
}
func (r *Repo) InitFeatures(req []types.FeatureStatus) error {
// get a feature by name, if it doesn't exist, create it. If it does exist, update it.
for _, feature := range req {
currentFeature, err := r.GetFeature(feature.Name)
if err != nil && err == sql.ErrNoRows {
err := r.CreateFeature(&feature)
if err != nil {
return err
}
continue
} else if err != nil {
return err
}
feature.Usage = int(currentFeature.Usage)
if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 {
feature.Active = false
}
err = r.UpdateFeature(feature)
if err != nil {
return err
}
}
return nil
}

View File

@ -1,318 +0,0 @@
package license
import (
"context"
"sync/atomic"
"time"
"github.com/jmoiron/sqlx"
"sync"
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"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"
"go.uber.org/zap"
)
var LM *Manager
// validate and update license every 24 hours
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
// stopping validation and protect in-consistent updates
done chan struct{}
// terminated waits for the validate go routine to end
terminated chan struct{}
// last time the license was validated
lastValidated int64
// keep track of validation failure attempts
failedAttempts uint64
// keep track of active license and features
activeLicenseV3 *model.LicenseV3
activeFeatures basemodel.FeatureSet
}
func StartManager(db *sqlx.DB, store sqlstore.SQLStore, zeus zeus.Zeus, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
repo := NewLicenseRepo(db, store)
m := &Manager{
repo: &repo,
zeus: zeus,
}
if err := m.start(features...); err != nil {
return m, err
}
LM = m
return m, nil
}
// start loads active license in memory and initiates validator
func (lm *Manager) start(features ...basemodel.Feature) error {
return lm.LoadActiveLicenseV3(features...)
}
func (lm *Manager) Stop() {
close(lm.done)
<-lm.terminated
}
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
if l == nil {
return
}
lm.activeLicenseV3 = l
lm.activeFeatures = append(l.Features, features...)
// set default features
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Panic("Couldn't activate features", zap.Error(err))
}
if !lm.validatorRunning {
// we want to make sure only one validator runs,
// we already have lock() so good to go
lm.validatorRunning = true
go lm.ValidatorV3(context.Background())
}
}
func setDefaultFeatures(lm *Manager) {
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
}
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
active, err := lm.repo.GetActiveLicenseV3(context.Background())
if err != nil {
return err
}
if active != nil {
lm.SetActiveV3(active, features...)
} else {
zap.L().Info("No active license found, defaulting to basic plan")
// if no active license is found, we default to basic(free) plan with all default features
lm.activeFeatures = model.BasicPlan
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Error("Couldn't initialize features", zap.Error(err))
return err
}
}
return nil
}
func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) {
licenses, err := lm.repo.GetLicensesV3(ctx)
if err != nil {
return nil, model.InternalError(err)
}
for _, l := range licenses {
if lm.activeLicenseV3 != nil && l.Key == lm.activeLicenseV3.Key {
l.IsCurrent = true
}
if l.ValidUntil == -1 {
// for subscriptions, there is no end-date as such
// but for showing user some validity we default one year timespan
l.ValidUntil = l.ValidFrom + 31556926
}
response = append(response, l)
}
return response, nil
}
// Validator validates license after an epoch of time
func (lm *Manager) ValidatorV3(ctx context.Context) {
zap.L().Info("ValidatorV3 started!")
defer close(lm.terminated)
tick := time.NewTicker(validationFrequency)
defer tick.Stop()
_ = lm.ValidateV3(ctx)
for {
select {
case <-lm.done:
return
default:
select {
case <-lm.done:
return
case <-tick.C:
_ = lm.ValidateV3(ctx)
}
}
}
}
func (lm *Manager) RefreshLicense(ctx context.Context) error {
license, err := validate.ValidateLicenseV3(ctx, lm.activeLicenseV3.Key, lm.zeus)
if err != nil {
return err
}
err = lm.repo.UpdateLicenseV3(ctx, license)
if err != nil {
return err
}
lm.SetActiveV3(license)
return nil
}
func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
if lm.activeLicenseV3 == nil {
return nil
}
defer func() {
lm.mutex.Lock()
lm.lastValidated = time.Now().Unix()
if reterr != nil {
zap.L().Error("License validation completed with error", zap.Error(reterr))
atomic.AddUint64(&lm.failedAttempts, 1)
// default to basic plan if validation fails for three consecutive times
if atomic.LoadUint64(&lm.failedAttempts) > 3 {
zap.L().Error("License validation completed with error for three consecutive times, defaulting to basic plan", zap.String("license_id", lm.activeLicenseV3.ID), zap.Bool("license_validation", false))
lm.activeLicenseV3 = nil
lm.activeFeatures = model.BasicPlan
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Error("Couldn't initialize features", zap.Error(err))
}
lm.done <- struct{}{}
lm.validatorRunning = false
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
map[string]interface{}{"err": reterr.Error()}, "", true, false)
} else {
// reset the failed attempts counter
atomic.StoreUint64(&lm.failedAttempts, 0)
zap.L().Info("License validation completed with no errors")
}
lm.mutex.Unlock()
}()
err := lm.RefreshLicense(ctx)
if err != nil {
return err
}
return nil
}
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (*model.LicenseV3, error) {
license, err := validate.ValidateLicenseV3(ctx, licenseKey, lm.zeus)
if err != nil {
return nil, err
}
// insert the new license to the sqlite db
modelErr := lm.repo.InsertLicenseV3(ctx, license)
if modelErr != nil {
zap.L().Error("failed to activate license", zap.Error(modelErr))
return nil, modelErr
}
// license is valid, activate it
lm.SetActiveV3(license)
return license, nil
}
func (lm *Manager) GetActiveLicense() *model.LicenseV3 {
return lm.activeLicenseV3
}
// CheckFeature will be internally used by backend routines
// for feature gating
func (lm *Manager) CheckFeature(featureKey string) error {
feature, err := lm.repo.GetFeature(featureKey)
if err != nil {
return err
}
if feature.Active {
return nil
}
return basemodel.ErrFeatureUnavailable{Key: featureKey}
}
// GetFeatureFlags returns current active features
func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) {
return lm.repo.GetAllFeatures()
}
func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error {
featureStatus := make([]types.FeatureStatus, len(features))
for i, f := range features {
featureStatus[i] = types.FeatureStatus{
Name: f.Name,
Active: f.Active,
Usage: int(f.Usage),
UsageLimit: int(f.UsageLimit),
Route: f.Route,
}
}
return lm.repo.InitFeatures(featureStatus)
}
func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error {
return lm.repo.UpdateFeature(types.FeatureStatus{
Name: feature.Name,
Active: feature.Active,
Usage: int(feature.Usage),
UsageLimit: int(feature.UsageLimit),
Route: feature.Route,
})
}
func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) {
featureStatus, err := lm.repo.GetFeature(key)
if err != nil {
return basemodel.Feature{}, err
}
return basemodel.Feature{
Name: featureStatus.Name,
Active: featureStatus.Active,
Usage: int64(featureStatus.Usage),
UsageLimit: int64(featureStatus.UsageLimit),
Route: featureStatus.Route,
}, nil
}
// GetRepo return the license repo
func (lm *Manager) GetRepo() *Repo {
return lm.repo
}

View File

@ -6,6 +6,8 @@ import (
"os"
"time"
"github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
eeuserimpl "github.com/SigNoz/signoz/ee/modules/user/impluser"
"github.com/SigNoz/signoz/ee/query-service/app"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
@ -16,6 +18,7 @@ import (
"github.com/SigNoz/signoz/pkg/config/fileprovider"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
pkglicensing "github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/user"
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/signoz"
@ -23,6 +26,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/version"
pkgzeus "github.com/SigNoz/signoz/pkg/zeus"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@ -90,8 +94,9 @@ func main() {
loggerMgr := initZapLog()
zap.ReplaceGlobals(loggerMgr)
defer loggerMgr.Sync() // flushes buffer, if any
ctx := context.Background()
config, err := signoz.NewConfig(context.Background(), config.ResolverConfig{
config, err := signoz.NewConfig(ctx, config.ResolverConfig{
Uris: []string{"env:"},
ProviderFactories: []config.ProviderFactory{
envprovider.NewFactory(),
@ -129,6 +134,10 @@ func main() {
config,
zeus.Config(),
httpzeus.NewProviderFactory(),
licensing.Config(24*time.Hour, 3),
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus)
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),
@ -163,22 +172,22 @@ func main() {
zap.L().Fatal("Failed to create server", zap.Error(err))
}
if err := server.Start(context.Background()); err != nil {
if err := server.Start(ctx); err != nil {
zap.L().Fatal("Could not start server", zap.Error(err))
}
signoz.Start(context.Background())
signoz.Start(ctx)
if err := signoz.Wait(context.Background()); err != nil {
if err := signoz.Wait(ctx); err != nil {
zap.L().Fatal("Failed to start signoz", zap.Error(err))
}
err = server.Stop()
err = server.Stop(ctx)
if err != nil {
zap.L().Fatal("Failed to stop server", zap.Error(err))
}
err = signoz.Stop(context.Background())
err = signoz.Stop(ctx)
if err != nil {
zap.L().Fatal("Failed to stop signoz", zap.Error(err))
}

View File

@ -1,244 +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 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":"ENTERPRISE"},"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": "ENTERPRISE",
},
"category": "FREE",
"status": "ACTIVE",
"valid_from": float64(1730899309),
"valid_until": float64(-1),
},
PlanName: PlanNameEnterprise,
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":"ENTERPRISE"},"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": "ENTERPRISE",
},
"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":"ENTERPRISE"},"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": "ENTERPRISE",
},
"valid_from": 1234.456,
"valid_until": 5678.567,
"category": "FREE",
"status": "ACTIVE",
},
PlanName: PlanNameEnterprise,
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

@ -15,8 +15,9 @@ import (
"go.uber.org/zap"
"github.com/SigNoz/signoz/ee/query-service/dao"
"github.com/SigNoz/signoz/ee/query-service/license"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/query-service/utils/encryption"
"github.com/SigNoz/signoz/pkg/zeus"
)
@ -35,64 +36,72 @@ var (
type Manager struct {
clickhouseConn clickhouse.Conn
licenseRepo *license.Repo
licenseService licensing.Licensing
scheduler *gocron.Scheduler
modelDao dao.ModelDao
zeus zeus.Zeus
organizationModule organization.Module
}
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn, zeus zeus.Zeus) (*Manager, error) {
func New(modelDao dao.ModelDao, licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus,organizationModule organization.Module) (*Manager, error) {
m := &Manager{
clickhouseConn: clickhouseConn,
licenseRepo: licenseRepo,
licenseService: licenseService,
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
modelDao: modelDao,
zeus: zeus,
organizationModule: organizationModule,
}
return m, nil
}
// start loads collects and exports any exported snapshot and starts the exporter
func (lm *Manager) Start() error {
func (lm *Manager) Start(ctx context.Context) error {
// compares the locker and stateUnlocked if both are same lock is applied else returns error
if !atomic.CompareAndSwapUint32(&locker, stateUnlocked, stateLocked) {
return fmt.Errorf("usage exporter is locked")
}
_, err := lm.scheduler.Do(func() { lm.UploadUsage() })
// upload usage once when starting the service
_, err := lm.scheduler.Do(func() { lm.UploadUsage(ctx) })
if err != nil {
return err
}
// upload usage once when starting the service
lm.UploadUsage()
lm.UploadUsage(ctx)
lm.scheduler.StartAsync()
return nil
}
func (lm *Manager) UploadUsage() {
ctx := context.Background()
// check if license is present or not
license, err := lm.licenseRepo.GetActiveLicense(ctx)
func (lm *Manager) UploadUsage(ctx context.Context) {
organizations, err := lm.organizationModule.GetAll(context.Background())
if err != nil {
zap.L().Error("failed to get active license", zap.Error(err))
return
}
if license == nil {
// we will not start the usage reporting if license is not present.
zap.L().Info("no license present, skipping usage reporting")
zap.L().Error("failed to get organizations", zap.Error(err))
return
}
for _, organization := range organizations {
// check if license is present or not
license, err := lm.licenseService.GetActive(ctx, organization.ID)
if err != nil {
zap.L().Error("failed to get active license", zap.Error(err))
return
}
if license == nil {
// we will not start the usage reporting if license is not present.
zap.L().Info("no license present, skipping usage reporting")
return
}
usages := []model.UsageDB{}
usages := []model.UsageDB{}
// get usage from clickhouse
dbs := []string{"signoz_logs", "signoz_traces", "signoz_metrics"}
query := `
// get usage from clickhouse
dbs := []string{"signoz_logs", "signoz_traces", "signoz_metrics"}
query := `
SELECT tenant, collector_id, exporter_id, timestamp, data
FROM %s.distributed_usage as u1
GLOBAL INNER JOIN
@ -107,76 +116,76 @@ func (lm *Manager) UploadUsage() {
order by timestamp
`
for _, db := range dbs {
dbusages := []model.UsageDB{}
err := lm.clickhouseConn.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
for _, db := range dbs {
dbusages := []model.UsageDB{}
err := lm.clickhouseConn.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
}
for _, u := range dbusages {
u.Type = db
usages = append(usages, u)
}
}
for _, u := range dbusages {
u.Type = db
usages = append(usages, u)
}
}
if len(usages) <= 0 {
zap.L().Info("no snapshots to upload, skipping.")
return
}
zap.L().Info("uploading usage data")
usagesPayload := []model.Usage{}
for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
if err != nil {
zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
if len(usages) <= 0 {
zap.L().Info("no snapshots to upload, skipping.")
return
}
usageData := model.Usage{}
err = json.Unmarshal(usageDataBytes, &usageData)
if err != nil {
zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
zap.L().Info("uploading usage data")
usagesPayload := []model.Usage{}
for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
if err != nil {
zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
return
}
usageData := model.Usage{}
err = json.Unmarshal(usageDataBytes, &usageData)
if err != nil {
zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
return
}
usageData.CollectorID = usage.CollectorID
usageData.ExporterID = usage.ExporterID
usageData.Type = usage.Type
usageData.Tenant = "default"
usageData.OrgName = "default"
usageData.TenantId = "default"
usagesPayload = append(usagesPayload, usageData)
}
key, _ := uuid.Parse(license.Key)
payload := model.UsagePayload{
LicenseKey: key,
Usage: usagesPayload,
}
body, errv2 := json.Marshal(payload)
if errv2 != nil {
zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2))
return
}
usageData.CollectorID = usage.CollectorID
usageData.ExporterID = usage.ExporterID
usageData.Type = usage.Type
usageData.Tenant = "default"
usageData.OrgName = "default"
usageData.TenantId = "default"
usagesPayload = append(usagesPayload, usageData)
}
key, _ := uuid.Parse(license.Key)
payload := model.UsagePayload{
LicenseKey: key,
Usage: usagesPayload,
}
body, errv2 := json.Marshal(payload)
if errv2 != nil {
zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2))
return
}
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
if errv2 != nil {
zap.L().Error("failed to upload usage: %v", zap.Error(errv2))
// not returning error here since it is captured in the failed count
return
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
if errv2 != nil {
zap.L().Error("failed to upload usage: %v", zap.Error(errv2))
// not returning error here since it is captured in the failed count
return
}
}
}
func (lm *Manager) Stop() {
func (lm *Manager) Stop(ctx context.Context) {
lm.scheduler.Stop()
zap.L().Info("sending usage data before shutting down")
// send usage before shutting down
lm.UploadUsage()
lm.UploadUsage(ctx)
atomic.StoreUint32(&locker, stateUnlocked)
}

View File

@ -36,8 +36,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
user,
isLoggedIn: isLoggedInState,
isFetchingOrgPreferences,
activeLicenseV3,
isFetchingActiveLicenseV3,
activeLicense,
isFetchingActiveLicense,
trialInfo,
featureFlags,
} = useAppContext();
@ -145,16 +145,16 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
};
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED;
const isExpired = activeLicenseV3.state === LicenseState.EXPIRED;
const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED;
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
const isExpired = activeLicense.state === LicenseState.EXPIRED;
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
const { platform } = activeLicenseV3;
const { platform } = activeLicense;
if (
isWorkspaceAccessRestricted &&
@ -164,26 +164,26 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
navigateToWorkSpaceAccessRestricted(currentRoute);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]);
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
useEffect(() => {
if (!isFetchingActiveLicenseV3) {
if (!isFetchingActiveLicense) {
const currentRoute = mapRoutes.get('current');
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
if (
shouldBlockWorkspace &&
currentRoute &&
activeLicenseV3?.platform === LicensePlatform.CLOUD
activeLicense?.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceBlocked(currentRoute);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isFetchingActiveLicenseV3,
isFetchingActiveLicense,
trialInfo?.workSpaceBlock,
activeLicenseV3?.platform,
activeLicense?.platform,
mapRoutes,
pathname,
]);
@ -197,20 +197,20 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
};
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
activeLicenseV3.state === LicenseState.DEFAULTED;
activeLicense.state === LicenseState.DEFAULTED;
if (
shouldSuspendWorkspace &&
currentRoute &&
activeLicenseV3.platform === LicensePlatform.CLOUD
activeLicense.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceSuspended(currentRoute);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]);
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) {

View File

@ -13,9 +13,9 @@ import AppLayout from 'container/AppLayout';
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useThemeConfig } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { StatusCodes } from 'http-status-codes';
import history from 'lib/history';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import posthog from 'posthog-js';
@ -41,14 +41,13 @@ import defaultRoutes, {
function App(): JSX.Element {
const themeConfig = useThemeConfig();
const {
licenses,
user,
isFetchingUser,
isFetchingLicenses,
isFetchingFeatureFlags,
trialInfo,
activeLicenseV3,
isFetchingActiveLicenseV3,
activeLicense,
isFetchingActiveLicense,
activeLicenseFetchError,
userFetchError,
featureFlagsFetchError,
isLoggedIn: isLoggedInState,
@ -66,7 +65,7 @@ function App(): JSX.Element {
const enableAnalytics = useCallback(
(user: IUser): void => {
// wait for the required data to be loaded before doing init for anything!
if (!isFetchingActiveLicenseV3 && activeLicenseV3 && org) {
if (!isFetchingActiveLicense && activeLicense && org) {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
@ -153,8 +152,8 @@ function App(): JSX.Element {
},
[
hostname,
isFetchingActiveLicenseV3,
activeLicenseV3,
isFetchingActiveLicense,
activeLicense,
org,
trialInfo?.trialConvertedToSubscription,
],
@ -163,18 +162,17 @@ function App(): JSX.Element {
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
if (
!isFetchingLicenses &&
licenses &&
!isFetchingActiveLicense &&
(activeLicense || activeLicenseFetchError) &&
!isFetchingUser &&
user &&
!!user.email
) {
const isOnBasicPlan =
licenses.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || licenses.licenses === null;
activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
);
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
@ -204,11 +202,12 @@ function App(): JSX.Element {
}, [
isLoggedInState,
user,
licenses,
isCloudUser,
isEnterpriseSelfHostedUser,
isFetchingLicenses,
isFetchingActiveLicense,
isFetchingUser,
activeLicense,
activeLicenseFetchError,
]);
useEffect(() => {
@ -231,8 +230,7 @@ function App(): JSX.Element {
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses &&
activeLicenseV3 &&
activeLicense &&
trialInfo
) {
let isChatSupportEnabled = false;
@ -270,8 +268,7 @@ function App(): JSX.Element {
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
licenses,
activeLicenseV3,
activeLicense,
trialInfo,
isCloudUser,
isEnterpriseSelfHostedUser,
@ -333,7 +330,7 @@ function App(): JSX.Element {
// if the user is in logged in state
if (isLoggedInState) {
// if the setup calls are loading then return a spinner
if (isFetchingLicenses || isFetchingUser || isFetchingFeatureFlags) {
if (isFetchingActiveLicense || isFetchingUser || isFetchingFeatureFlags) {
return <Spinner tip="Loading..." />;
}
@ -345,7 +342,11 @@ function App(): JSX.Element {
}
// if all of the data is not set then return a spinner, this is required because there is some gap between loading states and data setting
if ((!licenses || !user.email || !featureFlags) && !userFetchError) {
if (
(!activeLicense || !user.email || !featureFlags) &&
!userFetchError &&
!activeLicenseFetchError
) {
return <Spinner tip="Loading..." />;
}
}

View File

@ -1,29 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
CheckoutRequestPayloadProps,
CheckoutSuccessPayloadProps,
} from 'types/api/billing/checkout';
const updateCreditCardApi = async (
props: CheckoutRequestPayloadProps,
): Promise<SuccessResponse<CheckoutSuccessPayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/checkout', {
url: props.url,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default updateCreditCardApi;

View File

@ -1,29 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
CheckoutRequestPayloadProps,
CheckoutSuccessPayloadProps,
} from 'types/api/billing/checkout';
const manageCreditCardApi = async (
props: CheckoutRequestPayloadProps,
): Promise<SuccessResponse<CheckoutSuccessPayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/portal', {
url: props.url,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default manageCreditCardApi;

View File

@ -1,26 +0,0 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/licenses/apply';
const apply = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/licenses', {
key: props.key,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default apply;

View File

@ -1,18 +0,0 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/licenses/getAll';
const getAll = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
const response = await axios.get('/licenses');
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default getAll;

View File

@ -1,18 +0,0 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { LicenseV3EventQueueResModel } from 'types/api/licensesV3/getActive';
const getActive = async (): Promise<
SuccessResponse<LicenseV3EventQueueResModel> | ErrorResponse
> => {
const response = await axios.get('/licenses/active');
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default getActive;

View File

@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
CheckoutRequestPayloadProps,
CheckoutSuccessPayloadProps,
PayloadProps,
} from 'types/api/billing/checkout';
const updateCreditCardApi = async (
props: CheckoutRequestPayloadProps,
): Promise<SuccessResponseV2<CheckoutSuccessPayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/checkout', {
url: props.url,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default updateCreditCardApi;

View File

@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
CheckoutRequestPayloadProps,
CheckoutSuccessPayloadProps,
PayloadProps,
} from 'types/api/billing/checkout';
const manageCreditCardApi = async (
props: CheckoutRequestPayloadProps,
): Promise<SuccessResponseV2<CheckoutSuccessPayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/portal', {
url: props.url,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default manageCreditCardApi;

View File

@ -0,0 +1,25 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
LicenseEventQueueResModel,
PayloadProps,
} from 'types/api/licensesV3/getActive';
const getActive = async (): Promise<
SuccessResponseV2<LicenseEventQueueResModel>
> => {
try {
const response = await axios.get<PayloadProps>('/licenses/active');
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getActive;

View File

@ -0,0 +1,24 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/licenses/apply';
const apply = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/licenses', {
key: props.key,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default apply;

View File

@ -1,14 +1,14 @@
import { Button, Modal, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import updateCreditCardApi from 'api/v1/checkout/create';
import { useNotifications } from 'hooks/useNotifications';
import { CreditCard, X } from 'lucide-react';
import { useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
@ -18,20 +18,21 @@ export default function ChatSupportGateway(): JSX.Element {
);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
): void => {
if (data?.payload?.redirectURL) {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};

View File

@ -1,10 +1,9 @@
import './LaunchChatSupport.styles.scss';
import { Button, Modal, Tooltip, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
@ -14,8 +13,9 @@ import { useAppContext } from 'providers/App/App';
import { useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
export interface LaunchChatSupportProps {
eventName: string;
@ -118,20 +118,21 @@ function LaunchChatSupport({
};
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
): void => {
if (data?.payload?.redirectURL) {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};

View File

@ -5,16 +5,15 @@ import './AppLayout.styles.scss';
import * as Sentry from '@sentry/react';
import { Flex } from 'antd';
import manageCreditCardApi from 'api/billing/manage';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import manageCreditCardApi from 'api/v1/portal/create';
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
import getUserVersion from 'api/v1/version/getVersion';
import cx from 'classnames';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
@ -51,8 +50,9 @@ import {
UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import {
LicenseEvent,
LicensePlatform,
@ -75,8 +75,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isLoggedIn,
user,
trialInfo,
activeLicenseV3,
isFetchingActiveLicenseV3,
activeLicense,
isFetchingActiveLicense,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
@ -93,20 +93,21 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
): void => {
if (data?.payload?.redirectURL) {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};
@ -260,8 +261,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
useEffect(() => {
if (
!isFetchingActiveLicenseV3 &&
activeLicenseV3 &&
!isFetchingActiveLicense &&
activeLicense &&
trialInfo?.onTrial &&
!trialInfo?.trialConvertedToSubscription &&
!trialInfo?.workSpaceBlock &&
@ -269,16 +270,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
) {
setShowTrialExpiryBanner(true);
}
}, [isFetchingActiveLicenseV3, activeLicenseV3, trialInfo]);
}, [isFetchingActiveLicense, activeLicense, trialInfo]);
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED;
const isExpired = activeLicenseV3.state === LicenseState.EXPIRED;
const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED;
const isDefaulted = activeLicenseV3.state === LicenseState.DEFAULTED;
if (!isFetchingActiveLicense && activeLicense) {
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
const isExpired = activeLicense.state === LicenseState.EXPIRED;
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
const isDefaulted = activeLicense.state === LicenseState.DEFAULTED;
const isEvaluationExpired =
activeLicenseV3.state === LicenseState.EVALUATION_EXPIRED;
activeLicense.state === LicenseState.EVALUATION_EXPIRED;
const isWorkspaceAccessRestricted =
isTerminated ||
@ -287,7 +288,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isDefaulted ||
isEvaluationExpired;
const { platform } = activeLicenseV3;
const { platform } = activeLicense;
if (
isWorkspaceAccessRestricted &&
@ -296,17 +297,17 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
setShowWorkspaceRestricted(true);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
}, [isFetchingActiveLicense, activeLicense]);
useEffect(() => {
if (
!isFetchingActiveLicenseV3 &&
!isNull(activeLicenseV3) &&
activeLicenseV3?.event_queue?.event === LicenseEvent.DEFAULT
!isFetchingActiveLicense &&
!isNull(activeLicense) &&
activeLicense?.event_queue?.event === LicenseEvent.DEFAULT
) {
setShowPaymentFailedWarning(true);
}
}, [activeLicenseV3, isFetchingActiveLicenseV3]);
}, [activeLicense, isFetchingActiveLicense]);
useEffect(() => {
// after logging out hide the trial expiry banner
@ -392,7 +393,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
activeLicenseV3 &&
activeLicense &&
trialInfo
) {
let isChatSupportEnabled = false;
@ -421,7 +422,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isCloudUserVal,
isFetchingFeatureFlags,
isLoggedIn,
activeLicenseV3,
activeLicense,
trialInfo,
]);
@ -523,14 +524,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const renderWorkspaceRestrictedBanner = (): JSX.Element => (
<div className="workspace-restricted-banner">
{activeLicenseV3?.state === LicenseState.TERMINATED && (
{activeLicense?.state === LicenseState.TERMINATED && (
<>
Your SigNoz license is terminated, enterprise features have been disabled.
Please contact support at{' '}
<a href="mailto:support@signoz.io">support@signoz.io</a> for new license
</>
)}
{activeLicenseV3?.state === LicenseState.EXPIRED && (
{activeLicense?.state === LicenseState.EXPIRED && (
<>
Your SigNoz license has expired. Please contact support at{' '}
<a href="mailto:support@signoz.io">support@signoz.io</a> for renewal to
@ -544,7 +545,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</a>
</>
)}
{activeLicenseV3?.state === LicenseState.CANCELLED && (
{activeLicense?.state === LicenseState.CANCELLED && (
<>
Your SigNoz license is cancelled. Please contact support at{' '}
<a href="mailto:support@signoz.io">support@signoz.io</a> for reactivation
@ -559,7 +560,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</>
)}
{activeLicenseV3?.state === LicenseState.DEFAULTED && (
{activeLicense?.state === LicenseState.DEFAULTED && (
<>
Your SigNoz license is defaulted. Please clear the bill to continue using
the enterprise features. Contact support at{' '}
@ -575,7 +576,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</>
)}
{activeLicenseV3?.state === LicenseState.EVALUATION_EXPIRED && (
{activeLicense?.state === LicenseState.EVALUATION_EXPIRED && (
<>
Your SigNoz trial has ended. Please contact support at{' '}
<a href="mailto:support@signoz.io">support@signoz.io</a> for next steps to
@ -624,7 +625,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
Your bill payment has failed. Your workspace will get suspended on{' '}
<span>
{getFormattedDateWithMinutes(
dayjs(activeLicenseV3?.event_queue?.scheduled_at).unix() || Date.now(),
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
)}
.
</span>

View File

@ -16,10 +16,10 @@ import {
Typography,
} from 'antd';
import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout';
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';
import manageCreditCardApi from 'api/v1/portal/create';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@ -31,9 +31,8 @@ import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
@ -126,7 +125,6 @@ export default function BillingContainer(): JSX.Element {
const daysRemainingStr = t('days_remaining');
const [headerText, setHeaderText] = useState('');
const [billAmount, setBillAmount] = useState(0);
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [daysRemaining, setDaysRemaining] = useState(0);
const [isFreeTrial, setIsFreeTrial] = useState(false);
const [data, setData] = useState<any[]>([]);
@ -137,11 +135,10 @@ export default function BillingContainer(): JSX.Element {
const {
user,
org,
licenses,
trialInfo,
isFetchingActiveLicenseV3,
activeLicenseV3,
activeLicenseV3FetchError,
isFetchingActiveLicense,
activeLicense,
activeLicenseFetchError,
} = useAppContext();
const { notifications } = useNotifications();
@ -216,14 +213,9 @@ export default function BillingContainer(): JSX.Element {
});
useEffect(() => {
const activeValidLicense =
licenses?.licenses?.find((license) => license.isCurrent === true) || null;
setActiveLicense(activeValidLicense);
if (
!isFetchingActiveLicenseV3 &&
!activeLicenseV3FetchError &&
!isFetchingActiveLicense &&
!activeLicenseFetchError &&
trialInfo?.onTrial
) {
const remainingDays = getRemainingDays(trialInfo?.trialEnd);
@ -238,12 +230,11 @@ export default function BillingContainer(): JSX.Element {
);
}
}, [
licenses?.licenses,
activeLicenseV3,
activeLicense,
trialInfo?.onTrial,
trialInfo?.trialEnd,
isFetchingActiveLicenseV3,
activeLicenseV3FetchError,
isFetchingActiveLicense,
activeLicenseFetchError,
]);
const columns: ColumnsType<DataType> = [
@ -288,11 +279,11 @@ export default function BillingContainer(): JSX.Element {
);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
): void => {
if (data?.payload?.redirectURL) {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();

View File

@ -19,12 +19,12 @@ function DataSourceInfo({
dataSentToSigNoz: boolean;
isLoading: boolean;
}): JSX.Element {
const { activeLicenseV3 } = useAppContext();
const { activeLicense } = useAppContext();
const notSendingData = !dataSentToSigNoz;
const isEnabled =
activeLicenseV3 && activeLicenseV3.platform === LicensePlatform.CLOUD;
activeLicense && activeLicense.platform === LicensePlatform.CLOUD;
const {
data: deploymentsData,
@ -88,8 +88,8 @@ function DataSourceInfo({
logEvent('Homepage: Connect dataSource clicked', {});
if (
activeLicenseV3 &&
activeLicenseV3.platform === LicensePlatform.CLOUD
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
@ -105,8 +105,8 @@ function DataSourceInfo({
logEvent('Homepage: Connect dataSource clicked', {});
if (
activeLicenseV3 &&
activeLicenseV3.platform === LicensePlatform.CLOUD
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {

View File

@ -300,17 +300,17 @@ export default function Home(): JSX.Element {
}
}, [hostData, k8sPodsData, handleUpdateChecklistDoneItem]);
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
const { activeLicense, isFetchingActiveLicense } = useAppContext();
const [isEnabled, setIsEnabled] = useState(false);
useEffect(() => {
if (isFetchingActiveLicenseV3) {
if (isFetchingActiveLicense) {
setIsEnabled(false);
return;
}
setIsEnabled(Boolean(activeLicenseV3?.platform === LicensePlatform.CLOUD));
}, [activeLicenseV3, isFetchingActiveLicenseV3]);
setIsEnabled(Boolean(activeLicense?.platform === LicensePlatform.CLOUD));
}, [activeLicense, isFetchingActiveLicense]);
const { data: deploymentsData } = useGetDeploymentsData(isEnabled);

View File

@ -32,7 +32,7 @@ function HomeChecklist({
onSkip: (item: ChecklistItem) => void;
isLoading: boolean;
}): JSX.Element {
const { user, activeLicenseV3 } = useAppContext();
const { user, activeLicense } = useAppContext();
const [completedChecklistItems, setCompletedChecklistItems] = useState<
ChecklistItem[]
@ -94,8 +94,8 @@ function HomeChecklist({
if (item.toRoute !== ROUTES.GET_STARTED_WITH_CLOUD) {
history.push(item.toRoute || '');
} else if (
activeLicenseV3 &&
activeLicenseV3.platform === LicensePlatform.CLOUD
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
history.push(item.toRoute || '');
} else {

View File

@ -23,7 +23,7 @@ import { Link } from 'react-router-dom';
import { AppState } from 'store/reducers';
import {
LicensePlatform,
LicenseV3ResModel,
LicenseResModel,
} from 'types/api/licensesV3/getActive';
import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
@ -42,7 +42,7 @@ const EmptyState = memo(
activeLicenseV3,
}: {
user: IUser;
activeLicenseV3: LicenseV3ResModel | null;
activeLicenseV3: LicenseResModel | null;
}): JSX.Element => (
<div className="empty-state-container">
<div className="empty-state-content-container">
@ -146,7 +146,7 @@ function ServiceMetrics({
GlobalReducer
>((state) => state.globalTime);
const { user, activeLicenseV3 } = useAppContext();
const { user, activeLicense } = useAppContext();
const [timeRange, setTimeRange] = useState(() => {
const now = new Date().getTime();
@ -335,7 +335,7 @@ function ServiceMetrics({
{servicesExist ? (
<ServicesListTable services={top5Services} onRowClick={handleRowClick} />
) : (
<EmptyState user={user} activeLicenseV3={activeLicenseV3} />
<EmptyState user={user} activeLicenseV3={activeLicense} />
)}
</Card.Content>

View File

@ -32,7 +32,7 @@ export default function ServiceTraces({
(state) => state.globalTime,
);
const { user, activeLicenseV3 } = useAppContext();
const { user, activeLicense } = useAppContext();
const now = new Date().getTime();
const [timeRange, setTimeRange] = useState({
@ -124,8 +124,8 @@ export default function ServiceTraces({
});
if (
activeLicenseV3 &&
activeLicenseV3.platform === LicensePlatform.CLOUD
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
@ -160,7 +160,7 @@ export default function ServiceTraces({
</div>
</div>
),
[user?.role, activeLicenseV3],
[user?.role, activeLicense],
);
const renderDashboardsList = useCallback(

View File

@ -1,8 +1,9 @@
import { Button, Form, Input } from 'antd';
import apply from 'api/licenses/apply';
import apply from 'api/v3/licenses/put';
import { useNotifications } from 'hooks/useNotifications';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import APIError from 'types/api/error';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import {
@ -36,27 +37,18 @@ function ApplyLicenseForm({
setIsLoading(true);
try {
const response = await apply({
await apply({
key: params.key,
});
if (response.statusCode === 200) {
await Promise.all([licenseRefetch()]);
notifications.success({
message: 'Success',
description: t('license_applied'),
});
} else {
notifications.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
await Promise.all([licenseRefetch()]);
notifications.success({
message: 'Success',
description: t('license_applied'),
});
} catch (e) {
notifications.error({
message: 'Error',
description: t('unexpected_error'),
message: (e as APIError).getErrorCode(),
description: (e as APIError).getErrorMessage(),
});
}
setIsLoading(false);

View File

@ -1,58 +0,0 @@
import { Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { useTimezone } from 'providers/Timezone';
import { useTranslation } from 'react-i18next';
import { License } from 'types/api/licenses/def';
function ValidityColumn({ value }: { value: string }): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
return (
<Typography>
{formatTimezoneAdjustedTimestamp(value, DATE_TIME_FORMATS.ISO_DATETIME_UTC)}
</Typography>
);
}
function ListLicenses({ licenses }: ListLicensesProps): JSX.Element {
const { t } = useTranslation(['licenses']);
const columns: ColumnsType<License> = [
{
title: t('column_license_status'),
dataIndex: 'status',
key: 'status',
width: 100,
},
{
title: t('column_license_key'),
dataIndex: 'key',
key: 'key',
width: 80,
},
{
title: t('column_valid_from'),
dataIndex: 'ValidFrom',
key: 'valid from',
render: (value: string): JSX.Element => ValidityColumn({ value }),
width: 80,
},
{
title: t('column_valid_until'),
dataIndex: 'ValidUntil',
key: 'valid until',
render: (value: string): JSX.Element => ValidityColumn({ value }),
width: 80,
},
];
return <ResizeTable columns={columns} rowKey="id" dataSource={licenses} />;
}
interface ListLicensesProps {
licenses: License[];
}
export default ListLicenses;

View File

@ -4,29 +4,20 @@ import { useAppContext } from 'providers/App/App';
import { useTranslation } from 'react-i18next';
import ApplyLicenseForm from './ApplyLicenseForm';
import ListLicenses from './ListLicenses';
function Licenses(): JSX.Element {
const { t, ready: translationsReady } = useTranslation(['licenses']);
const { licenses, licensesRefetch } = useAppContext();
const { activeLicenseRefetch } = useAppContext();
if (!translationsReady) {
return <Spinner tip={t('loading_licenses')} height="90vh" />;
}
const allValidLicense =
licenses?.licenses?.filter((license) => license.isCurrent) || [];
const tabs = [
{
label: t('tab_current_license'),
key: 'licenses',
children: <ApplyLicenseForm licenseRefetch={licensesRefetch} />,
},
{
label: t('tab_license_history'),
key: 'history',
children: <ListLicenses licenses={allValidLicense} />,
children: <ApplyLicenseForm licenseRefetch={activeLicenseRefetch} />,
},
];

View File

@ -33,7 +33,7 @@ function ServiceMetricTable({
const { notifications } = useNotifications();
const { t: getText } = useTranslation(['services']);
const { isFetchingActiveLicenseV3, trialInfo } = useAppContext();
const { isFetchingActiveLicense, trialInfo } = useAppContext();
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const queries = useGetQueriesRange(queryRangeRequestData, ENTITY_VERSION_V4, {
@ -70,7 +70,7 @@ function ServiceMetricTable({
useEffect(() => {
if (
!isFetchingActiveLicenseV3 &&
!isFetchingActiveLicense &&
trialInfo?.onTrial &&
!trialInfo?.trialConvertedToSubscription &&
isCloudUserVal
@ -85,7 +85,7 @@ function ServiceMetricTable({
}, [
services,
isCloudUserVal,
isFetchingActiveLicenseV3,
isFetchingActiveLicense,
trialInfo?.onTrial,
trialInfo?.trialConvertedToSubscription,
]);

View File

@ -21,13 +21,13 @@ function ServiceTraceTable({
const [RPS, setRPS] = useState(0);
const { t: getText } = useTranslation(['services']);
const { isFetchingActiveLicenseV3, trialInfo } = useAppContext();
const { isFetchingActiveLicense, trialInfo } = useAppContext();
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const tableColumns = useMemo(() => getColumns(search, false), [search]);
useEffect(() => {
if (
!isFetchingActiveLicenseV3 &&
!isFetchingActiveLicense &&
trialInfo?.onTrial &&
!trialInfo?.trialConvertedToSubscription &&
isCloudUserVal
@ -42,7 +42,7 @@ function ServiceTraceTable({
}, [
services,
isCloudUserVal,
isFetchingActiveLicenseV3,
isFetchingActiveLicense,
trialInfo?.onTrial,
trialInfo?.trialConvertedToSubscription,
]);

View File

@ -12,7 +12,7 @@ import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { LICENSE_PLAN_KEY, LICENSE_PLAN_STATUS } from 'hooks/useLicense';
import { StatusCodes } from 'http-status-codes';
import history from 'lib/history';
import {
AlertTriangle,
@ -26,7 +26,6 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
@ -59,7 +58,13 @@ function SideNav(): JSX.Element {
AppReducer
>((state) => state.app);
const { user, featureFlags, licenses, trialInfo } = useAppContext();
const {
user,
featureFlags,
trialInfo,
activeLicense,
activeLicenseFetchError,
} = useAppContext();
const isOnboardingV3Enabled = featureFlags?.find(
(flag) => flag.name === FeatureKeys.ONBOARDING_V3,
@ -96,14 +101,11 @@ function SideNav(): JSX.Element {
const { t } = useTranslation('');
const licenseStatus: string =
licenses?.licenses?.find((e: License) => e.isCurrent)?.status || '';
const licenseStatus: string = activeLicense?.status || '';
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const isLicenseActive =
licenseStatus?.toLocaleLowerCase() ===
LICENSE_PLAN_STATUS.VALID.toLocaleLowerCase();
const isLicenseActive = licenseStatus === 'VALID';
const onClickSignozCloud = (): void => {
window.open(
@ -299,10 +301,10 @@ function SideNav(): JSX.Element {
}
const isOnBasicPlan =
licenses?.licenses?.some(
(license: License) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || licenses?.licenses === null;
activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
);
if (user.role !== USER_ROLES.ADMIN || isOnBasicPlan) {
updatedMenuItems = updatedMenuItems.filter(
@ -347,10 +349,10 @@ function SideNav(): JSX.Element {
isEnterpriseSelfHostedUser,
isCurrentVersionError,
isLatestVersion,
licenses?.licenses,
onClickVersionHandler,
t,
user.role,
activeLicenseFetchError,
]);
return (
@ -443,7 +445,7 @@ function SideNav(): JSX.Element {
onClick={onClickShortcuts}
/>
{licenses && !isLicenseActive && (
{!isLicenseActive && (
<NavItem
key="trySignozCloud"
item={trySignozCloudMenuItem}

View File

@ -1,8 +1,9 @@
import getActive from 'api/licensesV3/getActive';
import getActive from 'api/v3/licenses/active/get';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { LicenseV3ResModel } from 'types/api/licensesV3/getActive';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { LicenseResModel } from 'types/api/licensesV3/getActive';
const useActiveLicenseV3 = (isLoggedIn: boolean): UseLicense =>
useQuery({
@ -11,9 +12,6 @@ const useActiveLicenseV3 = (isLoggedIn: boolean): UseLicense =>
enabled: !!isLoggedIn,
});
type UseLicense = UseQueryResult<
SuccessResponse<LicenseV3ResModel> | ErrorResponse,
unknown
>;
type UseLicense = UseQueryResult<SuccessResponseV2<LicenseResModel>, APIError>;
export default useActiveLicenseV3;

View File

@ -1,4 +1,3 @@
import { AxiosError } from 'axios';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
@ -8,26 +7,26 @@ export const useGetTenantLicense = (): {
isCommunityUser: boolean;
isCommunityEnterpriseUser: boolean;
} => {
const { activeLicenseV3, activeLicenseV3FetchError } = useAppContext();
const { activeLicense, activeLicenseFetchError } = useAppContext();
const responsePayload = {
isCloudUser: activeLicenseV3?.platform === LicensePlatform.CLOUD || false,
isCloudUser: activeLicense?.platform === LicensePlatform.CLOUD || false,
isEnterpriseSelfHostedUser:
activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED || false,
activeLicense?.platform === LicensePlatform.SELF_HOSTED || false,
isCommunityUser: false,
isCommunityEnterpriseUser: false,
};
if (
activeLicenseV3FetchError &&
(activeLicenseV3FetchError as AxiosError)?.response?.status === 404
activeLicenseFetchError &&
activeLicenseFetchError.getHttpStatusCode() === 404
) {
responsePayload.isCommunityEnterpriseUser = true;
}
if (
activeLicenseV3FetchError &&
(activeLicenseV3FetchError as AxiosError)?.response?.status === 501
activeLicenseFetchError &&
activeLicenseFetchError.getHttpStatusCode() === 501
) {
responsePayload.isCommunityUser = true;
}

View File

@ -1,8 +0,0 @@
export const LICENSE_PLAN_KEY = {
ENTERPRISE_PLAN: 'ENTERPRISE_PLAN',
BASIC_PLAN: 'BASIC_PLAN',
};
export const LICENSE_PLAN_STATUS = {
VALID: 'VALID',
};

View File

@ -1,6 +0,0 @@
import { LICENSE_PLAN_KEY, LICENSE_PLAN_STATUS } from './constant';
import useLicense from './useLicense';
export default useLicense;
export { LICENSE_PLAN_KEY, LICENSE_PLAN_STATUS };

View File

@ -1,20 +0,0 @@
import getAll from 'api/licenses/getAll';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
// import { useAppContext } from 'providers/App/App';
import { useQuery, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/licenses/getAll';
const useLicense = (isLoggedIn: boolean): UseLicense =>
useQuery({
queryFn: getAll,
queryKey: [REACT_QUERY_KEY.GET_ALL_LICENCES],
enabled: !!isLoggedIn,
});
type UseLicense = UseQueryResult<
SuccessResponse<PayloadProps> | ErrorResponse,
unknown
>;
export default useLicense;

View File

@ -1,9 +1,8 @@
import './Support.styles.scss';
import { Button, Card, Modal, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import updateCreditCardApi from 'api/v1/checkout/create';
import { FeatureKeys } from 'constants/features';
import { useNotifications } from 'hooks/useNotifications';
import {
@ -18,8 +17,9 @@ import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useHistory, useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
const { Title, Text } = Typography;
@ -109,20 +109,21 @@ export default function Support(): JSX.Element {
!isPremiumChatSupportEnabled && !trialInfo?.trialConvertedToSubscription;
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
): void => {
if (data?.payload?.redirectURL) {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};

View File

@ -8,24 +8,24 @@ import { useEffect } from 'react';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
function WorkspaceAccessRestricted(): JSX.Element {
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
const { activeLicense, isFetchingActiveLicense } = useAppContext();
useEffect(() => {
if (!isFetchingActiveLicenseV3) {
const isTerminated = activeLicenseV3?.state === LicenseState.TERMINATED;
const isExpired = activeLicenseV3?.state === LicenseState.EXPIRED;
const isCancelled = activeLicenseV3?.state === LicenseState.CANCELLED;
if (!isFetchingActiveLicense) {
const isTerminated = activeLicense?.state === LicenseState.TERMINATED;
const isExpired = activeLicense?.state === LicenseState.EXPIRED;
const isCancelled = activeLicense?.state === LicenseState.CANCELLED;
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
if (
!isWorkspaceAccessRestricted ||
activeLicenseV3.platform === LicensePlatform.SELF_HOSTED
activeLicense.platform === LicensePlatform.SELF_HOSTED
) {
history.push(ROUTES.HOME);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
}, [isFetchingActiveLicense, activeLicense]);
return (
<div>
@ -44,7 +44,7 @@ function WorkspaceAccessRestricted(): JSX.Element {
width="65%"
>
<div className="workspace-access-restricted__container">
{isFetchingActiveLicenseV3 || !activeLicenseV3 ? (
{isFetchingActiveLicense || !activeLicense ? (
<Skeleton />
) : (
<>
@ -55,7 +55,7 @@ function WorkspaceAccessRestricted(): JSX.Element {
level={4}
className="workspace-access-restricted__details"
>
{activeLicenseV3.state === LicenseState.TERMINATED && (
{activeLicense.state === LicenseState.TERMINATED && (
<>
Your SigNoz license is terminated, please contact support at{' '}
<a href="mailto:cloud-support@signoz.io">
@ -64,7 +64,7 @@ function WorkspaceAccessRestricted(): JSX.Element {
for a new deployment
</>
)}
{activeLicenseV3.state === LicenseState.EXPIRED && (
{activeLicense.state === LicenseState.EXPIRED && (
<>
Your SigNoz license is expired, please contact support at{' '}
<a href="mailto:cloud-support@signoz.io">
@ -81,7 +81,7 @@ function WorkspaceAccessRestricted(): JSX.Element {
.
</>
)}
{activeLicenseV3.state === LicenseState.CANCELLED && (
{activeLicense.state === LicenseState.CANCELLED && (
<>
Your SigNoz license is cancelled, please contact support at{' '}
<a href="mailto:cloud-support@signoz.io">

View File

@ -16,8 +16,8 @@ import {
Tabs,
Typography,
} from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
@ -26,6 +26,7 @@ import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import APIError from 'types/api/error';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { getFormattedDate } from 'utils/timeUtils';
@ -41,9 +42,9 @@ import {
export default function WorkspaceBlocked(): JSX.Element {
const {
user,
isFetchingActiveLicenseV3,
isFetchingActiveLicense,
trialInfo,
activeLicenseV3,
activeLicense,
} = useAppContext();
const isAdmin = user.role === 'ADMIN';
const { notifications } = useNotifications();
@ -70,37 +71,38 @@ export default function WorkspaceBlocked(): JSX.Element {
};
useEffect(() => {
if (!isFetchingActiveLicenseV3) {
if (!isFetchingActiveLicense) {
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
if (
!shouldBlockWorkspace ||
activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED
activeLicense?.platform === LicensePlatform.SELF_HOSTED
) {
history.push(ROUTES.HOME);
}
}
}, [
isFetchingActiveLicenseV3,
isFetchingActiveLicense,
trialInfo?.workSpaceBlock,
activeLicenseV3?.platform,
activeLicense?.platform,
]);
const { mutate: updateCreditCard, isLoading } = useMutation(
updateCreditCardApi,
{
onSuccess: (data) => {
if (data.payload?.redirectURL) {
if (data.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
},
onError: () =>
onError: (error: APIError) =>
notifications.error({
message: t('somethingWentWrong'),
message: error.getErrorCode(),
description: error.getErrorMessage(),
}),
},
);
@ -320,7 +322,7 @@ export default function WorkspaceBlocked(): JSX.Element {
width="65%"
>
<div className="workspace-locked__container">
{isFetchingActiveLicenseV3 || !trialInfo ? (
{isFetchingActiveLicense || !trialInfo ? (
<Skeleton />
) : (
<>

View File

@ -10,7 +10,7 @@ import {
Space,
Typography,
} from 'antd';
import manageCreditCardApi from 'api/billing/manage';
import manageCreditCardApi from 'api/v1/portal/create';
import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
@ -19,6 +19,7 @@ import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import APIError from 'types/api/error';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { getFormattedDateWithMinutes } from 'utils/timeUtils';
@ -26,7 +27,7 @@ function WorkspaceSuspended(): JSX.Element {
const { user } = useAppContext();
const isAdmin = user.role === 'ADMIN';
const { notifications } = useNotifications();
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
const { activeLicense, isFetchingActiveLicense } = useAppContext();
const { t } = useTranslation(['failedPayment']);
@ -34,17 +35,18 @@ function WorkspaceSuspended(): JSX.Element {
manageCreditCardApi,
{
onSuccess: (data) => {
if (data.payload?.redirectURL) {
if (data.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
},
onError: () =>
onError: (error: APIError) =>
notifications.error({
message: t('somethingWentWrong'),
message: error.getErrorCode(),
description: error.getErrorMessage(),
}),
},
);
@ -56,18 +58,18 @@ function WorkspaceSuspended(): JSX.Element {
}, [manageCreditCard]);
useEffect(() => {
if (!isFetchingActiveLicenseV3) {
if (!isFetchingActiveLicense) {
const shouldSuspendWorkspace =
activeLicenseV3?.state === LicenseState.DEFAULTED;
activeLicense?.state === LicenseState.DEFAULTED;
if (
!shouldSuspendWorkspace ||
activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED
activeLicense?.platform === LicensePlatform.SELF_HOSTED
) {
history.push(ROUTES.HOME);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
}, [isFetchingActiveLicense, activeLicense]);
return (
<div>
<Modal
@ -99,7 +101,7 @@ function WorkspaceSuspended(): JSX.Element {
width="65%"
>
<div className="workspace-suspended__container">
{isFetchingActiveLicenseV3 || !activeLicenseV3 ? (
{isFetchingActiveLicense || !activeLicense ? (
<Skeleton />
) : (
<>
@ -115,7 +117,7 @@ function WorkspaceSuspended(): JSX.Element {
{t('yourDataIsSafe')}{' '}
<span className="workspace-suspended__details__highlight">
{getFormattedDateWithMinutes(
dayjs(activeLicenseV3?.event_queue?.scheduled_at).unix() ||
dayjs(activeLicense?.event_queue?.scheduled_at).unix() ||
Date.now(),
)}
</span>{' '}

View File

@ -6,7 +6,6 @@ import dayjs from 'dayjs';
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
import { useGlobalEventListener } from 'hooks/useGlobalEventListener';
import useLicense from 'hooks/useLicense';
import useGetUser from 'hooks/user/useGetUser';
import {
createContext,
@ -19,11 +18,10 @@ import {
} from 'react';
import { useQuery } from 'react-query';
import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags';
import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll';
import {
LicensePlatform,
LicenseResModel,
LicenseState,
LicenseV3ResModel,
TrialInfo,
} from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization';
@ -38,14 +36,10 @@ export const AppContext = createContext<IAppContext | undefined>(undefined);
export function AppProvider({ children }: PropsWithChildren): JSX.Element {
// on load of the provider set the user defaults with access jwt , refresh jwt and user id from local storage
const [user, setUser] = useState<IUser>(() => getUserDefaults());
const [licenses, setLicenses] = useState<LicensesResModel | null>(null);
const [
activeLicenseV3,
setActiveLicenseV3,
] = useState<LicenseV3ResModel | null>(null);
const [activeLicense, setActiveLicense] = useState<LicenseResModel | null>(
null,
);
const [trialInfo, setTrialInfo] = useState<TrialInfo | null>(null);
const [featureFlags, setFeatureFlags] = useState<FeatureFlags[] | null>(null);
const [orgPreferences, setOrgPreferences] = useState<OrgPreference[] | null>(
null,
@ -103,59 +97,40 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
}
}, [userData, isFetchingUser]);
// fetcher for licenses v2
// license will be fetched if we are in logged in state
const {
data: licenseData,
isFetching: isFetchingLicenses,
error: licensesFetchError,
refetch: licensesRefetch,
} = useLicense(isLoggedIn);
useEffect(() => {
if (!isFetchingLicenses && licenseData && licenseData.payload) {
setLicenses(licenseData.payload);
}
}, [licenseData, isFetchingLicenses]);
// fetcher for licenses v3
const {
data: activeLicenseV3Data,
isFetching: isFetchingActiveLicenseV3,
error: activeLicenseV3FetchError,
data: activeLicenseData,
isFetching: isFetchingActiveLicense,
error: activeLicenseFetchError,
refetch: activeLicenseRefetch,
} = useActiveLicenseV3(isLoggedIn);
useEffect(() => {
if (
!isFetchingActiveLicenseV3 &&
activeLicenseV3Data &&
activeLicenseV3Data.payload
) {
setActiveLicenseV3(activeLicenseV3Data.payload);
if (!isFetchingActiveLicense && activeLicenseData && activeLicenseData.data) {
setActiveLicense(activeLicenseData.data);
const isOnTrial = dayjs(
activeLicenseV3Data.payload.free_until || Date.now(),
activeLicenseData.data.free_until || Date.now(),
).isAfter(dayjs());
const trialInfo: TrialInfo = {
trialStart: activeLicenseV3Data.payload.valid_from,
trialEnd: dayjs(
activeLicenseV3Data.payload.free_until || Date.now(),
).unix(),
trialStart: activeLicenseData.data.valid_from,
trialEnd: dayjs(activeLicenseData.data.free_until || Date.now()).unix(),
onTrial: isOnTrial,
workSpaceBlock:
activeLicenseV3Data.payload.state === LicenseState.EVALUATION_EXPIRED &&
activeLicenseV3Data.payload.platform === LicensePlatform.CLOUD,
activeLicenseData.data.state === LicenseState.EVALUATION_EXPIRED &&
activeLicenseData.data.platform === LicensePlatform.CLOUD,
trialConvertedToSubscription:
activeLicenseV3Data.payload.state !== LicenseState.ISSUED &&
activeLicenseV3Data.payload.state !== LicenseState.EVALUATING &&
activeLicenseV3Data.payload.state !== LicenseState.EVALUATION_EXPIRED,
activeLicenseData.data.state !== LicenseState.ISSUED &&
activeLicenseData.data.state !== LicenseState.EVALUATING &&
activeLicenseData.data.state !== LicenseState.EVALUATION_EXPIRED,
gracePeriodEnd: dayjs(
activeLicenseV3Data.payload.event_queue.scheduled_at || Date.now(),
activeLicenseData.data.event_queue.scheduled_at || Date.now(),
).unix(),
};
setTrialInfo(trialInfo);
}
}, [activeLicenseV3Data, isFetchingActiveLicenseV3]);
}, [activeLicenseData, isFetchingActiveLicense]);
// fetcher for feature flags
const {
@ -242,9 +217,8 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
useGlobalEventListener('LOGOUT', () => {
setIsLoggedIn(false);
setUser(getUserDefaults());
setActiveLicenseV3(null);
setActiveLicense(null);
setTrialInfo(null);
setLicenses(null);
setFeatureFlags(null);
setOrgPreferences(null);
setOrg(null);
@ -254,46 +228,40 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
const value: IAppContext = useMemo(
() => ({
user,
licenses,
activeLicenseV3,
activeLicense,
featureFlags,
trialInfo,
orgPreferences,
isLoggedIn,
org,
isFetchingUser,
isFetchingLicenses,
isFetchingActiveLicenseV3,
isFetchingActiveLicense,
isFetchingFeatureFlags,
isFetchingOrgPreferences,
userFetchError,
licensesFetchError,
activeLicenseV3FetchError,
activeLicenseFetchError,
featureFlagsFetchError,
orgPreferencesFetchError,
licensesRefetch,
activeLicenseRefetch,
updateUser,
updateOrgPreferences,
updateOrg,
}),
[
trialInfo,
activeLicenseV3,
activeLicenseV3FetchError,
activeLicense,
activeLicenseFetchError,
featureFlags,
featureFlagsFetchError,
isFetchingActiveLicenseV3,
isFetchingActiveLicense,
isFetchingFeatureFlags,
isFetchingLicenses,
isFetchingOrgPreferences,
isFetchingUser,
isLoggedIn,
licenses,
licensesFetchError,
licensesRefetch,
org,
orgPreferences,
orgPreferencesFetchError,
activeLicenseRefetch,
updateOrg,
user,
userFetchError,

View File

@ -1,30 +1,27 @@
import APIError from 'types/api/error';
import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags';
import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll';
import { LicenseV3ResModel, TrialInfo } from 'types/api/licensesV3/getActive';
import { LicenseResModel, TrialInfo } from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization';
import { UserResponse as User } from 'types/api/user/getUser';
import { OrgPreference } from 'types/reducer/app';
export interface IAppContext {
user: IUser;
licenses: LicensesResModel | null;
activeLicenseV3: LicenseV3ResModel | null;
activeLicense: LicenseResModel | null;
trialInfo: TrialInfo | null;
featureFlags: FeatureFlags[] | null;
orgPreferences: OrgPreference[] | null;
isLoggedIn: boolean;
org: Organization[] | null;
isFetchingUser: boolean;
isFetchingLicenses: boolean;
isFetchingActiveLicenseV3: boolean;
isFetchingActiveLicense: boolean;
isFetchingFeatureFlags: boolean;
isFetchingOrgPreferences: boolean;
userFetchError: unknown;
licensesFetchError: unknown;
activeLicenseV3FetchError: unknown;
activeLicenseFetchError: APIError | null;
featureFlagsFetchError: unknown;
orgPreferencesFetchError: unknown;
licensesRefetch: () => void;
activeLicenseRefetch: () => void;
updateUser: (user: IUser) => void;
updateOrgPreferences: (orgPreferences: OrgPreference[]) => void;
updateOrg(orgId: string, updatedOrgName: string): void;

View File

@ -105,7 +105,8 @@ export function getAppContextMock(
appContextOverrides?: Partial<IAppContext>,
): IAppContext {
return {
activeLicenseV3: {
activeLicense: {
key: 'test-key',
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
@ -138,8 +139,8 @@ export function getAppContextMock(
trialConvertedToSubscription: false,
gracePeriodEnd: -1,
},
isFetchingActiveLicenseV3: false,
activeLicenseV3FetchError: null,
isFetchingActiveLicense: false,
activeLicenseFetchError: null,
user: {
accessJwt: 'some-token',
refreshJwt: 'some-refresh-token',
@ -160,20 +161,6 @@ export function getAppContextMock(
],
isFetchingUser: false,
userFetchError: null,
licenses: {
licenses: [
{
key: 'does-not-matter',
isCurrent: true,
planKey: 'ENTERPRISE_PLAN',
ValidFrom: new Date(),
ValidUntil: new Date(),
status: 'VALID',
},
],
},
isFetchingLicenses: false,
licensesFetchError: null,
featureFlags: [
{
name: FeatureKeys.SSO,
@ -246,7 +233,7 @@ export function getAppContextMock(
updateUser: jest.fn(),
updateOrg: jest.fn(),
updateOrgPreferences: jest.fn(),
licensesRefetch: jest.fn(),
activeLicenseRefetch: jest.fn(),
...appContextOverrides,
};
}

View File

@ -5,3 +5,8 @@ export interface CheckoutSuccessPayloadProps {
export interface CheckoutRequestPayloadProps {
url: string;
}
export interface PayloadProps {
data: CheckoutSuccessPayloadProps;
status: string;
}

View File

@ -1,5 +0,0 @@
import { License } from './def';
export type PayloadProps = {
licenses: License[];
};

View File

@ -30,7 +30,7 @@ export const LicensePlanKey = {
BASIC: 'BASIC',
};
export type LicenseV3EventQueueResModel = {
export type LicenseEventQueueResModel = {
event: LicenseEvent;
status: string;
scheduled_at: string;
@ -38,10 +38,11 @@ export type LicenseV3EventQueueResModel = {
updated_at: string;
};
export type LicenseV3ResModel = {
export type LicenseResModel = {
key: string;
status: LicenseStatus;
state: LicenseState;
event_queue: LicenseV3EventQueueResModel;
event_queue: LicenseEventQueueResModel;
platform: LicensePlatform;
created_at: string;
plan: {
@ -67,3 +68,8 @@ export type TrialInfo = {
trialConvertedToSubscription: boolean;
gracePeriodEnd: number;
};
export interface PayloadProps {
data: LicenseEventQueueResModel;
status: string;
}

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

@ -0,0 +1,18 @@
package licensing
import (
"time"
"github.com/SigNoz/signoz/pkg/factory"
)
var _ factory.Config = (*Config)(nil)
type Config struct {
PollInterval time.Duration `mapstructure:"poll_interval"`
FailureThreshold int `mapstructure:"failure_threshold"`
}
func (c Config) Validate() error {
return nil
}

View File

@ -0,0 +1,55 @@
package licensing
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
ErrCodeUnsupported = errors.MustNewCode("licensing_unsupported")
ErrCodeFeatureUnavailable = errors.MustNewCode("feature_unavailable")
)
type Licensing interface {
factory.Service
// Validate validates the license with the upstream server
Validate(ctx context.Context) error
// Activate validates and enables the license
Activate(ctx context.Context, organizationID valuer.UUID, key string) error
// GetActive fetches the current active license in org
GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error)
// Refresh refreshes the license state from upstream server
Refresh(ctx context.Context, organizationID valuer.UUID) error
// Checkout creates a checkout session via upstream server and returns the redirection link
Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error)
// Portal creates a portal session via upstream server and return the redirection link
Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error)
// feature surrogate
// CheckFeature checks if the feature is active or not
CheckFeature(ctx context.Context, key string) error
// GetFeatureFlags fetches all the defined feature flags
GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error)
// GetFeatureFlags fetches all the defined feature flags
GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error)
// InitFeatures initialises the feature flags
InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error
// UpdateFeatureFlag updates the feature flag
UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error
}
type API interface {
Activate(http.ResponseWriter, *http.Request)
Refresh(http.ResponseWriter, *http.Request)
GetActive(http.ResponseWriter, *http.Request)
Checkout(http.ResponseWriter, *http.Request)
Portal(http.ResponseWriter, *http.Request)
}

View File

@ -0,0 +1,35 @@
package nooplicensing
import (
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/licensing"
)
type noopLicensingAPI struct{}
func NewLicenseAPI() licensing.API {
return &noopLicensingAPI{}
}
func (api *noopLicensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}
func (api *noopLicensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}
func (api *noopLicensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}
func (api *noopLicensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}
func (api *noopLicensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}

View File

@ -0,0 +1,99 @@
package nooplicensing
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type noopLicensing struct {
stopChan chan struct{}
}
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(_ context.Context, _ factory.ProviderSettings, _ licensing.Config) (licensing.Licensing, error) {
return &noopLicensing{stopChan: make(chan struct{})}, nil
}
func (provider *noopLicensing) Start(context.Context) error {
<-provider.stopChan
return nil
}
func (provider *noopLicensing) Stop(context.Context) error {
close(provider.stopChan)
return nil
}
func (provider *noopLicensing) Activate(ctx context.Context, organizationID valuer.UUID, key string) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "fetching license is not supported")
}
func (provider *noopLicensing) Validate(ctx context.Context) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "validating license is not supported")
}
func (provider *noopLicensing) Refresh(ctx context.Context, organizationID valuer.UUID) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "refreshing license is not supported")
}
func (provider *noopLicensing) Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "checkout session is not supported")
}
func (provider *noopLicensing) Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "portal session is not supported")
}
func (provider *noopLicensing) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) {
return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "fetching active license is not supported")
}
func (provider *noopLicensing) CheckFeature(ctx context.Context, key string) error {
feature, err := provider.GetFeatureFlag(ctx, key)
if err != nil {
return err
}
if feature.Active {
return nil
}
return errors.Newf(errors.TypeNotFound, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key)
}
func (provider *noopLicensing) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) {
features, err := provider.GetFeatureFlags(ctx)
if err != nil {
return nil, err
}
for _, feature := range features {
if feature.Name == key {
return feature, nil
}
}
return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "no feature available with given key: %s", key)
}
func (provider *noopLicensing) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
return licensetypes.DefaultFeatureSet, nil
}
func (provider *noopLicensing) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "init features is not supported")
}
func (provider *noopLicensing) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "updating feature flag is not supported")
}

View File

@ -92,7 +92,7 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
return
}
_, err = h.module.CreateBulkInvite(ctx, claims.OrgID, claims.UserID, &types.PostableBulkInviteRequest{
invites, err := h.module.CreateBulkInvite(ctx, claims.OrgID, claims.UserID, &types.PostableBulkInviteRequest{
Invites: []types.PostableInvite{req},
})
if err != nil {
@ -100,7 +100,7 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
return
}
render.Success(rw, http.StatusCreated, nil)
render.Success(rw, http.StatusCreated, invites[0])
}
func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {

View File

@ -23,6 +23,7 @@ import (
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
@ -58,6 +59,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/postprocess"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
@ -89,7 +91,6 @@ func NewRouter() *mux.Router {
type APIHandler struct {
reader interfaces.Reader
ruleManager *rules.Manager
featureFlags interfaces.FeatureLookup
querier interfaces.Querier
querierV2 interfaces.Querier
queryBuilder *queryBuilder.QueryBuilder
@ -136,6 +137,8 @@ type APIHandler struct {
AlertmanagerAPI *alertmanager.API
LicensingAPI licensing.API
FieldsAPI *fields.API
Signoz *signoz.SigNoz
@ -155,9 +158,6 @@ type APIHandlerOpts struct {
// rule manager handles rule crud operations
RuleManager *rules.Manager
// feature flags querier
FeatureFlags interfaces.FeatureLookup
// Integrations
IntegrationsController *integrations.Controller
@ -177,6 +177,8 @@ type APIHandlerOpts struct {
AlertmanagerAPI *alertmanager.API
LicensingAPI licensing.API
FieldsAPI *fields.API
Signoz *signoz.SigNoz
@ -224,7 +226,6 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
preferSpanMetrics: opts.PreferSpanMetrics,
temporalityMap: make(map[string]map[v3.Temporality]bool),
ruleManager: opts.RuleManager,
featureFlags: opts.FeatureFlags,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
@ -244,6 +245,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
JWT: opts.JWT,
SummaryService: summaryService,
AlertmanagerAPI: opts.AlertmanagerAPI,
LicensingAPI: opts.LicensingAPI,
Signoz: opts.Signoz,
FieldsAPI: opts.FieldsAPI,
QuickFilters: opts.QuickFilters,
@ -607,7 +609,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
render.Success(rw, http.StatusOK, []any{})
})).Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
render.Error(rw, errorsV2.New(errorsV2.TypeUnsupported, errorsV2.CodeUnsupported, "not implemented"))
aH.LicensingAPI.Activate(rw, req)
})).Methods(http.MethodGet)
}
@ -1979,15 +1981,14 @@ func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
featureSet, err := aH.FF().GetFeatureFlags()
featureSet, err := aH.Signoz.Licensing.GetFeatureFlags(r.Context())
if err != nil {
aH.HandleError(w, err, http.StatusInternalServerError)
return
}
if aH.preferSpanMetrics {
for idx := range featureSet {
feature := &featureSet[idx]
if feature.Name == model.UseSpanMetrics {
for idx, feature := range featureSet {
if feature.Name == featuretypes.UseSpanMetrics {
featureSet[idx].Active = true
}
}
@ -1995,12 +1996,8 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
aH.Respond(w, featureSet)
}
func (aH *APIHandler) FF() interfaces.FeatureLookup {
return aH.featureFlags
}
func (aH *APIHandler) CheckFeature(f string) bool {
err := aH.FF().CheckFeature(f)
func (aH *APIHandler) CheckFeature(ctx context.Context, key string) bool {
err := aH.Signoz.Licensing.CheckFeature(ctx, key)
return err == nil
}

View File

@ -8,7 +8,6 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
metricsV3 "github.com/SigNoz/signoz/pkg/query-service/app/metrics/v3"
"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

@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
"github.com/SigNoz/signoz/pkg/prometheus"
@ -34,7 +35,6 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/featureManager"
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/rules"
@ -81,8 +81,6 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
// NewServer creates and initializes Server
func NewServer(serverOptions *ServerOptions) (*Server, error) {
// initiate feature manager
fm := featureManager.StartManager()
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
if err != nil {
@ -146,13 +144,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
Reader: reader,
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
RuleManager: rm,
FeatureFlags: fm,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: fluxInterval,
JWT: serverOptions.Jwt,
AlertmanagerAPI: alertmanager.NewAPI(serverOptions.SigNoz.Alertmanager),
LicensingAPI: nooplicensing.NewLicenseAPI(),
FieldsAPI: fields.NewAPI(serverOptions.SigNoz.TelemetryStore),
Signoz: serverOptions.SigNoz,
QuickFilters: quickFilter,

View File

@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
)
const (
@ -65,9 +66,9 @@ func UseMetricsPreAggregation() bool {
var KafkaSpanEval = GetOrDefaultEnv("KAFKA_SPAN_EVAL", "false")
var DEFAULT_FEATURE_SET = model.FeatureSet{
model.Feature{
Name: model.UseSpanMetrics,
var DEFAULT_FEATURE_SET = []*featuretypes.GettableFeature{
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,

View File

@ -1,60 +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 := constants.DEFAULT_FEATURE_SET
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

@ -11,6 +11,8 @@ import (
"github.com/SigNoz/signoz/pkg/config/fileprovider"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/query-service/app"
@ -120,6 +122,10 @@ func main() {
config,
zeus.Config{},
noopzeus.NewProviderFactory(),
licensing.Config{},
func(_ sqlstore.SQLStore, _ zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return nooplicensing.NewFactory()
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),

View File

@ -1,38 +0,0 @@
package model
type FeatureSet []Feature
type Feature struct {
Name string `db:"name" json:"name"`
Active bool `db:"active" json:"active"`
Usage int64 `db:"usage" json:"usage"`
UsageLimit int64 `db:"usage_limit" json:"usage_limit"`
Route string `db:"route" json:"route"`
}
const UseSpanMetrics = "USE_SPAN_METRICS"
const AnomalyDetection = "ANOMALY_DETECTION"
const TraceFunnels = "TRACE_FUNNELS"
var BasicPlan = FeatureSet{
Feature{
Name: UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: AnomalyDetection,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
Feature{
Name: TraceFunnels,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@ -24,7 +24,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/featureManager"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/signoz"
@ -304,7 +303,6 @@ func (tb *FilterSuggestionsTestBed) GetQBFilterSuggestionsForLogs(
func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
testDB := utils.NewQueryServiceDBForTests(t)
fm := featureManager.StartManager()
reader, mockClickhouse := NewMockClickhouseReader(t, testDB)
mockClickhouse.MatchExpectationsInOrder(false)
@ -317,9 +315,8 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(testDB)))
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
Reader: reader,
FeatureFlags: fm,
JWT: jwt,
Reader: reader,
JWT: jwt,
Signoz: &signoz.SigNoz{
Modules: modules,
Handlers: signoz.NewHandlers(modules, userHandler),

View File

@ -24,7 +24,6 @@ import (
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/featureManager"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
@ -366,7 +365,6 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI
t.Fatalf("could not create cloud integrations controller: %v", err)
}
fm := featureManager.StartManager()
reader, mockClickhouse := NewMockClickhouseReader(t, testDB)
mockClickhouse.MatchExpectationsInOrder(false)
@ -382,7 +380,6 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
Reader: reader,
CloudIntegrationsController: controller,
FeatureFlags: fm,
JWT: jwt,
Signoz: &signoz.SigNoz{
Modules: modules,

View File

@ -22,7 +22,6 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/featureManager"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils"
@ -567,7 +566,6 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration
t.Fatalf("could not create integrations controller: %v", err)
}
fm := featureManager.StartManager()
reader, mockClickhouse := NewMockClickhouseReader(t, testDB)
mockClickhouse.MatchExpectationsInOrder(false)
@ -587,9 +585,9 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration
quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(testDB)))
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
Reader: reader,
IntegrationsController: controller,
FeatureFlags: fm,
Reader: reader,
IntegrationsController: controller,
JWT: jwt,
CloudIntegrationsController: cloudIntegrationsController,
Signoz: &signoz.SigNoz{

View File

@ -80,6 +80,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
sqlmigration.NewCreateQuickFiltersFactory(sqlstore),
sqlmigration.NewUpdateQuickFiltersFactory(sqlstore),
sqlmigration.NewAuthRefactorFactory(sqlstore),
sqlmigration.NewUpdateLicenseFactory(sqlstore),
sqlmigration.NewMigratePATToFactorAPIKey(sqlstore),
)
}

View File

@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/sqlmigration"
@ -30,6 +31,7 @@ type SigNoz struct {
Prometheus prometheus.Prometheus
Alertmanager alertmanager.Alertmanager
Zeus zeus.Zeus
Licensing licensing.Licensing
Emailing emailing.Emailing
Modules Modules
Handlers Handlers
@ -40,6 +42,8 @@ func New(
config Config,
zeusConfig zeus.Config,
zeusProviderFactory factory.ProviderFactory[zeus.Zeus, zeus.Config],
licenseConfig licensing.Config,
licenseProviderFactoryCb func(sqlstore.SQLStore, zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config],
emailingProviderFactories factory.NamedMap[factory.ProviderFactory[emailing.Emailing, emailing.Config]],
cacheProviderFactories factory.NamedMap[factory.ProviderFactory[cache.Cache, cache.Config]],
webProviderFactories factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]],
@ -171,6 +175,16 @@ func New(
return nil, err
}
licensingProviderFactory := licenseProviderFactoryCb(sqlstore, zeus)
licensing, err := licensingProviderFactory.New(
ctx,
providerSettings,
licenseConfig,
)
if err != nil {
return nil, err
}
userModule := userModuleFactory(sqlstore, emailing, providerSettings)
userHandler := userHandlerFactory(userModule)
@ -184,6 +198,7 @@ func New(
instrumentation.Logger(),
factory.NewNamedService(factory.MustNewName("instrumentation"), instrumentation),
factory.NewNamedService(factory.MustNewName("alertmanager"), alertmanager),
factory.NewNamedService(factory.MustNewName("licensing"), licensing),
)
if err != nil {
return nil, err
@ -199,6 +214,7 @@ func New(
Prometheus: prometheus,
Alertmanager: alertmanager,
Zeus: zeus,
Licensing: licensing,
Emailing: emailing,
Modules: modules,
Handlers: handlers,

View File

@ -0,0 +1,149 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type updateLicense struct {
store sqlstore.SQLStore
}
type existingLicense34 struct {
bun.BaseModel `bun:"table:licenses_v3"`
ID string `bun:"id,pk,type:text"`
Key string `bun:"key,type:text,notnull,unique"`
Data string `bun:"data,type:text"`
}
type newLicense34 struct {
bun.BaseModel `bun:"table:license"`
types.Identifiable
types.TimeAuditable
Key string `bun:"key,type:text,notnull,unique"`
Data map[string]any `bun:"data,type:text"`
LastValidatedAt time.Time `bun:"last_validated_at,notnull"`
OrgID string `bun:"org_id,type:text,notnull" json:"orgID"`
}
func NewUpdateLicenseFactory(store sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("update_license"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return newUpdateLicense(ctx, ps, c, store)
})
}
func newUpdateLicense(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) {
return &updateLicense{store: store}, nil
}
func (migration *updateLicense) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *updateLicense) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
err = migration.store.Dialect().RenameTableAndModifyModel(ctx, tx, new(existingLicense34), new(newLicense34), []string{OrgReference}, func(ctx context.Context) error {
existingLicenses := make([]*existingLicense34, 0)
err = tx.NewSelect().Model(&existingLicenses).Scan(ctx)
if err != nil {
if err != sql.ErrNoRows {
return err
}
}
if err == nil && len(existingLicenses) > 0 {
var orgID string
err := migration.
store.
BunDB().
NewSelect().
Model((*types.Organization)(nil)).
Column("id").
Scan(ctx, &orgID)
if err != nil {
if err != sql.ErrNoRows {
return err
}
}
if err == nil {
newLicenses, err := migration.CopyExistingLicensesToNewLicenses(existingLicenses, orgID)
if err != nil {
return err
}
_, err = tx.
NewInsert().
Model(&newLicenses).
Exec(ctx)
if err != nil {
return err
}
}
return nil
}
return nil
})
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (migration *updateLicense) Down(context.Context, *bun.DB) error {
return nil
}
func (migration *updateLicense) CopyExistingLicensesToNewLicenses(existingLicenses []*existingLicense34, orgID string) ([]*newLicense34, error) {
newLicenses := make([]*newLicense34, len(existingLicenses))
for idx, existingLicense := range existingLicenses {
licenseID, err := valuer.NewUUID(existingLicense.ID)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "license id is not a valid UUID: %s", existingLicense.ID)
}
licenseData := map[string]any{}
err = json.Unmarshal([]byte(existingLicense.Data), &licenseData)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "unable to unmarshal license data in map[string]any")
}
newLicenses[idx] = &newLicense34{
Identifiable: types.Identifiable{
ID: licenseID,
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Key: existingLicense.Key,
Data: licenseData,
LastValidatedAt: time.Now(),
OrgID: orgID,
}
}
return newLicenses, nil
}

View File

@ -0,0 +1,28 @@
package featuretypes
import "github.com/uptrace/bun"
type FeatureSet []*GettableFeature
type GettableFeature struct {
Name string `db:"name" json:"name"`
Active bool `db:"active" json:"active"`
Usage int64 `db:"usage" json:"usage"`
UsageLimit int64 `db:"usage_limit" json:"usage_limit"`
Route string `db:"route" json:"route"`
}
type StorableFeature struct {
bun.BaseModel `bun:"table:feature_status"`
Name string `bun:"name,pk,type:text" json:"name"`
Active bool `bun:"active" json:"active"`
Usage int `bun:"usage,default:0" json:"usage"`
UsageLimit int `bun:"usage_limit,default:0" json:"usage_limit"`
Route string `bun:"route,type:text" json:"route"`
}
func NewStorableFeature() {}
const UseSpanMetrics = "USE_SPAN_METRICS"
const AnomalyDetection = "ANOMALY_DETECTION"
const TraceFunnels = "TRACE_FUNNELS"

View File

@ -85,3 +85,7 @@ type PostableInvite struct {
type PostableBulkInviteRequest struct {
Invites []PostableInvite `json:"invites"`
}
type GettableCreateInviteResponse struct {
InviteToken string `json:"token"`
}

View File

@ -1,46 +0,0 @@
package types
import (
"time"
"github.com/uptrace/bun"
)
type License struct {
bun.BaseModel `bun:"table:licenses"`
Key string `bun:"key,pk,type:text"`
CreatedAt time.Time `bun:"createdAt,default:current_timestamp"`
UpdatedAt time.Time `bun:"updatedAt,default:current_timestamp"`
PlanDetails string `bun:"planDetails,type:text"`
ActivationID string `bun:"activationId,type:text"`
ValidationMessage string `bun:"validationMessage,type:text"`
LastValidated time.Time `bun:"lastValidated,default:current_timestamp"`
}
type Site struct {
bun.BaseModel `bun:"table:sites"`
UUID string `bun:"uuid,pk,type:text"`
Alias string `bun:"alias,type:varchar(180),default:'PROD'"`
URL string `bun:"url,type:varchar(300)"`
CreatedAt time.Time `bun:"createdAt,default:current_timestamp"`
}
type FeatureStatus struct {
bun.BaseModel `bun:"table:feature_status"`
Name string `bun:"name,pk,type:text" json:"name"`
Active bool `bun:"active" json:"active"`
Usage int `bun:"usage,default:0" json:"usage"`
UsageLimit int `bun:"usage_limit,default:0" json:"usage_limit"`
Route string `bun:"route,type:text" json:"route"`
}
type LicenseV3 struct {
bun.BaseModel `bun:"table:licenses_v3"`
ID string `bun:"id,pk,type:text"`
Key string `bun:"key,type:text,notnull,unique"`
Data string `bun:"data,type:text"`
}

View File

@ -0,0 +1,389 @@
package licensetypes
import (
"context"
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type StorableLicense struct {
bun.BaseModel `bun:"table:license"`
types.Identifiable
types.TimeAuditable
Key string `bun:"key,type:text,notnull,unique"`
Data map[string]any `bun:"data,type:text"`
LastValidatedAt time.Time `bun:"last_validated_at,notnull"`
OrgID valuer.UUID `bun:"org_id,type:text,notnull" json:"orgID"`
}
// this data excludes ID and Key
type License struct {
ID valuer.UUID
Key string
Data map[string]interface{}
PlanName string
Features []*featuretypes.GettableFeature
Status string
ValidFrom int64
ValidUntil int64
CreatedAt time.Time
UpdatedAt time.Time
LastValidatedAt time.Time
OrganizationID valuer.UUID
}
type GettableLicense map[string]any
type PostableLicense struct {
Key string `json:"key"`
}
func NewStorableLicense(ID valuer.UUID, key string, data map[string]any, createdAt, updatedAt, lastValidatedAt time.Time, organizationID valuer.UUID) *StorableLicense {
return &StorableLicense{
Identifiable: types.Identifiable{
ID: ID,
},
TimeAuditable: types.TimeAuditable{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
Key: key,
Data: data,
LastValidatedAt: lastValidatedAt,
OrgID: organizationID,
}
}
func NewStorableLicenseFromLicense(license *License) *StorableLicense {
return &StorableLicense{
Identifiable: types.Identifiable{
ID: license.ID,
},
TimeAuditable: types.TimeAuditable{
CreatedAt: license.CreatedAt,
UpdatedAt: license.UpdatedAt,
},
Key: license.Key,
Data: license.Data,
LastValidatedAt: license.LastValidatedAt,
OrgID: license.OrganizationID,
}
}
func GetActiveLicenseFromStorableLicenses(storableLicenses []*StorableLicense, organizationID valuer.UUID) (*License, error) {
var activeLicense *License
for _, storableLicense := range storableLicenses {
license, err := NewLicenseFromStorableLicense(storableLicense)
if err != nil {
return nil, err
}
if license.Status != "VALID" {
continue
}
if activeLicense == nil &&
(license.ValidFrom != 0) &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
activeLicense = license
}
if activeLicense != nil &&
license.ValidFrom > activeLicense.ValidFrom &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
activeLicense = license
}
}
if activeLicense == nil {
return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "no active license found for the organization %s", organizationID.StringValue())
}
return activeLicense, nil
}
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 NewLicense(data []byte, organizationID valuer.UUID) (*License, error) {
licenseData := map[string]any{}
err := json.Unmarshal(data, &licenseData)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal license data")
}
var features []*featuretypes.GettableFeature
// extract id from data
licenseIDStr, err := extractKeyFromMapStringInterface[string](licenseData, "id")
if err != nil {
return nil, err
}
licenseID, err := valuer.NewUUID(licenseIDStr)
if err != nil {
return nil, err
}
delete(licenseData, "id")
// extract key from data
licenseKey, err := extractKeyFromMapStringInterface[string](licenseData, "key")
if err != nil {
return nil, err
}
delete(licenseData, "key")
// extract status from data
status, err := extractKeyFromMapStringInterface[string](licenseData, "status")
if err != nil {
return nil, err
}
planMap, err := extractKeyFromMapStringInterface[map[string]any](licenseData, "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 := make([]*featuretypes.GettableFeature, 0)
if _features, ok := licenseData["features"]; ok {
featuresData, err := json.Marshal(_features)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal features data")
}
if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal features data")
}
}
switch planName {
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
}
}
}
licenseData["features"] = features
_validFrom, err := extractKeyFromMapStringInterface[float64](licenseData, "valid_from")
if err != nil {
_validFrom = 0
}
validFrom := int64(_validFrom)
_validUntil, err := extractKeyFromMapStringInterface[float64](licenseData, "valid_until")
if err != nil {
_validUntil = 0
}
validUntil := int64(_validUntil)
return &License{
ID: licenseID,
Key: licenseKey,
Data: licenseData,
PlanName: planName,
Features: features,
ValidFrom: validFrom,
ValidUntil: validUntil,
Status: status,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
LastValidatedAt: time.Now(),
OrganizationID: organizationID,
}, nil
}
func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License, error) {
var features []*featuretypes.GettableFeature
// extract status from data
status, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "status")
if err != nil {
return nil, err
}
planMap, err := extractKeyFromMapStringInterface[map[string]any](storableLicense.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 := make([]*featuretypes.GettableFeature, 0)
if _features, ok := storableLicense.Data["features"]; ok {
featuresData, err := json.Marshal(_features)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal features data")
}
if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal features data")
}
}
switch planName {
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
}
}
}
storableLicense.Data["features"] = features
_validFrom, err := extractKeyFromMapStringInterface[float64](storableLicense.Data, "valid_from")
if err != nil {
_validFrom = 0
}
validFrom := int64(_validFrom)
_validUntil, err := extractKeyFromMapStringInterface[float64](storableLicense.Data, "valid_until")
if err != nil {
_validUntil = 0
}
validUntil := int64(_validUntil)
return &License{
ID: storableLicense.ID,
Key: storableLicense.Key,
Data: storableLicense.Data,
PlanName: planName,
Features: features,
ValidFrom: validFrom,
ValidUntil: validUntil,
Status: status,
CreatedAt: storableLicense.CreatedAt,
UpdatedAt: storableLicense.UpdatedAt,
LastValidatedAt: storableLicense.LastValidatedAt,
OrganizationID: storableLicense.OrgID,
}, nil
}
func (license *License) Update(data []byte) error {
updatedLicense, err := NewLicense(data, license.OrganizationID)
if err != nil {
return err
}
currentTime := time.Now()
license.Data = updatedLicense.Data
license.Features = updatedLicense.Features
license.ID = updatedLicense.ID
license.Key = updatedLicense.Key
license.PlanName = updatedLicense.PlanName
license.Status = updatedLicense.Status
license.ValidFrom = updatedLicense.ValidFrom
license.ValidUntil = updatedLicense.ValidUntil
license.UpdatedAt = currentTime
license.LastValidatedAt = currentTime
return nil
}
func NewGettableLicense(data map[string]any, key string) *GettableLicense {
gettableLicense := make(GettableLicense)
for k, v := range data {
gettableLicense[k] = v
}
gettableLicense["key"] = key
return &gettableLicense
}
func (p *PostableLicense) UnmarshalJSON(data []byte) error {
var postableLicense struct {
Key string `json:"key"`
}
err := json.Unmarshal(data, &postableLicense)
if err != nil {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal payload")
}
if postableLicense.Key == "" {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license key cannot be empty")
}
p.Key = postableLicense.Key
return nil
}
type Store interface {
Create(context.Context, *StorableLicense) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableLicense, error)
GetAll(context.Context, valuer.UUID) ([]*StorableLicense, error)
Update(context.Context, valuer.UUID, *StorableLicense) error
// feature surrogate
InitFeatures(context.Context, []*featuretypes.StorableFeature) error
CreateFeature(context.Context, *featuretypes.StorableFeature) error
GetFeature(context.Context, string) (*featuretypes.StorableFeature, error)
GetAllFeatures(context.Context) ([]*featuretypes.StorableFeature, error)
UpdateFeature(context.Context, *featuretypes.StorableFeature) error
// ListOrganizations returns the list of orgs
ListOrganizations(context.Context) ([]valuer.UUID, error)
}

View File

@ -0,0 +1,175 @@
package licensetypes
import (
"testing"
"time"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"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 *License
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":"0196f794-ff30-7bee-a5f4-ef5ad315715e"}`),
pass: false,
error: errors.New("key key is missing"),
},
{
name: "Error for invalid string license key",
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":10}`),
pass: false,
error: errors.New("key key is not a valid string"),
},
{
name: "Error for missing license status",
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e", "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":"0196f794-ff30-7bee-a5f4-ef5ad315715e","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":"0196f794-ff30-7bee-a5f4-ef5ad315715e","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":"0196f794-ff30-7bee-a5f4-ef5ad315715e","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":"0196f794-ff30-7bee-a5f4-ef5ad315715e","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":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`),
pass: true,
expected: &License{
ID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "ENTERPRISE",
},
"category": "FREE",
"status": "ACTIVE",
"valid_from": float64(1730899309),
"valid_until": float64(-1),
},
PlanName: PlanNameEnterprise,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "ACTIVE",
Features: make([]*featuretypes.GettableFeature, 0),
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
},
},
{
name: "Fallback to basic plan if license status is invalid",
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`),
pass: true,
expected: &License{
ID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "ENTERPRISE",
},
"category": "FREE",
"status": "INVALID",
"valid_from": float64(1730899309),
"valid_until": float64(-1),
},
PlanName: PlanNameBasic,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "INVALID",
Features: make([]*featuretypes.GettableFeature, 0),
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
},
},
{
name: "fallback states for validFrom and validUntil",
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from":1234.456,"valid_until":5678.567}`),
pass: true,
expected: &License{
ID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "ENTERPRISE",
},
"valid_from": 1234.456,
"valid_until": 5678.567,
"category": "FREE",
"status": "ACTIVE",
},
PlanName: PlanNameEnterprise,
ValidFrom: 1234,
ValidUntil: 5678,
Status: "ACTIVE",
Features: make([]*featuretypes.GettableFeature, 0),
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
LastValidatedAt: time.Time{},
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
},
},
}
for _, tc := range testCases {
license, err := NewLicense(tc.data, valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"))
if license != nil {
license.Features = make([]*featuretypes.GettableFeature, 0)
delete(license.Data, "features")
}
if tc.pass {
require.NoError(t, err)
require.NotNil(t, license)
// as the new license will pick the time.Now() value. doesn't make sense to compare them
license.CreatedAt = time.Time{}
license.UpdatedAt = time.Time{}
license.LastValidatedAt = time.Time{}
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,8 +1,6 @@
package model
package licensetypes
import (
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
)
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
const SSO = "SSO"
const Basic = "BASIC_PLAN"
@ -26,44 +24,44 @@ const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{
var BasicPlan = featuretypes.FeatureSet{
&featuretypes.GettableFeature{
Name: SSO,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.UseSpanMetrics,
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
&featuretypes.GettableFeature{
Name: Gateway,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
&featuretypes.GettableFeature{
Name: PremiumSupport,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AnomalyDetection,
&featuretypes.GettableFeature{
Name: featuretypes.AnomalyDetection,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.TraceFunnels,
&featuretypes.GettableFeature{
Name: featuretypes.TraceFunnels,
Active: false,
Usage: 0,
UsageLimit: -1,
@ -71,58 +69,68 @@ var BasicPlan = basemodel.FeatureSet{
},
}
var EnterprisePlan = basemodel.FeatureSet{
basemodel.Feature{
var EnterprisePlan = featuretypes.FeatureSet{
&featuretypes.GettableFeature{
Name: SSO,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.UseSpanMetrics,
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
&featuretypes.GettableFeature{
Name: Onboarding,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
&featuretypes.GettableFeature{
Name: ChatSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
&featuretypes.GettableFeature{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
&featuretypes.GettableFeature{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AnomalyDetection,
&featuretypes.GettableFeature{
Name: featuretypes.AnomalyDetection,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.TraceFunnels,
&featuretypes.GettableFeature{
Name: featuretypes.TraceFunnels,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var DefaultFeatureSet = featuretypes.FeatureSet{
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,

View File

@ -0,0 +1,33 @@
package licensetypes
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/errors"
)
type GettableSubscription struct {
RedirectURL string `json:"redirectURL"`
}
type PostableSubscription struct {
SuccessURL string `json:"url"`
}
func (p *PostableSubscription) UnmarshalJSON(data []byte) error {
var postableSubscription struct {
SuccessURL string `json:"url"`
}
err := json.Unmarshal(data, &postableSubscription)
if err != nil {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal payload")
}
if postableSubscription.SuccessURL == "" {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "success url cannot be empty")
}
p.SuccessURL = postableSubscription.SuccessURL
return nil
}

View File

@ -6,6 +6,7 @@ from testcontainers.core.container import Network
from wiremock.client import (
Mapping,
Mappings,
Requests,
)
from wiremock.constants import Config
from wiremock.testing.testcontainer import WireMockContainer
@ -78,3 +79,4 @@ def make_http_mocks():
yield _make_http_mocks
Mappings.delete_all_mappings()
Requests.reset_request_journal()

View File

@ -69,7 +69,7 @@ def test_apply_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None:
timeout=5,
)
assert response.json()["count"] >= 1
assert response.json()["count"] == 1
def test_refresh_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None:
@ -123,7 +123,7 @@ def test_refresh_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None
cursor = signoz.sqlstore.conn.cursor()
cursor.execute(
"SELECT data FROM licenses_v3 WHERE id='0196360e-90cd-7a74-8313-1aa815ce2a67'"
"SELECT data FROM license WHERE id='0196360e-90cd-7a74-8313-1aa815ce2a67'"
)
record = cursor.fetchone()[0]
assert json.loads(record)["valid_from"] == 1732146922
@ -134,7 +134,7 @@ def test_refresh_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None
timeout=5,
)
assert response.json()["count"] >= 1
assert response.json()["count"] == 1
def test_license_checkout(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None:
@ -172,7 +172,7 @@ def test_license_checkout(signoz: SigNoz, make_http_mocks, get_jwt_token) -> Non
timeout=5,
)
assert response.status_code == http.HTTPStatus.OK
assert response.status_code == http.HTTPStatus.CREATED
assert response.json()["data"]["redirectURL"] == "https://signoz.checkout.com"
response = requests.post(
@ -219,7 +219,7 @@ def test_license_portal(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None:
timeout=5,
)
assert response.status_code == http.HTTPStatus.OK
assert response.status_code == http.HTTPStatus.CREATED
assert response.json()["data"]["redirectURL"] == "https://signoz.portal.com"
response = requests.post(