- {isFetchingActiveLicenseV3 || !activeLicenseV3 ? (
+ {isFetchingActiveLicense || !activeLicense ? (
) : (
<>
@@ -115,7 +117,7 @@ function WorkspaceSuspended(): JSX.Element {
{t('yourDataIsSafe')}{' '}
{getFormattedDateWithMinutes(
- dayjs(activeLicenseV3?.event_queue?.scheduled_at).unix() ||
+ dayjs(activeLicense?.event_queue?.scheduled_at).unix() ||
Date.now(),
)}
{' '}
diff --git a/frontend/src/providers/App/App.tsx b/frontend/src/providers/App/App.tsx
index 58d4c682f7..8f84ee0184 100644
--- a/frontend/src/providers/App/App.tsx
+++ b/frontend/src/providers/App/App.tsx
@@ -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(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(() => getUserDefaults());
- const [licenses, setLicenses] = useState(null);
- const [
- activeLicenseV3,
- setActiveLicenseV3,
- ] = useState(null);
-
+ const [activeLicense, setActiveLicense] = useState(
+ null,
+ );
const [trialInfo, setTrialInfo] = useState(null);
-
const [featureFlags, setFeatureFlags] = useState(null);
const [orgPreferences, setOrgPreferences] = useState(
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,
diff --git a/frontend/src/providers/App/types.ts b/frontend/src/providers/App/types.ts
index 8c9d2117dc..4dd5b31bf9 100644
--- a/frontend/src/providers/App/types.ts
+++ b/frontend/src/providers/App/types.ts
@@ -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;
diff --git a/frontend/src/tests/test-utils.tsx b/frontend/src/tests/test-utils.tsx
index 71f253afe5..cdbe9bdd6d 100644
--- a/frontend/src/tests/test-utils.tsx
+++ b/frontend/src/tests/test-utils.tsx
@@ -105,7 +105,8 @@ export function getAppContextMock(
appContextOverrides?: Partial,
): 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,
};
}
diff --git a/frontend/src/types/api/billing/checkout.ts b/frontend/src/types/api/billing/checkout.ts
index 78523376f0..4b1a2311ca 100644
--- a/frontend/src/types/api/billing/checkout.ts
+++ b/frontend/src/types/api/billing/checkout.ts
@@ -5,3 +5,8 @@ export interface CheckoutSuccessPayloadProps {
export interface CheckoutRequestPayloadProps {
url: string;
}
+
+export interface PayloadProps {
+ data: CheckoutSuccessPayloadProps;
+ status: string;
+}
diff --git a/frontend/src/types/api/licenses/getAll.ts b/frontend/src/types/api/licenses/getAll.ts
deleted file mode 100644
index 58996cf36e..0000000000
--- a/frontend/src/types/api/licenses/getAll.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { License } from './def';
-
-export type PayloadProps = {
- licenses: License[];
-};
diff --git a/frontend/src/types/api/licensesV3/getActive.ts b/frontend/src/types/api/licensesV3/getActive.ts
index b073438bad..a26d766064 100644
--- a/frontend/src/types/api/licensesV3/getActive.ts
+++ b/frontend/src/types/api/licensesV3/getActive.ts
@@ -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;
+}
diff --git a/pkg/licensing/config.go b/pkg/licensing/config.go
new file mode 100644
index 0000000000..a88480d867
--- /dev/null
+++ b/pkg/licensing/config.go
@@ -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
+}
diff --git a/pkg/licensing/licensing.go b/pkg/licensing/licensing.go
new file mode 100644
index 0000000000..0e0196650f
--- /dev/null
+++ b/pkg/licensing/licensing.go
@@ -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)
+}
diff --git a/pkg/licensing/nooplicensing/api.go b/pkg/licensing/nooplicensing/api.go
new file mode 100644
index 0000000000..e484376fd5
--- /dev/null
+++ b/pkg/licensing/nooplicensing/api.go
@@ -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"))
+}
diff --git a/pkg/licensing/nooplicensing/provider.go b/pkg/licensing/nooplicensing/provider.go
new file mode 100644
index 0000000000..0e509615f2
--- /dev/null
+++ b/pkg/licensing/nooplicensing/provider.go
@@ -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")
+}
diff --git a/pkg/modules/user/impluser/handler.go b/pkg/modules/user/impluser/handler.go
index cfc867d87a..66d43e2b1d 100644
--- a/pkg/modules/user/impluser/handler.go
+++ b/pkg/modules/user/impluser/handler.go
@@ -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) {
diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go
index 15a1882e1c..4caad03633 100644
--- a/pkg/query-service/app/http_handler.go
+++ b/pkg/query-service/app/http_handler.go
@@ -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
}
diff --git a/pkg/query-service/app/queryBuilder/query_builder.go b/pkg/query-service/app/queryBuilder/query_builder.go
index f49a046937..2a9aa2a5e5 100644
--- a/pkg/query-service/app/queryBuilder/query_builder.go
+++ b/pkg/query-service/app/queryBuilder/query_builder.go
@@ -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 {
diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go
index 0ccadb1e8c..08a26c8799 100644
--- a/pkg/query-service/app/server.go
+++ b/pkg/query-service/app/server.go
@@ -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,
diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go
index 21850a53ac..d1446bbe12 100644
--- a/pkg/query-service/constants/constants.go
+++ b/pkg/query-service/constants/constants.go
@@ -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,
diff --git a/pkg/query-service/featureManager/manager.go b/pkg/query-service/featureManager/manager.go
deleted file mode 100644
index 7805fe6191..0000000000
--- a/pkg/query-service/featureManager/manager.go
+++ /dev/null
@@ -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}
-}
diff --git a/pkg/query-service/interfaces/featureLookup.go b/pkg/query-service/interfaces/featureLookup.go
deleted file mode 100644
index e2ecbcc3bb..0000000000
--- a/pkg/query-service/interfaces/featureLookup.go
+++ /dev/null
@@ -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
-}
diff --git a/pkg/query-service/main.go b/pkg/query-service/main.go
index 7c4ba52dbb..89644b8518 100644
--- a/pkg/query-service/main.go
+++ b/pkg/query-service/main.go
@@ -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(),
diff --git a/pkg/query-service/model/featureSet.go b/pkg/query-service/model/featureSet.go
deleted file mode 100644
index 4646d030f6..0000000000
--- a/pkg/query-service/model/featureSet.go
+++ /dev/null
@@ -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: "",
- },
-}
diff --git a/pkg/query-service/tests/integration/filter_suggestions_test.go b/pkg/query-service/tests/integration/filter_suggestions_test.go
index 781e453cdf..b0e02b1ec4 100644
--- a/pkg/query-service/tests/integration/filter_suggestions_test.go
+++ b/pkg/query-service/tests/integration/filter_suggestions_test.go
@@ -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),
diff --git a/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go b/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go
index 0d3a93ac1d..76ea8f590f 100644
--- a/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go
+++ b/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go
@@ -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,
diff --git a/pkg/query-service/tests/integration/signoz_integrations_test.go b/pkg/query-service/tests/integration/signoz_integrations_test.go
index 4111d6df42..8751ef5c2a 100644
--- a/pkg/query-service/tests/integration/signoz_integrations_test.go
+++ b/pkg/query-service/tests/integration/signoz_integrations_test.go
@@ -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{
diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go
index a1aed180c9..d5363edd68 100644
--- a/pkg/signoz/provider.go
+++ b/pkg/signoz/provider.go
@@ -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),
)
}
diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go
index d72a1c18cd..555b03a4d3 100644
--- a/pkg/signoz/signoz.go
+++ b/pkg/signoz/signoz.go
@@ -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,
diff --git a/pkg/sqlmigration/034_update_license.go b/pkg/sqlmigration/034_update_license.go
new file mode 100644
index 0000000000..0be8ab82bd
--- /dev/null
+++ b/pkg/sqlmigration/034_update_license.go
@@ -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
+}
diff --git a/pkg/types/featuretypes/feature.go b/pkg/types/featuretypes/feature.go
new file mode 100644
index 0000000000..964cd4a15c
--- /dev/null
+++ b/pkg/types/featuretypes/feature.go
@@ -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"
diff --git a/pkg/types/invite.go b/pkg/types/invite.go
index 40fea73d27..8de9ede349 100644
--- a/pkg/types/invite.go
+++ b/pkg/types/invite.go
@@ -85,3 +85,7 @@ type PostableInvite struct {
type PostableBulkInviteRequest struct {
Invites []PostableInvite `json:"invites"`
}
+
+type GettableCreateInviteResponse struct {
+ InviteToken string `json:"token"`
+}
diff --git a/pkg/types/license.go b/pkg/types/license.go
deleted file mode 100644
index fb9fd7e6d0..0000000000
--- a/pkg/types/license.go
+++ /dev/null
@@ -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"`
-}
diff --git a/pkg/types/licensetypes/license.go b/pkg/types/licensetypes/license.go
new file mode 100644
index 0000000000..994b2c7b63
--- /dev/null
+++ b/pkg/types/licensetypes/license.go
@@ -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)
+}
diff --git a/pkg/types/licensetypes/license_test.go b/pkg/types/licensetypes/license_test.go
new file mode 100644
index 0000000000..b2216accbb
--- /dev/null
+++ b/pkg/types/licensetypes/license_test.go
@@ -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)
+ }
+
+ }
+}
diff --git a/ee/query-service/model/plans.go b/pkg/types/licensetypes/plan.go
similarity index 61%
rename from ee/query-service/model/plans.go
rename to pkg/types/licensetypes/plan.go
index 2de2e7ccb8..4521018084 100644
--- a/ee/query-service/model/plans.go
+++ b/pkg/types/licensetypes/plan.go
@@ -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,
diff --git a/pkg/types/licensetypes/subscription.go b/pkg/types/licensetypes/subscription.go
new file mode 100644
index 0000000000..9065a8a465
--- /dev/null
+++ b/pkg/types/licensetypes/subscription.go
@@ -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
+}
diff --git a/tests/integration/fixtures/http.py b/tests/integration/fixtures/http.py
index fd8f798df4..2df0f1cc30 100644
--- a/tests/integration/fixtures/http.py
+++ b/tests/integration/fixtures/http.py
@@ -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()
diff --git a/tests/integration/src/bootstrap/c_license.py b/tests/integration/src/bootstrap/c_license.py
index 5a2647610f..6ed524d1d8 100644
--- a/tests/integration/src/bootstrap/c_license.py
+++ b/tests/integration/src/bootstrap/c_license.py
@@ -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(