mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 06:49:00 +08:00
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:
parent
7feb94e5eb
commit
b1c78c2f12
26
ee/licensing/config.go
Normal file
26
ee/licensing/config.go
Normal 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
|
||||
}
|
168
ee/licensing/httplicensing/api.go
Normal file
168
ee/licensing/httplicensing/api.go
Normal 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)
|
||||
}
|
285
ee/licensing/httplicensing/provider.go
Normal file
285
ee/licensing/httplicensing/provider.go
Normal 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,
|
||||
})
|
||||
}
|
186
ee/licensing/licensingstore/sqllicensingstore/store.go
Normal file
186
ee/licensing/licensingstore/sqllicensingstore/store.go
Normal 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
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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..." />;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
28
frontend/src/api/v1/checkout/create.ts
Normal file
28
frontend/src/api/v1/checkout/create.ts
Normal 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;
|
28
frontend/src/api/v1/portal/create.ts
Normal file
28
frontend/src/api/v1/portal/create.ts
Normal 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;
|
25
frontend/src/api/v3/licenses/active/get.ts
Normal file
25
frontend/src/api/v3/licenses/active/get.ts
Normal 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;
|
24
frontend/src/api/v3/licenses/put.ts
Normal file
24
frontend/src/api/v3/licenses/put.ts
Normal 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;
|
@ -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(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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;
|
@ -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} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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,
|
||||
]);
|
||||
|
@ -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,
|
||||
]);
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
export const LICENSE_PLAN_KEY = {
|
||||
ENTERPRISE_PLAN: 'ENTERPRISE_PLAN',
|
||||
BASIC_PLAN: 'BASIC_PLAN',
|
||||
};
|
||||
|
||||
export const LICENSE_PLAN_STATUS = {
|
||||
VALID: 'VALID',
|
||||
};
|
@ -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 };
|
@ -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;
|
@ -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(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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 />
|
||||
) : (
|
||||
<>
|
||||
|
@ -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>{' '}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -5,3 +5,8 @@ export interface CheckoutSuccessPayloadProps {
|
||||
export interface CheckoutRequestPayloadProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
data: CheckoutSuccessPayloadProps;
|
||||
status: string;
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { License } from './def';
|
||||
|
||||
export type PayloadProps = {
|
||||
licenses: License[];
|
||||
};
|
@ -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
18
pkg/licensing/config.go
Normal 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
|
||||
}
|
55
pkg/licensing/licensing.go
Normal file
55
pkg/licensing/licensing.go
Normal 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)
|
||||
}
|
35
pkg/licensing/nooplicensing/api.go
Normal file
35
pkg/licensing/nooplicensing/api.go
Normal 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"))
|
||||
}
|
99
pkg/licensing/nooplicensing/provider.go
Normal file
99
pkg/licensing/nooplicensing/provider.go
Normal 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")
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
}
|
@ -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
|
||||
}
|
@ -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(),
|
||||
|
@ -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: "",
|
||||
},
|
||||
}
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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{
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
|
149
pkg/sqlmigration/034_update_license.go
Normal file
149
pkg/sqlmigration/034_update_license.go
Normal 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
|
||||
}
|
28
pkg/types/featuretypes/feature.go
Normal file
28
pkg/types/featuretypes/feature.go
Normal 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"
|
@ -85,3 +85,7 @@ type PostableInvite struct {
|
||||
type PostableBulkInviteRequest struct {
|
||||
Invites []PostableInvite `json:"invites"`
|
||||
}
|
||||
|
||||
type GettableCreateInviteResponse struct {
|
||||
InviteToken string `json:"token"`
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
389
pkg/types/licensetypes/license.go
Normal file
389
pkg/types/licensetypes/license.go
Normal 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)
|
||||
}
|
175
pkg/types/licensetypes/license_test.go
Normal file
175
pkg/types/licensetypes/license_test.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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,
|
33
pkg/types/licensetypes/subscription.go
Normal file
33
pkg/types/licensetypes/subscription.go
Normal 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
|
||||
}
|
@ -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()
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user