Merge branch 'main' into chore/trace-funnels-bugfixes/improvements

This commit is contained in:
Shaheer Kochai 2025-06-03 19:05:51 +04:30 committed by GitHub
commit aad892eee4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 549 additions and 480 deletions

2
.github/CODEOWNERS vendored
View File

@ -11,5 +11,5 @@
/pkg/errors/ @grandwizard28
/pkg/factory/ @grandwizard28
/pkg/types/ @grandwizard28
/pkg/sqlmigration/ @vikrantgupta25
.golangci.yml @grandwizard28
**/(zeus|licensing|sqlmigration)/ @vikrantgupta25

View File

@ -8,7 +8,7 @@
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
<p align="center">
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Docker Downloads"> </a>
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/signoz.svg?label=Docker%20Downloads"> </a>
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>

View File

@ -5,15 +5,12 @@ import (
"encoding/json"
"time"
"github.com/SigNoz/signoz/ee/query-service/constants"
"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/modules/organization"
"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"
@ -89,13 +86,6 @@ func (provider *provider) Validate(ctx context.Context) error {
}
}
if len(organizations) == 0 {
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
}
return nil
}
@ -116,11 +106,6 @@ func (provider *provider) Activate(ctx context.Context, organizationID valuer.UU
return err
}
err = provider.InitFeatures(ctx, license.Features)
if err != nil {
return err
}
return nil
}
@ -140,28 +125,24 @@ func (provider *provider) GetActive(ctx context.Context, organizationID valuer.U
func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error {
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil
}
provider.settings.Logger().ErrorContext(ctx, "license validation failed", "org_id", 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", "org_id", 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 {
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", "failure_threshold", provider.config.FailureThreshold, "license_id", activeLicense.ID.StringValue(), "org_id", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
if err != nil {
return err
}
return nil
}
return err
@ -178,11 +159,6 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
err = provider.InitFeatures(ctx, activeLicense.Features)
if err != nil {
return err
}
return nil
}
@ -224,80 +200,14 @@ func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID
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)
func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
license, err := provider.GetActive(ctx, organizationID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return licensetypes.BasicPlan, 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,
}
}
if constants.IsDotMetricsEnabled {
gettableFeatures = append(gettableFeatures, &featuretypes.GettableFeature{
Name: featuretypes.DotMetricsEnabled,
Active: true,
})
}
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,
})
return license.Features, nil
}

View File

@ -5,7 +5,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"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"
)
@ -80,81 +79,3 @@ func (store *store) Update(ctx context.Context, organizationID valuer.UUID, stor
return nil
}
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(storableFeature).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "feature with name:%s already exists", storableFeature.Name)
}
return nil
}
func (store *store) GetFeature(ctx context.Context, key string) (*featuretypes.StorableFeature, error) {
storableFeature := new(featuretypes.StorableFeature)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storableFeature).
Where("name = ?", key).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "feature with name:%s does not exist", key)
}
return storableFeature, nil
}
func (store *store) GetAllFeatures(ctx context.Context) ([]*featuretypes.StorableFeature, error) {
storableFeatures := make([]*featuretypes.StorableFeature, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&storableFeatures).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "features do not exist")
}
return storableFeatures, nil
}
func (store *store) InitFeatures(ctx context.Context, storableFeatures []*featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(&storableFeatures).
On("CONFLICT (name) DO UPDATE").
Set("active = EXCLUDED.active").
Set("usage = EXCLUDED.usage").
Set("usage_limit = EXCLUDED.usage_limit").
Set("route = EXCLUDED.route").
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to initialise features")
}
return nil
}
func (store *store) UpdateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(storableFeature).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update feature with key: %s", storableFeature.Name)
}
return nil
}

View File

@ -1,7 +1,6 @@
package api
import (
"context"
"net/http"
"net/http/httputil"
"time"
@ -86,17 +85,12 @@ func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
return ah.opts.Gateway
}
func (ah *APIHandler) CheckFeature(ctx context.Context, key string) bool {
err := ah.Signoz.Licensing.CheckFeature(ctx, key)
return err == nil
}
// RegisterRoutes registers routes for this handler on the given router
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first
// routes available only in ee version
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
// paid plans specific routes
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)

View File

@ -12,7 +12,7 @@ import (
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/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
@ -31,7 +31,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
return
}
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context())
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context(), orgID)
if err != nil {
ah.HandleError(w, err, http.StatusInternalServerError)
return
@ -61,7 +61,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
if ah.opts.PreferSpanMetrics {
for idx, feature := range featureSet {
if feature.Name == featuretypes.UseSpanMetrics {
if feature.Name == licensetypes.UseSpanMetrics {
featureSet[idx].Active = true
}
}
}
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {
featureSet[idx].Active = true
}
}
@ -72,7 +80,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) ([]*featuretypes.GettableFeature, error) {
func fetchZeusFeatures(url, licenseKey string) ([]*licensetypes.Feature, error) {
// Check if the URL is empty
if url == "" {
return nil, fmt.Errorf("url is empty")
@ -131,28 +139,28 @@ func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature,
}
type ZeusFeaturesResponse struct {
Status string `json:"status"`
Data []*featuretypes.GettableFeature `json:"data"`
Status string `json:"status"`
Data []*licensetypes.Feature `json:"data"`
}
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
func MergeFeatureSets(zeusFeatures, internalFeatures []*featuretypes.GettableFeature) []*featuretypes.GettableFeature {
func MergeFeatureSets(zeusFeatures, internalFeatures []*licensetypes.Feature) []*licensetypes.Feature {
// Create a map to store the merged features
featureMap := make(map[string]*featuretypes.GettableFeature)
featureMap := make(map[string]*licensetypes.Feature)
// Add all features from the otherFeatures set to the map
for _, feature := range internalFeatures {
featureMap[feature.Name] = feature
featureMap[feature.Name.StringValue()] = feature
}
// Add all features from the zeusFeatures set to the map
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
for _, feature := range zeusFeatures {
featureMap[feature.Name] = feature
featureMap[feature.Name.StringValue()] = feature
}
// Convert the map back to a FeatureSet slice
var mergedFeatures []*featuretypes.GettableFeature
var mergedFeatures []*licensetypes.Feature
for _, feature := range featureMap {
mergedFeatures = append(mergedFeatures, feature)
}

View File

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

View File

@ -1,10 +0,0 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
const getFeaturesFlags = (): Promise<FeatureFlagProps[]> =>
axios
.get<ApiResponse<FeatureFlagProps[]>>(`/featureFlags`)
.then((response) => response.data.data);
export default getFeaturesFlags;

View File

@ -167,8 +167,8 @@ interface UpdateFunnelDescriptionPayload {
export const saveFunnelDescription = async (
payload: UpdateFunnelDescriptionPayload,
): Promise<SuccessResponse<FunnelData> | ErrorResponse> => {
const response: AxiosResponse = await axios.post(
`${FUNNELS_BASE_PATH}/save`,
const response: AxiosResponse = await axios.put(
`${FUNNELS_BASE_PATH}/${payload.funnel_id}`,
payload,
);

View File

@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
FeatureFlagProps,
PayloadProps,
} from 'types/api/features/getFeaturesFlags';
const list = async (): Promise<SuccessResponseV2<FeatureFlagProps[]>> => {
try {
const response = await axios.get<PayloadProps>(`/features`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default list;

View File

@ -1,14 +1,12 @@
// keep this consistent with backend constants.go
// keep this consistent with backend plan.go
export enum FeatureKeys {
SSO = 'SSO',
USE_SPAN_METRICS = 'USE_SPAN_METRICS',
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
ONBOARDING_V3 = 'ONBOARDING_V3',
THIRD_PARTY_API = 'THIRD_PARTY_API',
TRACE_FUNNELS = 'TRACE_FUNNELS',
DOT_METRICS_ENABLED = 'DOT_METRICS_ENABLED',
SSO = 'sso',
USE_SPAN_METRICS = 'use_span_metrics',
ONBOARDING = 'onboarding',
CHAT_SUPPORT = 'chat_support',
GATEWAY = 'gateway',
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
ONBOARDING_V3 = 'onboarding_v3',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
}

View File

@ -316,7 +316,6 @@ describe('Create Alert Channel (Normal User)', () => {
expect(screen.getByText('Microsoft Teams')).toBeInTheDocument();
});
// TODO[vikrantgupta25]: check with Shaheer
it.skip('Should check if the upgrade plan message is shown', () => {
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
expect(

View File

@ -36,7 +36,6 @@ function QuerySection({
const { t } = useTranslation('alerts');
const [currentTab, setCurrentTab] = useState(queryCategory);
// TODO[vikrantgupta25] : check if this is still required ??
const handleQueryCategoryChange = (queryType: string): void => {
setQueryCategory(queryType as EQueryType);
setCurrentTab(queryType as EQueryType);

View File

@ -160,8 +160,6 @@ function GridCardGraph({
};
});
// TODO [vikrantgupta25] remove this useEffect with refactor as this is prone to race condition
// this is added to tackle the case of async communication between VariableItem.tsx and GridCard.tsx
useEffect(() => {
if (variablesToGetUpdated.length > 0) {
queryClient.cancelQueries([

View File

@ -41,7 +41,6 @@ const { Search } = Input;
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const { t } = useTranslation('common');
const { user } = useAppContext();
// TODO[vikrantgupta25]: check with sagar on cleanup
const [addNewAlert, action] = useComponentPermission(
['add_new_alert', 'action'],
user.role,

View File

@ -62,6 +62,8 @@ function LogsExplorerChart({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
urlQuery.delete(QueryParams.relativeTime);
// Remove Hidden Filters from URL query parameters on time change
urlQuery.delete(QueryParams.activeLogId);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
},

View File

@ -188,6 +188,26 @@ function LogsExplorerViews({
},
],
legend: '{{severity_text}}',
...(activeLogId && {
filters: {
...listQuery?.filters,
items: [
...(listQuery?.filters?.items || []),
{
id: v4(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['<='],
value: activeLogId,
},
],
op: 'AND',
},
}),
};
const modifiedQuery: Query = {
@ -202,7 +222,7 @@ function LogsExplorerViews({
};
return modifiedQuery;
}, [stagedQuery, listQuery]);
}, [stagedQuery, listQuery, activeLogId]);
const exportDefaultQuery = useMemo(
() =>
@ -287,12 +307,12 @@ function LogsExplorerViews({
});
// Add filter for activeLogId if present
let updatedFilters = paginateData.filters;
let updatedFilters = params.filters;
if (activeLogId) {
updatedFilters = {
...paginateData.filters,
...params.filters,
items: [
...(paginateData.filters?.items || []),
...(params.filters?.items || []),
{
id: v4(),
key: {

View File

@ -1,17 +1,23 @@
import ROUTES from 'constants/routes';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { VirtuosoMockContext } from 'react-virtuoso';
import { fireEvent, render, RenderResult } from 'tests/test-utils';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import LogsExplorerViews from '..';
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
import {
logsQueryRangeSuccessNewFormatResponse,
mockQueryBuilderContextValue,
} from './mock';
const queryRangeURL = 'http://localhost/api/v3/query_range';
const ACTIVE_LOG_ID = 'test-log-id';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
@ -81,6 +87,12 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('hooks/logs/useCopyLogLink', () => ({
useCopyLogLink: jest.fn().mockReturnValue({
activeLogId: ACTIVE_LOG_ID,
}),
}));
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
beforeEach(() => {
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
@ -162,4 +174,47 @@ describe('LogsExplorerViews -', () => {
queryByText('Something went wrong. Please try again or contact support.'),
).toBeInTheDocument();
});
it('should add activeLogId filter when present in URL', () => {
// Mock useCopyLogLink to return an activeLogId
(useCopyLogLink as jest.Mock).mockReturnValue({
activeLogId: ACTIVE_LOG_ID,
});
lodsQueryServerRequest();
render(
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</QueryBuilderContext.Provider>,
);
// Get the query data from the first call to useGetExplorerQueryRange
const {
queryData,
} = (useGetExplorerQueryRange as jest.Mock).mock.calls[0][0].builder;
const firstQuery = queryData[0];
// Get the original number of filters from mock data
const originalFiltersLength =
mockQueryBuilderContextValue.currentQuery.builder.queryData[0].filters?.items
.length || 0;
const expectedFiltersLength = originalFiltersLength + 1; // +1 for activeLogId filter
// Verify that the activeLogId filter is present
expect(
firstQuery.filters?.items.some(
(item: TagFilterItem) =>
item.key?.key === 'id' && item.op === '<=' && item.value === ACTIVE_LOG_ID,
),
).toBe(true);
// Verify the total number of filters (original + 1 new activeLogId filter)
expect(firstQuery.filters?.items.length).toBe(expectedFiltersLength);
});
});

View File

@ -1,3 +1,13 @@
import {
initialQueriesMap,
initialQueryBuilderFormValues,
OPERATORS,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { noop } from 'lodash-es';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
export const logsQueryRangeSuccessNewFormatResponse = {
data: {
result: [],
@ -49,3 +59,148 @@ export const logsQueryRangeSuccessNewFormatResponse = {
},
},
};
export const mockQueryBuilderContextValue = {
isDefaultQuery: (): boolean => false,
currentQuery: {
...initialQueriesMap.logs,
builder: {
...initialQueriesMap.logs.builder,
queryData: [
{
...initialQueryBuilderFormValues,
filters: {
items: [
{
id: '1',
key: {
key: 'service',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['='],
value: 'frontend',
},
{
id: '2',
key: {
key: 'log_level',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['='],
value: 'INFO',
},
],
op: 'AND',
},
},
initialQueryBuilderFormValues,
],
},
},
setSupersetQuery: jest.fn(),
supersetQuery: {
...initialQueriesMap.logs,
builder: {
...initialQueriesMap.logs.builder,
queryData: [
{
...initialQueryBuilderFormValues,
filters: {
items: [
{
id: '1',
key: {
key: 'service',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['='],
value: 'frontend',
},
{
id: '2',
key: {
key: 'log_level',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['='],
value: 'INFO',
},
],
op: 'AND',
},
},
initialQueryBuilderFormValues,
],
},
},
stagedQuery: {
...initialQueriesMap.logs,
builder: {
...initialQueriesMap.logs.builder,
queryData: [
{
...initialQueryBuilderFormValues,
filters: {
items: [
{
id: '1',
key: {
key: 'service',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['='],
value: 'frontend',
},
{
id: '2',
key: {
key: 'log_level',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['='],
value: 'INFO',
},
],
op: 'AND',
},
},
initialQueryBuilderFormValues,
],
},
},
initialDataSource: null,
panelType: PANEL_TYPES.TIME_SERIES,
isEnabledQuery: false,
lastUsedQuery: 0,
setLastUsedQuery: noop,
handleSetQueryData: noop,
handleSetFormulaData: noop,
handleSetQueryItemData: noop,
handleSetConfig: noop,
removeQueryBuilderEntityByIndex: noop,
removeQueryTypeItemByIndex: noop,
addNewBuilderQuery: noop,
cloneQuery: noop,
addNewFormula: noop,
addNewQueryItem: noop,
redirectWithQueryBuilderData: noop,
handleRunQuery: noop,
resetQuery: noop,
updateAllQueriesOperators: (): Query => initialQueriesMap.logs,
updateQueriesData: (): Query => initialQueriesMap.logs,
initQueryBuilderData: noop,
handleOnUnitsChange: noop,
isStagedQueryUpdated: (): boolean => false,
};

View File

@ -377,6 +377,8 @@ function DateTimeSelection({
urlQuery.delete('endTime');
urlQuery.set(QueryParams.relativeTime, value);
// Remove Hidden Filters from URL query parameters on time change
urlQuery.delete(QueryParams.activeLogId);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
@ -669,9 +671,7 @@ function DateTimeSelection({
urlQuery.set(QueryParams.endTime, endTime);
urlQuery.delete(QueryParams.relativeTime);
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname, updateTimeInterval, globalTimeLoading]);

View File

@ -142,6 +142,7 @@ export const useValidateFunnelSteps = ({
interface SaveFunnelDescriptionPayload {
funnel_id: string;
description: string;
timestamp: number;
}
export const useSaveFunnelDescription = (): UseMutationResult<
@ -149,7 +150,11 @@ export const useSaveFunnelDescription = (): UseMutationResult<
Error,
SaveFunnelDescriptionPayload
> =>
useMutation({
useMutation<
SuccessResponse<FunnelData> | ErrorResponse,
Error,
SaveFunnelDescriptionPayload
>({
mutationFn: saveFunnelDescription,
});

View File

@ -1,18 +1,29 @@
import getFeaturesFlags from 'api/features/getFeatureFlags';
import list from 'api/v1/features/list';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
const useGetFeatureFlag = (
export interface Props {
onSuccessHandler: (routes: FeatureFlagProps[]) => void;
isLoggedIn: boolean;
}
type UseGetFeatureFlag = UseQueryResult<
SuccessResponseV2<FeatureFlagProps[]>,
APIError
>;
export const useGetFeatureFlag = (
onSuccessHandler: (routes: FeatureFlagProps[]) => void,
isLoggedIn: boolean,
): UseQueryResult<FeatureFlagProps[], unknown> =>
useQuery<FeatureFlagProps[]>({
queryFn: getFeaturesFlags,
): UseGetFeatureFlag =>
useQuery<SuccessResponseV2<FeatureFlagProps[]>, APIError>({
queryKey: [REACT_QUERY_KEY.GET_FEATURES_FLAGS],
onSuccess: onSuccessHandler,
queryFn: () => list(),
onSuccess: (data) => {
onSuccessHandler(data.data);
},
retryOnMount: false,
enabled: !!isLoggedIn,
});
export default useGetFeatureFlag;

View File

@ -145,14 +145,6 @@ describe('Logs Explorer Tests', () => {
await waitFor(() =>
expect(queryByTestId('logs-list-virtuoso')).toBeInTheDocument(),
);
// check for data being present in the UI
// todo[@vikrantgupta25]: skipping this for now as the formatting matching is not picking up in the CI will debug later.
// expect(
// queryByText(
// `2024-02-16 02:50:22.000 | 2024-02-15T21:20:22.035Z INFO frontend Dispatch successful {"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}`,
// ),
// ).toBeInTheDocument();
});
test('Multiple Current Queries', async () => {

View File

@ -41,6 +41,7 @@ function AddFunnelDescriptionModal({
{
funnel_id: funnelId,
description,
timestamp: Date.now(),
},
{
onSuccess: () => {

View File

@ -5,7 +5,7 @@ import getUserVersion from 'api/v1/version/getVersion';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs from 'dayjs';
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
import { useGetFeatureFlag } from 'hooks/useGetFeatureFlag';
import { useGlobalEventListener } from 'hooks/useGlobalEventListener';
import useGetUser from 'hooks/user/useGetUser';
import {

View File

@ -832,6 +832,8 @@ export function QueryBuilderProvider({
),
);
}
// Remove Hidden Filters from URL query parameters on query change
urlQuery.delete(QueryParams.activeLogId);
const generatedUrl = redirectingUrl
? `${redirectingUrl}?${urlQuery}`

View File

@ -8,4 +8,7 @@ export interface FeatureFlagProps {
route: string;
}
export type PayloadProps = FeatureFlagProps[];
export interface PayloadProps {
data: FeatureFlagProps[];
status: string;
}

View File

@ -25,12 +25,6 @@ export enum LicensePlatform {
CLOUD = 'CLOUD',
}
// Legacy
export const LicensePlanKey = {
ENTERPRISE: 'ENTERPRISE',
BASIC: 'BASIC',
};
export type LicenseEventQueueResModel = {
event: LicenseEvent;
status: string;

View File

@ -6,7 +6,6 @@ import (
"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"
)
@ -31,18 +30,8 @@ type Licensing interface {
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
GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error)
}
type API interface {

View File

@ -6,7 +6,6 @@ import (
"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"
)
@ -60,40 +59,6 @@ func (provider *noopLicensing) GetActive(ctx context.Context, organizationID val
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) {
func (provider *noopLicensing) GetFeatureFlags(_ context.Context, _ valuer.UUID) ([]*licensetypes.Feature, error) {
return licensetypes.DefaultFeatureSet, nil
}
func (provider *noopLicensing) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "init features is not supported")
}
func (provider *noopLicensing) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "updating feature flag is not supported")
}

View File

@ -62,7 +62,7 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
@ -550,7 +550,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v2/traces/waterfall/{traceId}", am.ViewAccess(aH.GetWaterfallSpansForTraceWithMetadata)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/features", am.ViewAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/health", am.OpenAccess(aH.getHealth)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/listErrors", am.ViewAccess(aH.listErrors)).Methods(http.MethodPost)
@ -1931,32 +1931,29 @@ func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
featureSet, err := aH.Signoz.Licensing.GetFeatureFlags(r.Context())
featureSet, err := aH.Signoz.Licensing.GetFeatureFlags(r.Context(), valuer.GenerateUUID())
if err != nil {
aH.HandleError(w, err, http.StatusInternalServerError)
return
}
if aH.preferSpanMetrics {
for idx, feature := range featureSet {
if feature.Name == featuretypes.UseSpanMetrics {
if feature.Name == licensetypes.UseSpanMetrics {
featureSet[idx].Active = true
}
}
}
if constants.IsDotMetricsEnabled {
featureSet = append(featureSet, &featuretypes.GettableFeature{
Name: featuretypes.DotMetricsEnabled,
Active: true,
})
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {
featureSet[idx].Active = true
}
}
}
aH.Respond(w, featureSet)
}
func (aH *APIHandler) CheckFeature(ctx context.Context, key string) bool {
err := aH.Signoz.Licensing.CheckFeature(ctx, key)
return err == nil
}
// getHealth is used to check the health of the service.
// 'live' query param can be used to check liveliness of
// the service by checking the database connection.

View File

@ -186,6 +186,14 @@ func HydrateFileUris(spec interface{}, fs embed.FS, basedir string) (interface{}
if strings.HasPrefix(dashboardUri, "file://") {
dashboards[i] = strings.Replace(dashboardUri, ".json", "_dot.json", 1)
}
} else if dashBoardMap, ok := dashboard.(map[string]interface{}); ok {
if dashboardUri, ok := dashBoardMap["definition"].(string); ok {
if strings.HasPrefix(dashboardUri, "file://") {
dashboardUri = strings.Replace(dashboardUri, ".json", "_dot.json", 1)
}
dashBoardMap["definition"] = dashboardUri
}
dashboards[i] = dashBoardMap
}
}
v = dashboards

View File

@ -124,7 +124,7 @@
"multiSelect": false,
"name": "host.name",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'host.name') AS host.name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'ClickHouseMetrics_VersionInteger' and __normalized=false \nGROUP BY host.name",
"queryValue": "SELECT JSONExtractString(labels, 'host.name') AS `host.name`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'ClickHouseMetrics_VersionInteger' and __normalized=false \nGROUP BY `host.name`",
"selectedValue": "",
"showALLOption": false,
"sort": "DISABLED",

View File

@ -9,7 +9,6 @@ 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 (
@ -66,16 +65,6 @@ func UseMetricsPreAggregation() bool {
var KafkaSpanEval = GetOrDefaultEnv("KAFKA_SPAN_EVAL", "false")
var DEFAULT_FEATURE_SET = []*featuretypes.GettableFeature{
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
func GetEvalDelay() time.Duration {
evalDelayStr := GetOrDefaultEnv("RULES_EVAL_DELAY", "2m")
evalDelayDuration, err := time.ParseDuration(evalDelayStr)

View File

@ -90,6 +90,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
sqlmigration.NewAddKeyOrganizationFactory(sqlstore),
sqlmigration.NewAddTraceFunnelsFactory(sqlstore),
sqlmigration.NewUpdateDashboardFactory(sqlstore),
sqlmigration.NewDropFeatureSetFactory(),
)
}

View File

@ -0,0 +1,58 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type dropFeatureSet struct{}
func NewDropFeatureSetFactory() factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("drop_feature_set"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return newDropFeatureSet(ctx, ps, c)
})
}
func newDropFeatureSet(_ context.Context, _ factory.ProviderSettings, _ Config) (SQLMigration, error) {
return &dropFeatureSet{}, nil
}
func (migration *dropFeatureSet) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *dropFeatureSet) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
if _, err := tx.
NewDropTable().
IfExists().
Table("feature_status").
Exec(ctx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *dropFeatureSet) Down(context.Context, *bun.DB) error {
return nil
}

View File

@ -1,29 +0,0 @@
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"
const DotMetricsEnabled = "DOT_METRICS_ENABLED"

View File

@ -9,7 +9,6 @@ import (
"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"
)
@ -30,9 +29,9 @@ type License struct {
ID valuer.UUID
Key string
Data map[string]interface{}
PlanName string
Features []*featuretypes.GettableFeature
Status string
PlanName valuer.String
Features []*Feature
Status valuer.String
ValidFrom int64
ValidUntil int64
CreatedAt time.Time
@ -124,7 +123,7 @@ func NewLicense(data []byte, organizationID valuer.UUID) (*License, error) {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal license data")
}
var features []*featuretypes.GettableFeature
var features []*Feature
// extract id from data
licenseIDStr, err := extractKeyFromMapStringInterface[string](licenseData, "id")
@ -145,26 +144,28 @@ func NewLicense(data []byte, organizationID valuer.UUID) (*License, error) {
delete(licenseData, "key")
// extract status from data
status, err := extractKeyFromMapStringInterface[string](licenseData, "status")
statusStr, err := extractKeyFromMapStringInterface[string](licenseData, "status")
if err != nil {
return nil, err
}
status := valuer.NewString(statusStr)
planMap, err := extractKeyFromMapStringInterface[map[string]any](licenseData, "plan")
if err != nil {
return nil, err
}
planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
planNameStr, err := extractKeyFromMapStringInterface[string](planMap, "name")
if err != nil {
return nil, err
}
planName := valuer.NewString(planNameStr)
// if license status is invalid then default it to basic
if status == LicenseStatusInvalid {
planName = PlanNameBasic
}
featuresFromZeus := make([]*featuretypes.GettableFeature, 0)
featuresFromZeus := make([]*Feature, 0)
if _features, ok := licenseData["features"]; ok {
featuresData, err := json.Marshal(_features)
if err != nil {
@ -232,28 +233,30 @@ func NewLicense(data []byte, organizationID valuer.UUID) (*License, error) {
}
func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License, error) {
var features []*featuretypes.GettableFeature
var features []*Feature
// extract status from data
status, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "status")
statusStr, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "status")
if err != nil {
return nil, err
}
status := valuer.NewString(statusStr)
planMap, err := extractKeyFromMapStringInterface[map[string]any](storableLicense.Data, "plan")
if err != nil {
return nil, err
}
planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
planNameStr, err := extractKeyFromMapStringInterface[string](planMap, "name")
if err != nil {
return nil, err
}
planName := valuer.NewString(planNameStr)
// if license status is invalid then default it to basic
if status == LicenseStatusInvalid {
planName = PlanNameBasic
}
featuresFromZeus := make([]*featuretypes.GettableFeature, 0)
featuresFromZeus := make([]*Feature, 0)
if _features, ok := storableLicense.Data["features"]; ok {
featuresData, err := json.Marshal(_features)
if err != nil {
@ -320,6 +323,10 @@ func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License,
}
func (license *License) UpdateFeatures(features []*Feature) {
license.Features = features
}
func (license *License) Update(data []byte) error {
updatedLicense, err := NewLicense(data, license.OrganizationID)
if err != nil {
@ -373,11 +380,4 @@ type Store interface {
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
}

View File

@ -4,7 +4,6 @@ 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"
@ -92,8 +91,8 @@ func TestNewLicenseV3(t *testing.T) {
PlanName: PlanNameEnterprise,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "ACTIVE",
Features: make([]*featuretypes.GettableFeature, 0),
Status: valuer.NewString("ACTIVE"),
Features: make([]*Feature, 0),
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
},
},
@ -116,8 +115,8 @@ func TestNewLicenseV3(t *testing.T) {
PlanName: PlanNameBasic,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "INVALID",
Features: make([]*featuretypes.GettableFeature, 0),
Status: valuer.NewString("INVALID"),
Features: make([]*Feature, 0),
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
},
},
@ -140,8 +139,8 @@ func TestNewLicenseV3(t *testing.T) {
PlanName: PlanNameEnterprise,
ValidFrom: 1234,
ValidUntil: 5678,
Status: "ACTIVE",
Features: make([]*featuretypes.GettableFeature, 0),
Status: valuer.NewString("ACTIVE"),
Features: make([]*Feature, 0),
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
LastValidatedAt: time.Time{},
@ -153,7 +152,7 @@ func TestNewLicenseV3(t *testing.T) {
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)
license.Features = make([]*Feature, 0)
delete(license.Data, "features")
}

View File

@ -1,67 +1,72 @@
package licensetypes
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
const SSO = "SSO"
const Basic = "BASIC_PLAN"
const Enterprise = "ENTERPRISE_PLAN"
import "github.com/SigNoz/signoz/pkg/valuer"
var (
PlanNameEnterprise = "ENTERPRISE"
PlanNameBasic = "BASIC"
// Feature Key
SSO = valuer.NewString("sso")
Onboarding = valuer.NewString("onboarding")
ChatSupport = valuer.NewString("chat_support")
Gateway = valuer.NewString("gateway")
PremiumSupport = valuer.NewString("premium_support")
UseSpanMetrics = valuer.NewString("use_span_metrics")
AnomalyDetection = valuer.NewString("anomaly_detection")
DotMetricsEnabled = valuer.NewString("dot_metrics_enabled")
// License State
LicenseStatusInvalid = valuer.NewString("invalid")
// Plan
PlanNameEnterprise = valuer.NewString("enterprise")
PlanNameBasic = valuer.NewString("basic")
)
var (
MapOldPlanKeyToNewPlanName map[string]string = map[string]string{PlanNameBasic: Basic, PlanNameEnterprise: Enterprise}
)
type Feature struct {
Name valuer.String `json:"name"`
Active bool `json:"active"`
Usage int64 `json:"usage"`
UsageLimit int64 `json:"usage_limit"`
Route string `json:"route"`
}
var (
LicenseStatusInvalid = "INVALID"
)
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
var BasicPlan = featuretypes.FeatureSet{
&featuretypes.GettableFeature{
var BasicPlan = []*Feature{
{
Name: SSO,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
{
Name: UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: Gateway,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: PremiumSupport,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.AnomalyDetection,
{
Name: AnomalyDetection,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.TraceFunnels,
{
Name: DotMetricsEnabled,
Active: false,
Usage: 0,
UsageLimit: -1,
@ -69,58 +74,58 @@ var BasicPlan = featuretypes.FeatureSet{
},
}
var EnterprisePlan = featuretypes.FeatureSet{
&featuretypes.GettableFeature{
var EnterprisePlan = []*Feature{
{
Name: SSO,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
{
Name: UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: Onboarding,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: ChatSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.AnomalyDetection,
{
Name: AnomalyDetection,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.TraceFunnels,
{
Name: DotMetricsEnabled,
Active: false,
Usage: 0,
UsageLimit: -1,
@ -128,9 +133,16 @@ var EnterprisePlan = featuretypes.FeatureSet{
},
}
var DefaultFeatureSet = featuretypes.FeatureSet{
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
var DefaultFeatureSet = []*Feature{
{
Name: UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
{
Name: DotMetricsEnabled,
Active: false,
Usage: 0,
UsageLimit: -1,