Compare commits

..

18 Commits

Author SHA1 Message Date
nityanandagohain
752899a7a4 fix: add to registry resource 2026-09-01 01:00:51 +05:30
nityanandagohain
eb4c570d53 fix: address comments 2026-09-01 00:35:40 +05:30
nityanandagohain
7ce405aff7 fix: updatablequickfilters updated 2026-08-31 15:40:03 +05:30
nityanandagohain
72618d1d83 Merge remote-tracking branch 'origin/main' into issue_5947 2026-08-31 15:31:19 +05:30
nityanandagohain
80b7edc22a fix: address comments 2026-08-31 15:28:58 +05:30
nityanandagohain
45a8bb424c Merge remote-tracking branch 'origin/main' into issue_5947 2026-08-28 20:28:22 +05:30
nityanandagohain
17afa7a3cf fix: address comments 2026-08-28 20:25:14 +05:30
nityanandagohain
6dc9bf7b16 Merge remote-tracking branch 'origin/main' into issue_5947 2026-08-27 14:13:30 +05:30
nityanandagohain
d15a1452f8 fix: minor fixes 2026-08-27 14:13:08 +05:30
nityanandagohain
2fe033ec78 fix: trigger build 2026-08-26 18:20:23 +05:30
nityanandagohain
02a2800f89 Merge remote-tracking branch 'origin/main' into issue_5947 2026-08-26 18:20:04 +05:30
nityanandagohain
915aa2eb70 Merge remote-tracking branch 'origin/issue_5947' into issue_5947 2026-08-26 16:44:26 +05:30
nityanandagohain
518caff0f2 Merge remote-tracking branch 'origin/main' into issue_5947 2026-08-26 16:44:09 +05:30
nityanandagohain
0d3ac28286 fix: update openapi spec 2026-08-26 16:41:50 +05:30
Nityananda Gohain
47beef07de Merge branch 'main' into issue_5947 2026-08-26 16:32:51 +05:30
nityanandagohain
5f2891bd6c fix: test cleanup 2026-08-26 16:32:01 +05:30
nityanandagohain
97f0e832ab fix: minor cleanup 2026-08-26 16:31:17 +05:30
nityanandagohain
770a8f7b0a fix: add quick filters v2 api to support TelemetryFieldKey 2026-08-26 11:12:59 +05:30
76 changed files with 2618 additions and 2892 deletions

View File

@@ -50,6 +50,7 @@ jobs:
- logspipelines
- passwordauthn
- preference
- quickfilter
- querierlogs
- queriertraces
- queriermetrics

View File

@@ -96,7 +96,6 @@ func runGenerateAuthz(_ context.Context) error {
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceLicense).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceLogs).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceTraces).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMetrics).String(): true,

View File

@@ -5642,219 +5642,6 @@ components:
- total
- endTimeBeforeRetention
type: object
LicensetypesFeature:
properties:
active:
type: boolean
name:
type: string
route:
type: string
usage:
format: int64
type: integer
usage_limit:
format: int64
type: integer
type: object
LicensetypesGettableActiveLicense:
properties:
createdAt:
format: date-time
type: string
eventQueue:
$ref: '#/components/schemas/LicensetypesLicenseEventQueue'
features:
items:
$ref: '#/components/schemas/LicensetypesFeature'
nullable: true
type: array
freeUntil:
format: date-time
type: string
id:
type: string
plan:
$ref: '#/components/schemas/LicensetypesLicensePlan'
platform:
type: string
state:
type: string
status:
type: string
updatedAt:
format: date-time
type: string
validFrom:
format: int64
type: integer
validUntil:
format: int64
type: integer
required:
- id
- validFrom
- validUntil
- status
- state
- platform
- freeUntil
- createdAt
- updatedAt
- plan
- features
- eventQueue
type: object
LicensetypesGettableLicense:
properties:
createdAt:
format: date-time
type: string
eventQueue:
$ref: '#/components/schemas/LicensetypesLicenseEventQueue'
features:
items:
$ref: '#/components/schemas/LicensetypesFeature'
nullable: true
type: array
freeUntil:
format: date-time
type: string
id:
type: string
plan:
$ref: '#/components/schemas/LicensetypesLicensePlan'
platform:
type: string
state:
type: string
status:
type: string
updatedAt:
format: date-time
type: string
validFrom:
format: int64
type: integer
validUntil:
format: int64
type: integer
required:
- id
- validFrom
- validUntil
- status
- state
- platform
- freeUntil
- createdAt
- updatedAt
- plan
- features
- eventQueue
type: object
LicensetypesGettableLicenseWithKey:
properties:
createdAt:
format: date-time
type: string
eventQueue:
$ref: '#/components/schemas/LicensetypesLicenseEventQueue'
features:
items:
$ref: '#/components/schemas/LicensetypesFeature'
nullable: true
type: array
freeUntil:
format: date-time
type: string
id:
type: string
key:
type: string
plan:
$ref: '#/components/schemas/LicensetypesLicensePlan'
platform:
type: string
state:
type: string
status:
type: string
updatedAt:
format: date-time
type: string
validFrom:
format: int64
type: integer
validUntil:
format: int64
type: integer
required:
- id
- validFrom
- validUntil
- status
- state
- platform
- freeUntil
- createdAt
- updatedAt
- plan
- features
- eventQueue
- key
type: object
LicensetypesLicenseEventQueue:
properties:
createdAt:
format: date-time
type: string
event:
type: string
scheduledAt:
format: date-time
type: string
status:
type: string
updatedAt:
format: date-time
type: string
required:
- event
- status
- scheduledAt
- createdAt
- updatedAt
type: object
LicensetypesLicensePlan:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
isActive:
type: boolean
name:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- isActive
- createdAt
- updatedAt
type: object
LicensetypesPostableLicense:
properties:
key:
type: string
type: object
LlmpricingruletypesGettablePricingRules:
properties:
items:
@@ -7596,6 +7383,26 @@ components:
- custom
- text
type: string
QuickfiltertypesSourceFilters:
properties:
filters:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
source:
type: string
required:
- filters
type: object
QuickfiltertypesUpdatableQuickFilters:
properties:
filters:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
required:
- filters
type: object
RenderErrorResponse:
properties:
error:
@@ -18532,6 +18339,171 @@ paths:
summary: Get query range result (v2)
tags:
- dashboard
/api/v2/quick_filters:
get:
deprecated: false
description: Returns the org's quick filters for every source, each filter as
a telemetry field key.
operationId: ListQuickFilters
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/QuickfiltertypesSourceFilters'
nullable: true
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- quick-filter:list
- tokenizer:
- quick-filter:list
summary: List quick filters
tags:
- quick_filter
/api/v2/quick_filters/{source}:
get:
deprecated: false
description: Returns the org's quick filters for one source, each filter as
a telemetry field key.
operationId: GetQuickFilters
parameters:
- in: path
name: source
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/QuickfiltertypesSourceFilters'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- quick-filter:read
- tokenizer:
- quick-filter:read
summary: Get a source's quick filters
tags:
- quick_filter
put:
deprecated: false
description: Replaces the org's quick filters for the source named in the path.
operationId: UpdateQuickFilters
parameters:
- in: path
name: source
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/QuickfiltertypesUpdatableQuickFilters'
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- quick-filter:update
- tokenizer:
- quick-filter:update
summary: Update quick filters
tags:
- quick_filter
/api/v2/readyz:
get:
operationId: Readyz
@@ -24413,347 +24385,6 @@ paths:
summary: Get flamegraph view for a trace
tags:
- tracedetail
/api/v4/licenses:
get:
deprecated: false
description: This endpoint lists all the licenses of the organization.
operationId: ListLicenses
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/LicensetypesGettableLicense'
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- license:list
- tokenizer:
- license:list
summary: List licenses.
tags:
- licenses
post:
deprecated: false
description: This endpoint validates the license key with the upstream server
and activates the license for the organization.
operationId: ActivateLicense
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LicensetypesPostableLicense'
responses:
"202":
description: Accepted
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- license:create
- tokenizer:
- license:create
summary: Activate a license.
tags:
- licenses
/api/v4/licenses/{id}:
delete:
deprecated: false
description: This endpoint deletes the license by id. Licenses managed by SigNoz
Cloud cannot be deleted.
operationId: DeleteLicense
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- license:delete
- tokenizer:
- license:delete
summary: Delete a license.
tags:
- licenses
get:
deprecated: false
description: This endpoint gets the license by id.
operationId: GetLicense
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/LicensetypesGettableLicenseWithKey'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- license:read
- tokenizer:
- license:read
summary: Get a license.
tags:
- licenses
put:
deprecated: false
description: This endpoint refreshes the active license of the organization
from the upstream server.
operationId: RefreshLicense
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- license:update
- tokenizer:
- license:update
summary: Refresh a license.
tags:
- licenses
/api/v4/licenses/active:
get:
deprecated: false
description: This endpoint gets the active license of the organization.
operationId: GetActiveLicense
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/LicensetypesGettableActiveLicense'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key: []
- tokenizer: []
summary: Get the active license.
tags:
- licenses
/api/v4/traces/{traceID}/waterfall:
post:
deprecated: false

View File

@@ -22,6 +22,89 @@ func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
return &licensingAPI{licensing: licensing}
}
func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
req := new(licensetypes.PostableLicense)
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
return
}
err = api.licensing.Activate(r.Context(), orgID, req.Key)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusAccepted, nil)
}
func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
license, err := api.licensing.GetActive(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
}
gettableLicense := licensetypes.NewGettableLicense(license.Data, license.Key)
render.Success(rw, http.StatusOK, gettableLicense)
}
func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
err = api.licensing.Refresh(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

View File

@@ -96,12 +96,12 @@ func (provider *provider) Validate(ctx context.Context) error {
}
func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) error {
zeusLicense, err := provider.zeus.GetLicense(ctx, key)
data, err := provider.zeus.GetLicense(ctx, key)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
}
license, err := licensetypes.NewLicense(zeusLicense, organizationID)
license, err := licensetypes.NewLicense(data, organizationID)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
}
@@ -115,47 +115,6 @@ func (provider *provider) Activate(ctx context.Context, organizationID valuer.UU
return nil
}
func (provider *provider) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.License, error) {
storableLicense, err := provider.store.Get(ctx, organizationID, licenseID)
if err != nil {
return nil, err
}
return licensetypes.NewLicenseFromStorableLicense(storableLicense)
}
func (provider *provider) List(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.License, error) {
storableLicenses, err := provider.store.GetAll(ctx, organizationID)
if err != nil {
return nil, err
}
licenses := make([]*licensetypes.License, 0, len(storableLicenses))
for _, storableLicense := range storableLicenses {
license, err := licensetypes.NewLicenseFromStorableLicense(storableLicense)
if err != nil {
return nil, err
}
licenses = append(licenses, license)
}
return licenses, nil
}
func (provider *provider) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
license, err := provider.Get(ctx, organizationID, licenseID)
if err != nil {
return err
}
if err := license.ErrIfCloud(); err != nil {
return errors.WithAdditionalf(err, "license %s cannot be deleted", licenseID.StringValue())
}
return provider.store.Delete(ctx, organizationID, licenseID)
}
func (provider *provider) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) {
storableLicenses, err := provider.store.GetAll(ctx, organizationID)
if err != nil {
@@ -180,7 +139,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
zeusLicense, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
if err != nil {
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
@@ -195,7 +154,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
err = activeLicense.Update(zeusLicense)
err = activeLicense.Update(data)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity from license data")
}

View File

@@ -64,22 +64,6 @@ func (store *store) GetAll(ctx context.Context, organizationID valuer.UUID) ([]*
return storableLicenses, nil
}
func (store *store) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
_, err := store.
sqlstore.
BunDB().
NewDelete().
Model(new(licensetypes.StorableLicense)).
Where("org_id = ?", organizationID).
Where("id = ?", licenseID).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to delete license with ID: %s", licenseID)
}
return nil
}
func (store *store) Update(ctx context.Context, organizationID valuer.UUID, storableLicense *licensetypes.StorableLicense) error {
_, err := store.
sqlstore.

View File

@@ -76,6 +76,11 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
// v3
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.LicensingAPI.GetActive)).Methods(http.MethodGet)
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)

View File

@@ -51,7 +51,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
}, nil
}
func (provider *Provider) GetLicense(ctx context.Context, key string) (*zeustypes.License, error) {
func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, error) {
response, err := provider.do(
ctx,
provider.config.URL.JoinPath("/v2/licenses/me"),
@@ -63,12 +63,7 @@ func (provider *Provider) GetLicense(ctx context.Context, key string) (*zeustype
return nil, err
}
license := new(zeustypes.License)
if err := json.Unmarshal([]byte(gjson.GetBytes(response, "data").String()), license); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, zeus.ErrCodeResponseMalformed, "failed to unmarshal license data")
}
return license, nil
return []byte(gjson.GetBytes(response, "data").String()), nil
}
func (provider *Provider) GetCheckoutURL(ctx context.Context, key string, body []byte) ([]byte, error) {

View File

@@ -103,30 +103,30 @@ function createMockLicense(
overrides: Partial<LicenseResModel> = {},
): LicenseResModel {
return {
id: 'test-license-id',
eventQueue: {
createdAt: '0',
key: 'test-key',
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
scheduledAt: '0',
scheduled_at: '0',
status: '',
updatedAt: '0',
updated_at: '0',
},
state: LicenseState.ACTIVATED,
status: LicenseStatus.VALID,
platform: LicensePlatform.CLOUD,
createdAt: '0',
created_at: '0',
plan: {
id: '0',
createdAt: '0',
created_at: '0',
description: '',
isActive: true,
is_active: true,
name: '',
updatedAt: '0',
updated_at: '0',
},
freeUntil: '0',
updatedAt: '0',
validFrom: 0,
validUntil: 0,
plan_id: '0',
free_until: '0',
updated_at: '0',
valid_from: 0,
valid_until: 0,
...overrides,
};
}

View File

@@ -1,538 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
DeleteLicensePathParameters,
GetActiveLicense200,
GetLicense200,
GetLicensePathParameters,
LicensetypesPostableLicenseDTO,
ListLicenses200,
RefreshLicensePathParameters,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint lists all the licenses of the organization.
* @summary List licenses.
*/
export const listLicenses = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListLicenses200>({
url: `/api/v4/licenses`,
method: 'GET',
signal,
});
};
export const getListLicensesQueryKey = () => {
return [`/api/v4/licenses`] as const;
};
export const getListLicensesQueryOptions = <
TData = Awaited<ReturnType<typeof listLicenses>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLicenses>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListLicensesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listLicenses>>> = ({
signal,
}) => listLicenses(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listLicenses>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListLicensesQueryResult = NonNullable<
Awaited<ReturnType<typeof listLicenses>>
>;
export type ListLicensesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List licenses.
*/
export function useListLicenses<
TData = Awaited<ReturnType<typeof listLicenses>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLicenses>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListLicensesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List licenses.
*/
export const invalidateListLicenses = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListLicensesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint validates the license key with the upstream server and activates the license for the organization.
* @summary Activate a license.
*/
export const activateLicense = (
licensetypesPostableLicenseDTO?: BodyType<LicensetypesPostableLicenseDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v4/licenses`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: licensetypesPostableLicenseDTO,
signal,
});
};
export const getActivateLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
const mutationKey = ['activateLicense'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof activateLicense>>,
{ data?: BodyType<LicensetypesPostableLicenseDTO> }
> = (props) => {
const { data } = props ?? {};
return activateLicense(data);
};
return { mutationFn, ...mutationOptions };
};
export type ActivateLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof activateLicense>>
>;
export type ActivateLicenseMutationBody =
| BodyType<LicensetypesPostableLicenseDTO>
| undefined;
export type ActivateLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Activate a license.
*/
export const useActivateLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
return useMutation(getActivateLicenseMutationOptions(options));
};
/**
* This endpoint deletes the license by id. Licenses managed by SigNoz Cloud cannot be deleted.
* @summary Delete a license.
*/
export const deleteLicense = (
{ id }: DeleteLicensePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v4/licenses/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
> => {
const mutationKey = ['deleteLicense'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteLicense>>,
{ pathParams: DeleteLicensePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteLicense(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteLicense>>
>;
export type DeleteLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete a license.
*/
export const useDeleteLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
> => {
return useMutation(getDeleteLicenseMutationOptions(options));
};
/**
* This endpoint gets the license by id.
* @summary Get a license.
*/
export const getLicense = (
{ id }: GetLicensePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetLicense200>({
url: `/api/v4/licenses/${id}`,
method: 'GET',
signal,
});
};
export const getGetLicenseQueryKey = ({ id }: GetLicensePathParameters) => {
return [`/api/v4/licenses/${id}`] as const;
};
export const getGetLicenseQueryOptions = <
TData = Awaited<ReturnType<typeof getLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLicensePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLicense>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetLicenseQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getLicense>>> = ({
signal,
}) => getLicense({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getLicense>>, TError, TData> & {
queryKey: QueryKey;
};
};
export type GetLicenseQueryResult = NonNullable<
Awaited<ReturnType<typeof getLicense>>
>;
export type GetLicenseQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get a license.
*/
export function useGetLicense<
TData = Awaited<ReturnType<typeof getLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLicensePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLicense>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetLicenseQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get a license.
*/
export const invalidateGetLicense = async (
queryClient: QueryClient,
{ id }: GetLicensePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetLicenseQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint refreshes the active license of the organization from the upstream server.
* @summary Refresh a license.
*/
export const refreshLicense = (
{ id }: RefreshLicensePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v4/licenses/${id}`,
method: 'PUT',
signal,
});
};
export const getRefreshLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
> => {
const mutationKey = ['refreshLicense'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof refreshLicense>>,
{ pathParams: RefreshLicensePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return refreshLicense(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type RefreshLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof refreshLicense>>
>;
export type RefreshLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Refresh a license.
*/
export const useRefreshLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
> => {
return useMutation(getRefreshLicenseMutationOptions(options));
};
/**
* This endpoint gets the active license of the organization.
* @summary Get the active license.
*/
export const getActiveLicense = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetActiveLicense200>({
url: `/api/v4/licenses/active`,
method: 'GET',
signal,
});
};
export const getGetActiveLicenseQueryKey = () => {
return [`/api/v4/licenses/active`] as const;
};
export const getGetActiveLicenseQueryOptions = <
TData = Awaited<ReturnType<typeof getActiveLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetActiveLicenseQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getActiveLicense>>> = ({
signal,
}) => getActiveLicense(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetActiveLicenseQueryResult = NonNullable<
Awaited<ReturnType<typeof getActiveLicense>>
>;
export type GetActiveLicenseQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get the active license.
*/
export function useGetActiveLicense<
TData = Awaited<ReturnType<typeof getActiveLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetActiveLicenseQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get the active license.
*/
export const invalidateGetActiveLicense = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetActiveLicenseQueryKey() },
options,
);
return queryClient;
};

View File

@@ -0,0 +1,316 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
GetQuickFilters200,
GetQuickFiltersPathParameters,
ListQuickFilters200,
QuickfiltertypesUpdatableQuickFiltersDTO,
RenderErrorResponseDTO,
UpdateQuickFiltersPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns the org's quick filters for every source, each filter as a telemetry field key.
* @summary List quick filters
*/
export const listQuickFilters = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListQuickFilters200>({
url: `/api/v2/quick_filters`,
method: 'GET',
signal,
});
};
export const getListQuickFiltersQueryKey = () => {
return [`/api/v2/quick_filters`] as const;
};
export const getListQuickFiltersQueryOptions = <
TData = Awaited<ReturnType<typeof listQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listQuickFilters>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListQuickFiltersQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listQuickFilters>>> = ({
signal,
}) => listQuickFilters(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listQuickFilters>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListQuickFiltersQueryResult = NonNullable<
Awaited<ReturnType<typeof listQuickFilters>>
>;
export type ListQuickFiltersQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List quick filters
*/
export function useListQuickFilters<
TData = Awaited<ReturnType<typeof listQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listQuickFilters>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListQuickFiltersQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List quick filters
*/
export const invalidateListQuickFilters = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListQuickFiltersQueryKey() },
options,
);
return queryClient;
};
/**
* Returns the org's quick filters for one source, each filter as a telemetry field key.
* @summary Get a source's quick filters
*/
export const getQuickFilters = (
{ source }: GetQuickFiltersPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetQuickFilters200>({
url: `/api/v2/quick_filters/${source}`,
method: 'GET',
signal,
});
};
export const getGetQuickFiltersQueryKey = ({
source,
}: GetQuickFiltersPathParameters) => {
return [`/api/v2/quick_filters/${source}`] as const;
};
export const getGetQuickFiltersQueryOptions = <
TData = Awaited<ReturnType<typeof getQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ source }: GetQuickFiltersPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetQuickFiltersQueryKey({ source });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getQuickFilters>>> = ({
signal,
}) => getQuickFilters({ source }, signal);
return {
queryKey,
queryFn,
enabled: !!source,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetQuickFiltersQueryResult = NonNullable<
Awaited<ReturnType<typeof getQuickFilters>>
>;
export type GetQuickFiltersQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get a source's quick filters
*/
export function useGetQuickFilters<
TData = Awaited<ReturnType<typeof getQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ source }: GetQuickFiltersPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetQuickFiltersQueryOptions({ source }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get a source's quick filters
*/
export const invalidateGetQuickFilters = async (
queryClient: QueryClient,
{ source }: GetQuickFiltersPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetQuickFiltersQueryKey({ source }) },
options,
);
return queryClient;
};
/**
* Replaces the org's quick filters for the source named in the path.
* @summary Update quick filters
*/
export const updateQuickFilters = (
{ source }: UpdateQuickFiltersPathParameters,
quickfiltertypesUpdatableQuickFiltersDTO?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/quick_filters/${source}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: quickfiltertypesUpdatableQuickFiltersDTO,
signal,
});
};
export const getUpdateQuickFiltersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
> => {
const mutationKey = ['updateQuickFilters'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateQuickFilters>>,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateQuickFilters(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateQuickFiltersMutationResult = NonNullable<
Awaited<ReturnType<typeof updateQuickFilters>>
>;
export type UpdateQuickFiltersMutationBody =
| BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>
| undefined;
export type UpdateQuickFiltersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update quick filters
*/
export const useUpdateQuickFilters = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
> => {
return useMutation(getUpdateQuickFiltersMutationOptions(options));
};

View File

@@ -7173,247 +7173,6 @@ export interface InframonitoringtypesVolumesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface LicensetypesFeatureDTO {
/**
* @type boolean
*/
active?: boolean;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
route?: string;
/**
* @type integer
* @format int64
*/
usage?: number;
/**
* @type integer
* @format int64
*/
usage_limit?: number;
}
export interface LicensetypesLicenseEventQueueDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
/**
* @type string
*/
event: string;
/**
* @type string
* @format date-time
*/
scheduledAt: string;
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
}
export interface LicensetypesLicensePlanDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type boolean
*/
isActive: boolean;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
}
export interface LicensetypesGettableActiveLicenseDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
eventQueue: LicensetypesLicenseEventQueueDTO;
/**
* @type array,null
*/
features: LicensetypesFeatureDTO[] | null;
/**
* @type string
* @format date-time
*/
freeUntil: string;
/**
* @type string
*/
id: string;
plan: LicensetypesLicensePlanDTO;
/**
* @type string
*/
platform: string;
/**
* @type string
*/
state: string;
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
/**
* @type integer
* @format int64
*/
validFrom: number;
/**
* @type integer
* @format int64
*/
validUntil: number;
}
export interface LicensetypesGettableLicenseDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
eventQueue: LicensetypesLicenseEventQueueDTO;
/**
* @type array,null
*/
features: LicensetypesFeatureDTO[] | null;
/**
* @type string
* @format date-time
*/
freeUntil: string;
/**
* @type string
*/
id: string;
plan: LicensetypesLicensePlanDTO;
/**
* @type string
*/
platform: string;
/**
* @type string
*/
state: string;
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
/**
* @type integer
* @format int64
*/
validFrom: number;
/**
* @type integer
* @format int64
*/
validUntil: number;
}
export interface LicensetypesGettableLicenseWithKeyDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
eventQueue: LicensetypesLicenseEventQueueDTO;
/**
* @type array,null
*/
features: LicensetypesFeatureDTO[] | null;
/**
* @type string
* @format date-time
*/
freeUntil: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
key: string;
plan: LicensetypesLicensePlanDTO;
/**
* @type string
*/
platform: string;
/**
* @type string
*/
state: string;
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
/**
* @type integer
* @format int64
*/
validFrom: number;
/**
* @type integer
* @format int64
*/
validUntil: number;
}
export interface LicensetypesPostableLicenseDTO {
/**
* @type string
*/
key?: string;
}
/**
* @nullable
*/
@@ -8678,6 +8437,24 @@ export enum Querybuildertypesv5QueryTypeDTO {
clickhouse_sql = 'clickhouse_sql',
promql = 'promql',
}
export interface QuickfiltertypesSourceFiltersDTO {
/**
* @type array
*/
filters: TelemetrytypesTelemetryFieldKeyDTO[];
/**
* @type string
*/
source?: string;
}
export interface QuickfiltertypesUpdatableQuickFiltersDTO {
/**
* @type array
*/
filters: TelemetrytypesTelemetryFieldKeyDTO[];
}
export interface RenderErrorResponseDTO {
error: ErrorsJSONDTO;
/**
@@ -12068,6 +11845,31 @@ export type GetPublicDashboardPanelQueryRangeV2200 = {
status: string;
};
export type ListQuickFilters200 = {
/**
* @type array,null
*/
data: QuickfiltertypesSourceFiltersDTO[] | null;
/**
* @type string
*/
status: string;
};
export type GetQuickFiltersPathParameters = {
source: string;
};
export type GetQuickFilters200 = {
data: QuickfiltertypesSourceFiltersDTO;
/**
* @type string
*/
status: string;
};
export type UpdateQuickFiltersPathParameters = {
source: string;
};
export type Readyz200 = {
data: FactoryResponseDTO;
/**
@@ -12670,42 +12472,6 @@ export type GetFlamegraph200 = {
status: string;
};
export type ListLicenses200 = {
/**
* @type array
*/
data: LicensetypesGettableLicenseDTO[];
/**
* @type string
*/
status: string;
};
export type DeleteLicensePathParameters = {
id: string;
};
export type GetLicensePathParameters = {
id: string;
};
export type GetLicense200 = {
data: LicensetypesGettableLicenseWithKeyDTO;
/**
* @type string
*/
status: string;
};
export type RefreshLicensePathParameters = {
id: string;
};
export type GetActiveLicense200 = {
data: LicensetypesGettableActiveLicenseDTO;
/**
* @type string
*/
status: string;
};
export type GetWaterfallV4PathParameters = {
traceID: string;
};

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { refreshLicense } from 'api/generated/services/licenses';
import refreshPaymentStatus from 'api/v3/licenses/put';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { RefreshCcw } from '@signozhq/icons';
@@ -14,21 +14,17 @@ function RefreshPaymentStatus({
className?: string;
}): JSX.Element {
const { t } = useTranslation(['failedPayment']);
const { activeLicense, activeLicenseRefetch } = useAppContext();
const { activeLicenseRefetch } = useAppContext();
const [isLoading, setIsLoading] = useState(false);
const handleRefreshPaymentStatus = async (): Promise<void> => {
if (!activeLicense) {
return;
}
setIsLoading(true);
try {
await refreshLicense({ id: activeLicense.id });
await refreshPaymentStatus();
activeLicenseRefetch();
await Promise.all([activeLicenseRefetch()]);
} catch (e) {
console.error(e);
}

View File

@@ -28,6 +28,7 @@ export const REACT_QUERY_KEY = {
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
GET_TRACE_V4_WATERFALL: 'GET_TRACE_V4_WATERFALL',
GET_TRACE_AGGREGATIONS: 'GET_TRACE_AGGREGATIONS',

View File

@@ -453,7 +453,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (
!isFetchingActiveLicense &&
!isNull(activeLicense) &&
activeLicense?.eventQueue?.event === LicenseEvent.DEFAULT
activeLicense?.event_queue?.event === LicenseEvent.DEFAULT
) {
setShowPaymentFailedWarning(true);
}
@@ -820,7 +820,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
Your bill payment has failed. Your workspace will get suspended on{' '}
<span>
{getFormattedDateWithMinutes(
dayjs(activeLicense?.eventQueue?.scheduledAt).unix() || Date.now(),
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
)}
.
</span>

View File

@@ -15,11 +15,6 @@ import { getFormattedDate } from 'utils/timeUtils';
import BillingContainer from './BillingContainer';
jest.mock('hooks/useActiveLicenseKey/useActiveLicenseKey', () => ({
__esModule: true,
default: jest.fn(() => ({ licenseKey: 'test-key', isLoading: false })),
}));
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({

View File

@@ -30,7 +30,6 @@ import useAxiosError from 'hooks/useAxiosError';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { isEmpty, pick } from 'lodash-es';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import { useAppContext } from 'providers/App/App';
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
@@ -146,7 +145,6 @@ export default function BillingContainer(): JSX.Element {
activeLicense,
activeLicenseFetchError,
} = useAppContext();
const { licenseKey } = useActiveLicenseKey();
const { notifications } = useNotifications();
const handleError = useAxiosError();
@@ -209,9 +207,9 @@ export default function BillingContainer(): JSX.Element {
isFetching: isFetchingBillingData,
data: billingData,
} = useQuery([REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id], {
queryFn: () => getUsage(licenseKey || ''),
queryFn: () => getUsage(activeLicense?.key || ''),
onError: handleError,
enabled: !!licenseKey,
enabled: activeLicense !== null,
onSuccess: processUsageData,
});

View File

@@ -16,7 +16,6 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { StatusCodes } from 'http-status-codes';
import find from 'lodash-es/find';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import { useAppContext } from 'providers/App/App';
import {
ErrorResponse,
@@ -79,8 +78,7 @@ function GeneralSettings({
const [logsCurrentTTLValues, setLogsCurrentTTLValues] =
useState(logsTtlValuesPayload);
const { user } = useAppContext();
const { licenseKey } = useActiveLicenseKey();
const { user, activeLicense } = useAppContext();
const [setRetentionPermission] = useComponentPermission(
['set_retention_period'],
@@ -675,13 +673,13 @@ function GeneralSettings({
</span>
</div>
{(showCustomDomainSettings || licenseKey) && (
{(showCustomDomainSettings || activeLicense?.key) && (
<div className="custom-domain-card">
{showCustomDomainSettings && <CustomDomainSettings />}
{showCustomDomainSettings && licenseKey && (
{showCustomDomainSettings && activeLicense?.key && (
<div className="custom-domain-card-divider" />
)}
{licenseKey && (
{activeLicense?.key && (
<>
<LicenseKeyRow />
<LicenseRowDismissibleCallout />

View File

@@ -2,16 +2,16 @@ import { useCopyToClipboard } from 'react-use';
import { Copy, KeyRound } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { toast } from '@signozhq/ui/sonner';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import { useAppContext } from 'providers/App/App';
import { getMaskedKey } from 'utils/maskedKey';
import './LicenseKeyRow.styles.scss';
function LicenseKeyRow(): JSX.Element | null {
const { licenseKey } = useActiveLicenseKey();
const { activeLicense } = useAppContext();
const [, copyToClipboard] = useCopyToClipboard();
if (!licenseKey) {
if (!activeLicense?.key) {
return null;
}
@@ -27,14 +27,16 @@ function LicenseKeyRow(): JSX.Element | null {
<span className="license-key-row__label">SigNoz License Key</span>
</span>
<span className="license-key-row__value">
<code className="license-key-row__code">{getMaskedKey(licenseKey)}</code>
<code className="license-key-row__code">
{getMaskedKey(activeLicense.key)}
</code>
<Button
type="button"
size="sm"
aria-label="Copy license key"
data-testid="license-key-row-copy-btn"
className="license-key-row__copy-btn"
onClick={(): void => handleCopyLicenseKey(licenseKey)}
onClick={(): void => handleCopyLicenseKey(activeLicense.key)}
>
<Copy size={12} />
</Button>

View File

@@ -1,13 +1,7 @@
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import LicenseKeyRow from '../LicenseKeyRow';
jest.mock('hooks/useActiveLicenseKey/useActiveLicenseKey');
const mockUseActiveLicenseKey = useActiveLicenseKey as jest.MockedFunction<
typeof useActiveLicenseKey
>;
const mockCopyToClipboard = jest.fn();
jest.mock('react-use', () => ({
@@ -29,22 +23,20 @@ describe('LicenseKeyRow', () => {
jest.clearAllMocks();
});
it('renders nothing when the license key is absent', () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: undefined,
isLoading: false,
it('renders nothing when activeLicense key is absent', () => {
const { container } = render(<LicenseKeyRow />, undefined, {
appContextOverrides: { activeLicense: null },
});
const { container } = render(<LicenseKeyRow />);
expect(container).toBeEmptyDOMElement();
});
it('renders label and masked key when the license key exists', () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'abcdefghij',
isLoading: false,
it('renders label and masked key when activeLicense key exists', () => {
render(<LicenseKeyRow />, undefined, {
appContextOverrides: {
activeLicense: { key: 'abcdefghij' } as any,
},
});
render(<LicenseKeyRow />);
expect(screen.getByText('SigNoz License Key')).toBeInTheDocument();
expect(screen.getByText('ab·······ij')).toBeInTheDocument();
@@ -53,10 +45,6 @@ describe('LicenseKeyRow', () => {
it('calls copyToClipboard and shows success toast when clipboard is available', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'test-key',
isLoading: false,
});
render(<LicenseKeyRow />);
await user.click(screen.getByRole('button', { name: /copy license key/i }));

View File

@@ -2,9 +2,9 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Button, Form } from 'antd';
import { activateLicense } from 'api/generated/services/licenses';
import apply from 'api/v3/licenses/post';
import { useNotifications } from 'hooks/useNotifications';
import { toAPIError } from 'utils/errorUtils';
import APIError from 'types/api/error';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import {
@@ -26,7 +26,7 @@ function ApplyLicenseForm({
const isDisabled = isLoading || !key;
const onFinish = async (values: unknown): Promise<void> => {
const onFinish = async (values: unknown | { key: string }): Promise<void> => {
const params = values as { key: string };
if (params.key === '' || !params.key) {
notifications.error({
@@ -38,19 +38,18 @@ function ApplyLicenseForm({
setIsLoading(true);
try {
await activateLicense({
await apply({
key: params.key,
});
licenseRefetch();
await Promise.all([licenseRefetch()]);
notifications.success({
message: 'Success',
description: t('license_applied'),
});
} catch (e) {
const apiError = toAPIError(e as Parameters<typeof toAPIError>[0]);
notifications.error({
message: apiError.getErrorCode(),
description: apiError.getErrorMessage(),
message: (e as APIError).getErrorCode(),
description: (e as APIError).getErrorMessage(),
});
}
setIsLoading(false);

View File

@@ -3,13 +3,13 @@ import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { useNotifications } from 'hooks/useNotifications';
import { Copy } from '@signozhq/icons';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import { useAppContext } from 'providers/App/App';
import { getMaskedKey } from 'utils/maskedKey';
import './LicenseSection.styles.scss';
function LicenseSection(): JSX.Element | null {
const { licenseKey } = useActiveLicenseKey();
const { activeLicense } = useAppContext();
const { notifications } = useNotifications();
const [, handleCopyToClipboard] = useCopyToClipboard();
@@ -20,7 +20,7 @@ function LicenseSection(): JSX.Element | null {
});
};
if (!licenseKey) {
if (!activeLicense?.key) {
return <></>;
}
@@ -35,13 +35,13 @@ function LicenseSection(): JSX.Element | null {
<div className="license-section-content-item-title-action">
<span>License key</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Typography.Text code>{getMaskedKey(licenseKey)}</Typography.Text>
<Typography.Text code>{getMaskedKey(activeLicense.key)}</Typography.Text>
<Button
variant="link"
color="none"
aria-label="Copy license key"
data-testid="license-key-copy-btn"
onClick={(): void => handleCopyKey(licenseKey)}
onClick={(): void => handleCopyKey(activeLicense.key)}
>
<Copy size={14} />
</Button>

View File

@@ -1,6 +1,5 @@
import userEvent from '@testing-library/user-event';
import MySettingsContainer from 'container/MySettings';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import { logEventMock } from '__tests__/logEventMock';
import {
act,
@@ -13,11 +12,6 @@ import {
import APIError from 'types/api/error';
import { toast } from '@signozhq/ui/sonner';
jest.mock('hooks/useActiveLicenseKey/useActiveLicenseKey');
const mockUseActiveLicenseKey = useActiveLicenseKey as jest.MockedFunction<
typeof useActiveLicenseKey
>;
const toggleThemeFunction = jest.fn();
const copyToClipboardFn = jest.fn();
const editUserFn = jest.fn();
@@ -93,10 +87,6 @@ describe('MySettings Flows', () => {
jest.clearAllMocks();
editUserFn.mockResolvedValue({});
updateMyPasswordFn.mockResolvedValue({});
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'test-key',
isLoading: false,
});
render(<MySettingsContainer />);
});
@@ -378,11 +368,11 @@ describe('MySettings Flows', () => {
});
it('Should not render license section when license key is missing', () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: undefined,
isLoading: false,
const { container } = render(<MySettingsContainer />, undefined, {
appContextOverrides: {
activeLicense: null,
},
});
const { container } = render(<MySettingsContainer />);
const scoped = within(container);
expect(scoped.queryByText('License')).not.toBeInTheDocument();
@@ -393,32 +383,38 @@ describe('MySettings Flows', () => {
});
it('Should mask license key in the UI', () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'abcd',
isLoading: false,
const { container } = render(<MySettingsContainer />, undefined, {
appContextOverrides: {
activeLicense: {
key: 'abcd',
} as any,
},
});
const { container } = render(<MySettingsContainer />);
expect(within(container).getByText('ab·······cd')).toBeInTheDocument();
});
it('Should not mask license key if it is too short', () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'abc',
isLoading: false,
const { container } = render(<MySettingsContainer />, undefined, {
appContextOverrides: {
activeLicense: {
key: 'abc',
} as any,
},
});
const { container } = render(<MySettingsContainer />);
expect(within(container).getByText('abc')).toBeInTheDocument();
});
it('Should copy license key and show success toast', async () => {
const user = userEvent.setup();
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'test-license-key-12345',
isLoading: false,
const { container } = render(<MySettingsContainer />, undefined, {
appContextOverrides: {
activeLicense: {
key: 'test-license-key-12345',
} as any,
},
});
const { container } = render(<MySettingsContainer />);
await user.click(within(container).getByTestId('license-key-copy-btn'));

View File

@@ -329,12 +329,11 @@ describe('transformTransactionGroupsToResourcePermissions', () => {
it('returns all resources from RESOURCE_ORDER even with empty transaction groups', () => {
const result = transformTransactionGroupsToResourcePermissions([]);
expect(result).toHaveLength(8);
expect(result).toHaveLength(7);
expect(result.map((r) => r.resourceKind)).toStrictEqual([
'factor-api-key',
'role',
'serviceaccount',
'license',
'logs',
'traces',
'metrics',
@@ -419,12 +418,11 @@ describe('createEmptyRolePermissions', () => {
it('creates permissions for all resources in RESOURCE_ORDER', () => {
const result = createEmptyRolePermissions();
expect(result).toHaveLength(8);
expect(result).toHaveLength(7);
expect(result.map((r) => r.resourceKind)).toStrictEqual([
'factor-api-key',
'role',
'serviceaccount',
'license',
'logs',
'traces',
'metrics',

View File

@@ -2,7 +2,6 @@ import {
Bot,
ChartLine,
DraftingCompass,
FileKey,
Gauge,
Key,
Logs,
@@ -62,13 +61,6 @@ export const RESOURCE_PANELS: Record<AuthZResource, ResourcePanelConfig> = {
'Type service account ID, separate multiple with comma or space',
docsAnchor: 'service-account',
},
license: {
label: 'Licenses',
description: 'Licenses of the workspace, including the license key.',
icon: FileKey,
selectorPlaceholder: 'Type license ID, separate multiple with comma or space',
docsAnchor: 'license',
},
logs: {
label: 'Logs',
description: 'Log data collected across the workspace.',

View File

@@ -1,29 +0,0 @@
import { useQuery, UseQueryResult } from 'react-query';
import {
getActiveLicense,
getGetActiveLicenseQueryKey,
} from 'api/generated/services/licenses';
import APIError from 'types/api/error';
import { LicenseResModel } from 'types/api/licensesV3/getActive';
import { toAPIError } from 'utils/errorUtils';
import { toLicenseResModel } from './utils';
const useActiveLicense = (
isLoggedIn: boolean,
): UseQueryResult<LicenseResModel, APIError> =>
useQuery({
queryFn: async (): Promise<LicenseResModel> => {
try {
const response = await getActiveLicense();
return toLicenseResModel(response.data);
} catch (error) {
throw toAPIError(error as Parameters<typeof toAPIError>[0]);
}
},
queryKey: getGetActiveLicenseQueryKey(),
enabled: !!isLoggedIn,
retry: false,
});
export default useActiveLicense;

View File

@@ -1,37 +0,0 @@
import { LicensetypesGettableActiveLicenseDTO } from 'api/generated/services/sigNoz.schemas';
import {
LicenseEvent,
LicensePlatform,
LicenseResModel,
LicenseState,
LicenseStatus,
} from 'types/api/licensesV3/getActive';
export const toLicenseResModel = (
dto: LicensetypesGettableActiveLicenseDTO,
): LicenseResModel => ({
id: dto.id,
status: dto.status as LicenseStatus,
state: dto.state as LicenseState,
platform: dto.platform as LicensePlatform,
plan: {
id: dto.plan.id,
name: dto.plan.name,
description: dto.plan.description,
isActive: dto.plan.isActive,
createdAt: dto.plan.createdAt,
updatedAt: dto.plan.updatedAt,
},
eventQueue: {
event: dto.eventQueue.event as LicenseEvent,
status: dto.eventQueue.status,
scheduledAt: dto.eventQueue.scheduledAt,
createdAt: dto.eventQueue.createdAt,
updatedAt: dto.eventQueue.updatedAt,
},
freeUntil: dto.freeUntil,
createdAt: dto.createdAt,
updatedAt: dto.updatedAt,
validFrom: dto.validFrom,
validUntil: dto.validUntil,
});

View File

@@ -1,35 +0,0 @@
import { useMemo } from 'react';
import { useGetLicense } from 'api/generated/services/licenses';
import { buildLicenseReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/license.permissions';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
import { useAppContext } from 'providers/App/App';
interface UseActiveLicenseKey {
licenseKey: string | undefined;
isLoading: boolean;
}
const useActiveLicenseKey = (): UseActiveLicenseKey => {
const { activeLicense } = useAppContext();
const permissions = useMemo(
() => (activeLicense ? [buildLicenseReadPermission(activeLicense.id)] : []),
[activeLicense],
);
const { allowed, isLoading: isAuthZLoading } = useAuthZ(permissions, {
enabled: !!activeLicense,
});
const { data, isLoading: isLicenseLoading } = useGetLicense(
{ id: activeLicense?.id ?? '' },
{ query: { enabled: !!activeLicense && allowed } },
);
return {
licenseKey: data?.data.key,
isLoading:
!!activeLicense && (isAuthZLoading || (allowed && isLicenseLoading)),
};
};
export default useActiveLicenseKey;

View File

@@ -0,0 +1,18 @@
import { useQuery, UseQueryResult } from 'react-query';
import getActive from 'api/v3/licenses/active/get';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { LicenseResModel } from 'types/api/licensesV3/getActive';
const useActiveLicenseV3 = (isLoggedIn: boolean): UseLicense =>
useQuery({
queryFn: getActive,
queryKey: [REACT_QUERY_KEY.GET_ACTIVE_LICENSE_V3],
enabled: !!isLoggedIn,
retry: false,
});
type UseLicense = UseQueryResult<SuccessResponseV2<LicenseResModel>, APIError>;
export default useActiveLicenseV3;

View File

@@ -8,19 +8,6 @@ export default {
type: 'metaresource',
allowedVerbs: ['create', 'delete', 'list', 'read', 'update'],
},
{
kind: 'license',
type: 'metaresource',
allowedVerbs: [
'attach',
'create',
'delete',
'detach',
'list',
'read',
'update',
],
},
{
kind: 'role',
type: 'role',

View File

@@ -1,6 +0,0 @@
import { buildPermission } from '../utils';
import type { BrandedPermission } from '../types';
// Resource-level — require a specific license id
export const buildLicenseReadPermission = (id: string): BrandedPermission =>
buildPermission('read', `license:${id}`);

View File

@@ -127,30 +127,30 @@ export function buildLicense(
overrides?: Partial<LicenseResModel>,
): LicenseResModel {
return {
id: 'test-license-id',
key: 'test-key',
status: LicenseStatus.VALID,
state: LicenseState.ACTIVATED,
platform: LicensePlatform.CLOUD,
eventQueue: {
createdAt: '0',
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
scheduledAt: '0',
scheduled_at: '0',
status: '',
updatedAt: '0',
updated_at: '0',
},
plan: {
id: '0',
createdAt: '0',
created_at: '0',
description: '',
isActive: true,
is_active: true,
name: '',
updatedAt: '0',
updated_at: '0',
},
freeUntil: '0',
updatedAt: '0',
validFrom: 0,
validUntil: 0,
createdAt: '0',
plan_id: '0',
free_until: '0',
updated_at: '0',
valid_from: 0,
valid_until: 0,
created_at: '0',
...overrides,
};
}

View File

@@ -22,7 +22,7 @@ import listUserPreferences from 'api/v1/user/preferences/list';
import getUserVersion from 'api/v1/version/get';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs from 'dayjs';
import useActiveLicense from 'hooks/useActiveLicense/useActiveLicense';
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
import {
IsAdminPermission,
IsEditorPermission,
@@ -210,34 +210,35 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
}
}, [userData, isFetchingUserData]);
// fetcher for the active license
// fetcher for licenses v3
const {
data: activeLicenseData,
isFetching: isFetchingActiveLicense,
error: activeLicenseFetchError,
refetch: activeLicenseRefetch,
} = useActiveLicense(isLoggedIn);
} = useActiveLicenseV3(isLoggedIn);
useEffect(() => {
if (!isFetchingActiveLicense && activeLicenseData) {
setActiveLicense(activeLicenseData);
if (!isFetchingActiveLicense && activeLicenseData && activeLicenseData.data) {
setActiveLicense(activeLicenseData.data);
const freeUntilUnix = dayjs(activeLicenseData.freeUntil).unix();
const scheduledAtUnix = dayjs(
activeLicenseData.eventQueue.scheduledAt,
).unix();
const isOnTrial = dayjs(
activeLicenseData.data.free_until || Date.now(),
).isAfter(dayjs());
const trialInfo: TrialInfo = {
trialStart: activeLicenseData.validFrom,
trialEnd: freeUntilUnix > 0 ? freeUntilUnix : dayjs().unix(),
onTrial: dayjs(activeLicenseData.freeUntil).isAfter(dayjs()),
trialStart: activeLicenseData.data.valid_from,
trialEnd: dayjs(activeLicenseData.data.free_until || Date.now()).unix(),
onTrial: isOnTrial,
workSpaceBlock:
activeLicenseData.state === LicenseState.EVALUATION_EXPIRED &&
activeLicenseData.platform === LicensePlatform.CLOUD,
activeLicenseData.data.state === LicenseState.EVALUATION_EXPIRED &&
activeLicenseData.data.platform === LicensePlatform.CLOUD,
trialConvertedToSubscription:
activeLicenseData.state !== LicenseState.ISSUED &&
activeLicenseData.state !== LicenseState.EVALUATING &&
activeLicenseData.state !== LicenseState.EVALUATION_EXPIRED,
gracePeriodEnd: scheduledAtUnix > 0 ? scheduledAtUnix : dayjs().unix(),
activeLicenseData.data.state !== LicenseState.ISSUED &&
activeLicenseData.data.state !== LicenseState.EVALUATING &&
activeLicenseData.data.state !== LicenseState.EVALUATION_EXPIRED,
gracePeriodEnd: dayjs(
activeLicenseData.data.event_queue.scheduled_at || Date.now(),
).unix(),
};
setTrialInfo(trialInfo);

View File

@@ -158,30 +158,30 @@ export function getAppContextMock(
): IAppContext {
return {
activeLicense: {
id: 'test-license-id',
eventQueue: {
createdAt: '0',
key: 'test-key',
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
scheduledAt: '0',
scheduled_at: '0',
status: '',
updatedAt: '0',
updated_at: '0',
},
state: LicenseState.ACTIVATED,
status: LicenseStatus.VALID,
platform: LicensePlatform.CLOUD,
createdAt: '0',
created_at: '0',
plan: {
id: '0',
createdAt: '0',
created_at: '0',
description: '',
isActive: true,
is_active: true,
name: '',
updatedAt: '0',
updated_at: '0',
},
freeUntil: '0',
updatedAt: '0',
validFrom: 0,
validUntil: 0,
plan_id: '0',
free_until: '0',
updated_at: '0',
valid_from: 0,
valid_until: 0,
},
trialInfo: {
trialStart: -1,

View File

@@ -0,0 +1,10 @@
import { License } from './def';
export interface Props {
key: string;
}
export interface PayloadProps {
status: string;
data: License;
}

View File

@@ -0,0 +1,8 @@
export interface License {
key: string;
ValidFrom: Date;
ValidUntil: Date;
planKey: string;
status: string;
isCurrent: boolean;
}

View File

@@ -1,59 +1,57 @@
export enum LicenseEvent {
NO_EVENT = '',
DEFAULT = 'default',
DEFAULT = 'DEFAULT',
}
export enum LicenseStatus {
SUSPENDED = 'suspended',
VALID = 'valid',
INVALID = 'invalid',
SUSPENDED = 'SUSPENDED',
VALID = 'VALID',
INVALID = 'INVALID',
}
export enum LicenseState {
DEFAULTED = 'defaulted',
ACTIVATED = 'activated',
EXPIRED = 'expired',
ISSUED = 'issued',
EVALUATING = 'evaluating',
EVALUATION_EXPIRED = 'evaluation_expired',
TERMINATED = 'terminated',
CANCELLED = 'cancelled',
DEFAULTED = 'DEFAULTED',
ACTIVATED = 'ACTIVATED',
EXPIRED = 'EXPIRED',
ISSUED = 'ISSUED',
EVALUATING = 'EVALUATING',
EVALUATION_EXPIRED = 'EVALUATION_EXPIRED',
TERMINATED = 'TERMINATED',
CANCELLED = 'CANCELLED',
}
export enum LicensePlatform {
SELF_HOSTED = 'self_hosted',
CLOUD = 'cloud',
SELF_HOSTED = 'SELF_HOSTED',
CLOUD = 'CLOUD',
}
export type LicensePlanResModel = {
id: string;
name: string;
description: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
};
export type LicenseEventQueueResModel = {
event: LicenseEvent;
status: string;
scheduledAt: string;
createdAt: string;
updatedAt: string;
scheduled_at: string;
created_at: string;
updated_at: string;
};
export type LicenseResModel = {
id: string;
key: string;
status: LicenseStatus;
state: LicenseState;
event_queue: LicenseEventQueueResModel;
platform: LicensePlatform;
plan: LicensePlanResModel;
eventQueue: LicenseEventQueueResModel;
freeUntil: string;
createdAt: string;
updatedAt: string;
validFrom: number;
validUntil: number;
created_at: string;
plan: {
created_at: string;
description: string;
is_active: boolean;
name: string;
updated_at: string;
};
plan_id: string;
free_until: string;
updated_at: string;
valid_from: number;
valid_until: number;
};
// Duplicate of old licenses API response, need to improve this later
@@ -65,3 +63,8 @@ export type TrialInfo = {
trialConvertedToSubscription: boolean;
gracePeriodEnd: number;
};
export interface PayloadProps {
data: LicenseEventQueueResModel;
status: string;
}

View File

@@ -1,165 +0,0 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/gorilla/mux"
)
func (provider *provider) addLicensingRoutes(router *mux.Router) error {
if err := router.Handle("/api/v4/licenses", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Create, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "ActivateLicense",
Tags: []string{"licenses"},
Summary: "Activate a license.",
Description: "This endpoint validates the license key with the upstream server and activates the license for the organization.",
Request: new(licensetypes.PostableLicense),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusAccepted,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.List, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "ListLicenses",
Tags: []string{"licenses"},
Summary: "List licenses.",
Description: "This endpoint lists all the licenses of the organization.",
Request: nil,
RequestContentType: "",
Response: make([]*licensetypes.GettableLicense, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryDataAccess,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses/active", handler.New(provider.authzMiddleware.OpenAccess(provider.licensingHandler.GetActive), handler.OpenAPIDef{
ID: "GetActiveLicense",
Tags: []string{"licenses"},
Summary: "Get the active license.",
Description: "This endpoint gets the active license of the organization.",
Request: nil,
RequestContentType: "",
Response: new(licensetypes.GettableActiveLicense),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusNotImplemented},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes(nil),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Get, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "GetLicense",
Tags: []string{"licenses"},
Summary: "Get a license.",
Description: "This endpoint gets the license by id.",
Request: nil,
RequestContentType: "",
Response: new(licensetypes.GettableLicenseWithKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Refresh, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "RefreshLicense",
Tags: []string{"licenses"},
Summary: "Refresh a license.",
Description: "This endpoint refreshes the active license of the organization from the upstream server.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Delete, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "DeleteLicense",
Tags: []string{"licenses"},
Summary: "Delete a license.",
Description: "This endpoint deletes the license by id. Licenses managed by SigNoz Cloud cannot be deleted.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbDelete)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbDelete,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/aiobservability"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
@@ -25,6 +24,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/savedview"
@@ -68,7 +68,6 @@ type provider struct {
authzHandler authz.Handler
rawDataExportHandler rawdataexport.Handler
zeusHandler zeus.Handler
licensingHandler licensing.Handler
querierHandler querier.Handler
serviceAccountHandler serviceaccount.Handler
serviceAccountGetter serviceaccount.Getter
@@ -82,6 +81,8 @@ type provider struct {
llmPricingRuleHandler llmpricingrule.Handler
statsHandler statsreporter.Handler
savedViewHandler savedview.Handler
quickFilterModule quickfilter.Module
quickFilterHandler quickfilter.Handler
}
func NewFactory(
@@ -107,7 +108,6 @@ func NewFactory(
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
licensingHandler licensing.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
serviceAccountGetter serviceaccount.Getter,
@@ -121,6 +121,8 @@ func NewFactory(
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
quickFilterModule quickfilter.Module,
quickFilterHandler quickfilter.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -149,7 +151,6 @@ func NewFactory(
authzHandler,
rawDataExportHandler,
zeusHandler,
licensingHandler,
querierHandler,
serviceAccountHandler,
serviceAccountGetter,
@@ -163,6 +164,8 @@ func NewFactory(
rulerHandler,
statsHandler,
savedViewHandler,
quickFilterModule,
quickFilterHandler,
)
})
}
@@ -193,7 +196,6 @@ func newProvider(
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
licensingHandler licensing.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
serviceAccountGetter serviceaccount.Getter,
@@ -207,6 +209,8 @@ func newProvider(
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
quickFilterModule quickfilter.Module,
quickFilterHandler quickfilter.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -236,7 +240,6 @@ func newProvider(
authzHandler: authzHandler,
rawDataExportHandler: rawDataExportHandler,
zeusHandler: zeusHandler,
licensingHandler: licensingHandler,
querierHandler: querierHandler,
serviceAccountHandler: serviceAccountHandler,
serviceAccountGetter: serviceAccountGetter,
@@ -250,6 +253,8 @@ func newProvider(
llmPricingRuleHandler: llmPricingRuleHandler,
statsHandler: statsHandler,
savedViewHandler: savedViewHandler,
quickFilterModule: quickFilterModule,
quickFilterHandler: quickFilterHandler,
}
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
@@ -338,10 +343,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addLicensingRoutes(router); err != nil {
return err
}
if err := provider.addZeusRoutes(router); err != nil {
return err
}
@@ -394,6 +395,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addQuickFilterRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,120 @@
package signozapiserver
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/quickfiltertypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
func (provider *provider) addQuickFilterRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/quick_filters", handler.New(
provider.authzMiddleware.CheckResources(provider.quickFilterHandler.ListQuickFiltersV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "ListQuickFilters",
Tags: []string{"quick_filter"},
Summary: "List quick filters",
Description: "Returns the org's quick filters for every source, each filter as a telemetry field key.",
Request: nil,
RequestContentType: "",
Response: new([]*quickfiltertypes.SourceFilters),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceQuickFilter.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceQuickFilter,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryDataAccess,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/quick_filters/{source}", handler.New(
provider.authzMiddleware.CheckResources(provider.quickFilterHandler.GetQuickFiltersV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "GetQuickFilters",
Tags: []string{"quick_filter"},
Summary: "Get a source's quick filters",
Description: "Returns the org's quick filters for one source, each filter as a telemetry field key.",
Request: nil,
RequestContentType: "",
Response: new(quickfiltertypes.SourceFilters),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceQuickFilter.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceQuickFilter,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("source"),
Selector: provider.quickFilterSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/quick_filters/{source}", handler.New(
provider.authzMiddleware.CheckResources(provider.quickFilterHandler.UpdateQuickFiltersV2, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "UpdateQuickFilters",
Tags: []string{"quick_filter"},
Summary: "Update quick filters",
Description: "Replaces the org's quick filters for the source named in the path.",
Request: new(quickfiltertypes.UpdatableQuickFilters),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceQuickFilter.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceQuickFilter,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("source"),
Selector: provider.quickFilterSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
return nil
}
func (provider *provider) quickFilterSelector(ctx context.Context, resource coretypes.Resource, source string, orgID valuer.UUID) ([]coretypes.Selector, error) {
validatedSource, err := quickfiltertypes.NewSource(source)
if err != nil {
return nil, err
}
// A source can have no stored row yet: GET serves it as empty and PUT
// creates it, so only the wildcard grant applies until the row exists.
quickFilter, err := provider.quickFilterModule.Get(ctx, orgID, validatedSource)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return []coretypes.Selector{resource.Type().MustSelector(coretypes.WildCardSelectorString)}, nil
}
return nil, err
}
return []coretypes.Selector{
resource.Type().MustSelector(quickFilter.ID.StringValue()),
resource.Type().MustSelector(coretypes.WildCardSelectorString),
}, nil
}

View File

@@ -1,169 +0,0 @@
package licensing
import (
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
type handler struct {
licensing Licensing
}
func NewHandler(licensing Licensing) Handler {
return &handler{licensing: licensing}
}
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
req := new(licensetypes.PostableLicense)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
err = handler.licensing.Activate(ctx, valuer.MustNewUUID(claims.OrgID), req.Key)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusAccepted, nil)
}
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
licenses, err := handler.licensing.List(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
}
gettableLicenses := make([]*licensetypes.GettableLicense, 0, len(licenses))
for _, license := range licenses {
gettableLicenses = append(gettableLicenses, licensetypes.NewGettableLicense(license))
}
render.Success(rw, http.StatusOK, gettableLicenses)
}
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
licenseID, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
license, err := handler.licensing.Get(ctx, valuer.MustNewUUID(claims.OrgID), licenseID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, licensetypes.NewGettableLicenseWithKey(license))
}
func (handler *handler) Refresh(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
licenseID, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
activeLicense, err := handler.licensing.GetActive(ctx, orgID)
if err != nil {
render.Error(rw, err)
return
}
if activeLicense.ID != licenseID {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "only the active license %s can be refreshed", activeLicense.ID.StringValue()))
return
}
if err := handler.licensing.Refresh(ctx, orgID); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
licenseID, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
if err := handler.licensing.Delete(ctx, valuer.MustNewUUID(claims.OrgID), licenseID); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) GetActive(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
license, err := handler.licensing.GetActive(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, licensetypes.NewGettableActiveLicense(license))
}

View File

@@ -21,16 +21,10 @@ type Licensing interface {
// Validate validates the license with the upstream server
Validate(ctx context.Context) error
// Activate validates the key with the upstream server and enables the license
// 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)
// Get fetches the license by id in org
Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.License, error)
// List fetches all the licenses in org
List(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.License, error)
// Delete deletes the license by id in org, cloud licenses cannot be deleted
Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) 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
@@ -44,20 +38,10 @@ type Licensing interface {
}
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)
}
type Handler interface {
Create(http.ResponseWriter, *http.Request)
List(http.ResponseWriter, *http.Request)
Get(http.ResponseWriter, *http.Request)
Refresh(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
GetActive(http.ResponseWriter, *http.Request)
}

View File

@@ -14,6 +14,18 @@ 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"))
}

View File

@@ -39,18 +39,6 @@ func (provider *noopLicensing) Activate(ctx context.Context, organizationID valu
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "fetching license is not supported")
}
func (provider *noopLicensing) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.License, error) {
return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "fetching license is not supported")
}
func (provider *noopLicensing) List(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.License, error) {
return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "listing licenses is not supported")
}
func (provider *noopLicensing) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "deleting license is not supported")
}
func (provider *noopLicensing) Validate(ctx context.Context) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "validating license is not supported")
}

View File

@@ -4,10 +4,13 @@ import (
"encoding/json"
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/quickfiltertypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -20,6 +23,90 @@ func NewHandler(module quickfilter.Module) quickfilter.Handler {
return &handler{module: module}
}
// legacySourceFilters is the v1 API shape: filters as v3 attribute keys,
// with the source still spelled "signal" on the wire.
type legacySourceFilters struct {
Source quickfiltertypes.Source `json:"signal"`
Filters []v3.AttributeKey `json:"filters"`
}
// newTelemetryFieldKeysFromLegacy converts a v1 write payload with the same
// normalizations as the storage migration: alias contexts, numerics to number.
// The v1 shape carries no per filter signal, so meter keys get it restored.
func newTelemetryFieldKeysFromLegacy(source quickfiltertypes.Source, filters []v3.AttributeKey) ([]telemetrytypes.TelemetryFieldKey, error) {
var fieldSignal telemetrytypes.Signal
if source == quickfiltertypes.SourceMeter {
fieldSignal = telemetrytypes.SignalMetrics
}
fieldKeys := make([]telemetrytypes.TelemetryFieldKey, 0, len(filters))
for _, filter := range filters {
if err := filter.Validate(); err != nil {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid filter: %v", err)
}
fieldContext, ok := telemetrytypes.FieldContextFromText(string(filter.Type))
if !ok {
fieldContext = telemetrytypes.FieldContextUnspecified
}
var fieldDataType telemetrytypes.FieldDataType
if err := fieldDataType.Scan(string(filter.DataType)); err != nil {
fieldDataType = telemetrytypes.FieldDataTypeUnspecified
}
if fieldDataType == telemetrytypes.FieldDataTypeInt64 {
fieldDataType = telemetrytypes.FieldDataTypeNumber
}
fieldKeys = append(fieldKeys, telemetrytypes.TelemetryFieldKey{
Name: filter.Key,
Signal: fieldSignal,
FieldContext: fieldContext,
FieldDataType: fieldDataType,
})
}
return fieldKeys, nil
}
// newLegacySourceFilters renders stored telemetry field keys
// back into the v1 shape, restoring the legacy spellings v1 clients expect.
func newLegacySourceFilters(sourceFilters *quickfiltertypes.SourceFilters) *legacySourceFilters {
filters := make([]v3.AttributeKey, 0, len(sourceFilters.Filters))
for _, fieldKey := range sourceFilters.Filters {
// Only tag and resource exist in the v3 enum; other contexts render as
// unspecified so v1 clients never see spellings their queries can't use.
var attributeType v3.AttributeKeyType
switch fieldKey.FieldContext {
case telemetrytypes.FieldContextAttribute:
attributeType = v3.AttributeKeyTypeTag
case telemetrytypes.FieldContextResource:
attributeType = v3.AttributeKeyTypeResource
default:
attributeType = v3.AttributeKeyTypeUnspecified
}
var dataType v3.AttributeKeyDataType
switch fieldKey.FieldDataType {
case telemetrytypes.FieldDataTypeNumber:
dataType = v3.AttributeKeyDataTypeFloat64
default:
dataType = v3.AttributeKeyDataType(fieldKey.FieldDataType.StringValue())
}
filters = append(filters, v3.AttributeKey{
Key: fieldKey.Name,
Type: attributeType,
DataType: dataType,
})
}
return &legacySourceFilters{
Source: sourceFilters.Source,
Filters: filters,
}
}
func (handler *handler) GetQuickFilters(rw http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
@@ -27,13 +114,41 @@ func (handler *handler) GetQuickFilters(rw http.ResponseWriter, r *http.Request)
return
}
filters, err := handler.module.GetQuickFilters(r.Context(), valuer.MustNewUUID(claims.OrgID))
filters, err := handler.module.GetQuickFilters(r.Context(), valuer.MustNewUUID(claims.OrgID), quickfiltertypes.Source{})
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, filters)
legacyFilters := make([]*legacySourceFilters, 0, len(filters))
for _, sourceFilters := range filters {
legacyFilters = append(legacyFilters, newLegacySourceFilters(sourceFilters))
}
render.Success(rw, http.StatusOK, legacyFilters)
}
func (handler *handler) GetSourceFilters(rw http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
source := mux.Vars(r)["signal"]
validatedSource, err := quickfiltertypes.NewSource(source)
if err != nil {
render.Error(rw, err)
return
}
filters, err := handler.module.GetQuickFilters(r.Context(), valuer.MustNewUUID(claims.OrgID), validatedSource)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, newLegacySourceFilters(handler.sourceFiltersOrEmpty(filters, validatedSource)))
}
func (handler *handler) UpdateQuickFilters(rw http.ResponseWriter, r *http.Request) {
@@ -43,14 +158,19 @@ func (handler *handler) UpdateQuickFilters(rw http.ResponseWriter, r *http.Reque
return
}
var req quickfiltertypes.UpdatableQuickFilters
decodeErr := json.NewDecoder(r.Body).Decode(&req)
if decodeErr != nil {
render.Error(rw, decodeErr)
var req legacySourceFilters
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, err)
return
}
err = handler.module.UpdateQuickFilters(r.Context(), valuer.MustNewUUID(claims.OrgID), req.Signal, req.Filters)
fieldKeys, err := newTelemetryFieldKeysFromLegacy(req.Source, req.Filters)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.UpsertQuickFilters(r.Context(), valuer.MustNewUUID(claims.OrgID), req.Source, fieldKeys)
if err != nil {
render.Error(rw, err)
return
@@ -59,21 +179,14 @@ func (handler *handler) UpdateQuickFilters(rw http.ResponseWriter, r *http.Reque
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) GetSignalFilters(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) ListQuickFiltersV2(rw http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
signal := mux.Vars(r)["signal"]
validatedSignal, err := quickfiltertypes.NewSignal(signal)
if err != nil {
render.Error(rw, err)
return
}
filters, err := handler.module.GetSignalFilters(r.Context(), valuer.MustNewUUID(claims.OrgID), validatedSignal)
filters, err := handler.module.GetQuickFilters(r.Context(), valuer.MustNewUUID(claims.OrgID), quickfiltertypes.Source{})
if err != nil {
render.Error(rw, err)
return
@@ -81,3 +194,64 @@ func (handler *handler) GetSignalFilters(rw http.ResponseWriter, r *http.Request
render.Success(rw, http.StatusOK, filters)
}
func (handler *handler) UpdateQuickFiltersV2(rw http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
source := mux.Vars(r)["source"]
validatedSource, err := quickfiltertypes.NewSource(source)
if err != nil {
render.Error(rw, err)
return
}
var req quickfiltertypes.UpdatableQuickFilters
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, err)
return
}
err = handler.module.UpsertQuickFilters(r.Context(), valuer.MustNewUUID(claims.OrgID), validatedSource, req.Filters)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) GetQuickFiltersV2(rw http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
source := mux.Vars(r)["source"]
validatedSource, err := quickfiltertypes.NewSource(source)
if err != nil {
render.Error(rw, err)
return
}
filters, err := handler.module.GetQuickFilters(r.Context(), valuer.MustNewUUID(claims.OrgID), validatedSource)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, handler.sourceFiltersOrEmpty(filters, validatedSource))
}
// sourceFiltersOrEmpty keeps the single-source response contract: a source
// with no stored filters is served as an empty filter list, not an error.
func (handler *handler) sourceFiltersOrEmpty(filters []*quickfiltertypes.SourceFilters, source quickfiltertypes.Source) *quickfiltertypes.SourceFilters {
if len(filters) == 0 {
return quickfiltertypes.NewSourceFiltersFromSource(source)
}
return filters[0]
}

View File

@@ -0,0 +1,62 @@
package implquickfilter
import (
"testing"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/quickfiltertypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewTelemetryFieldKeysFromLegacy(t *testing.T) {
fieldKeys, err := newTelemetryFieldKeysFromLegacy(quickfiltertypes.SourceTraces, []v3.AttributeKey{
{Key: "service.name", Type: v3.AttributeKeyTypeResource, DataType: v3.AttributeKeyDataTypeString},
{Key: "http.method", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeString},
{Key: "duration_nano", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeFloat64},
{Key: "code_line", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeInt64},
})
require.NoError(t, err)
require.Len(t, fieldKeys, 4)
assert.Equal(t, telemetrytypes.TelemetryFieldKey{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString}, fieldKeys[0])
assert.Equal(t, telemetrytypes.FieldContextAttribute, fieldKeys[1].FieldContext)
assert.Equal(t, telemetrytypes.FieldDataTypeNumber, fieldKeys[2].FieldDataType)
assert.Equal(t, telemetrytypes.FieldDataTypeNumber, fieldKeys[3].FieldDataType)
t.Run("meter writes restore the per-filter telemetry signal", func(t *testing.T) {
fieldKeys, err := newTelemetryFieldKeysFromLegacy(quickfiltertypes.SourceMeter, []v3.AttributeKey{
{Key: "host.name", DataType: v3.AttributeKeyDataTypeString},
})
require.NoError(t, err)
require.Len(t, fieldKeys, 1)
assert.Equal(t, telemetrytypes.SignalMetrics, fieldKeys[0].Signal)
})
t.Run("rejects a filter without a key", func(t *testing.T) {
_, err := newTelemetryFieldKeysFromLegacy(quickfiltertypes.SourceTraces, []v3.AttributeKey{{DataType: v3.AttributeKeyDataTypeString}})
require.Error(t, err)
})
}
func TestNewLegacySourceFilters(t *testing.T) {
legacy := newLegacySourceFilters(&quickfiltertypes.SourceFilters{
Source: quickfiltertypes.SourceLogs,
Filters: []telemetrytypes.TelemetryFieldKey{
{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "http.method", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "duration_nano", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber},
{Name: "severity_text", FieldContext: telemetrytypes.FieldContextLog, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "host.name", Signal: telemetrytypes.SignalMetrics},
},
})
assert.Equal(t, quickfiltertypes.SourceLogs, legacy.Source)
require.Len(t, legacy.Filters, 5)
assert.Equal(t, v3.AttributeKey{Key: "service.name", Type: v3.AttributeKeyTypeResource, DataType: v3.AttributeKeyDataTypeString}, legacy.Filters[0])
assert.Equal(t, v3.AttributeKeyTypeTag, legacy.Filters[1].Type)
assert.Equal(t, v3.AttributeKeyDataTypeFloat64, legacy.Filters[2].DataType)
assert.Equal(t, v3.AttributeKeyTypeUnspecified, legacy.Filters[3].Type, "contexts outside the v3 enum must render as unspecified")
assert.Equal(t, v3.AttributeKey{Key: "host.name"}, legacy.Filters[4])
}

View File

@@ -2,12 +2,11 @@ package implquickfilter
import (
"context"
"encoding/json"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/quickfiltertypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -19,91 +18,54 @@ func NewModule(store quickfiltertypes.QuickFilterStore) quickfilter.Module {
return &module{store: store}
}
// GetQuickFilters returns all quick filters for an organization.
func (module *module) GetQuickFilters(ctx context.Context, orgID valuer.UUID) ([]*quickfiltertypes.SignalFilters, error) {
storedFilters, err := module.store.Get(ctx, orgID)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error fetching organization filters")
}
result := make([]*quickfiltertypes.SignalFilters, 0, len(storedFilters))
for _, storedFilter := range storedFilters {
signalFilter, err := quickfiltertypes.NewSignalFilterFromStorableQuickFilter(storedFilter)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error processing filter for signal: %s", storedFilter.Signal)
}
result = append(result, signalFilter)
}
return result, nil
func (module *module) Get(ctx context.Context, orgID valuer.UUID, source quickfiltertypes.Source) (*quickfiltertypes.StorableQuickFilter, error) {
return module.store.GetBySource(ctx, orgID, source.StringValue())
}
// GetSignalFilters returns quick filters for a specific signal in an organization.
func (m *module) GetSignalFilters(ctx context.Context, orgID valuer.UUID, signal quickfiltertypes.Signal) (*quickfiltertypes.SignalFilters, error) {
storedFilter, err := m.store.GetBySignal(ctx, orgID, signal.StringValue())
// GetQuickFilters returns quick filters for a source, or for every source when source is zero.
func (module *module) GetQuickFilters(ctx context.Context, orgID valuer.UUID, source quickfiltertypes.Source) ([]*quickfiltertypes.SourceFilters, error) {
if source.IsZero() {
storedFilters, err := module.store.Get(ctx, orgID)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error fetching organization filters")
}
result := make([]*quickfiltertypes.SourceFilters, 0, len(storedFilters))
for _, storedFilter := range storedFilters {
sourceFilter, err := quickfiltertypes.NewSourceFilterFromStorableQuickFilter(storedFilter)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error processing filter for source: %s", storedFilter.Source)
}
result = append(result, sourceFilter)
}
return result, nil
}
storedFilter, err := module.store.GetBySource(ctx, orgID, source.StringValue())
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return []*quickfiltertypes.SourceFilters{}, nil
}
return nil, err
}
// If no filter exists for this signal, return empty filters with the requested signal
if storedFilter == nil {
return &quickfiltertypes.SignalFilters{
Signal: signal,
Filters: []v3.AttributeKey{},
}, nil
}
// Convert stored filter to signal filter
signalFilter, err := quickfiltertypes.NewSignalFilterFromStorableQuickFilter(storedFilter)
sourceFilter, err := quickfiltertypes.NewSourceFilterFromStorableQuickFilter(storedFilter)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error processing filter for signal: %s", storedFilter.Signal)
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error processing filter for source: %s", storedFilter.Source)
}
return signalFilter, nil
return []*quickfiltertypes.SourceFilters{sourceFilter}, nil
}
// UpdateQuickFilters updates quick filters for a specific signal in an organization.
func (module *module) UpdateQuickFilters(ctx context.Context, orgID valuer.UUID, signal quickfiltertypes.Signal, filters []v3.AttributeKey) error {
// Validate each filter
for _, filter := range filters {
if err := filter.Validate(); err != nil {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid filter: %v", err)
}
}
// Marshal filters to JSON
filterJSON, err := json.Marshal(filters)
// UpsertQuickFilters replaces quick filters for a specific source in an organization, creating them if absent.
func (module *module) UpsertQuickFilters(ctx context.Context, orgID valuer.UUID, source quickfiltertypes.Source, filters []telemetrytypes.TelemetryFieldKey) error {
filter, err := quickfiltertypes.NewStorableQuickFilter(orgID, source, filters)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error marshalling filters")
}
// Check if filter exists
existingFilter, err := module.store.GetBySignal(ctx, orgID, signal.StringValue())
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error checking existing filters")
}
var filter *quickfiltertypes.StorableQuickFilter
if existingFilter != nil {
// Update in place
if err := existingFilter.Update(filterJSON); err != nil {
return errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "error updating existing filter")
}
filter = existingFilter
} else {
// Create new
filter, err = quickfiltertypes.NewStorableQuickFilter(orgID, signal, filterJSON)
if err != nil {
return errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "error creating new filter")
}
}
// Persist filter
if err := module.store.Upsert(ctx, filter); err != nil {
return err
}
return nil
return module.store.Upsert(ctx, filter)
}
func (module *module) SetDefaultConfig(ctx context.Context, orgID valuer.UUID) error {

View File

@@ -26,7 +26,7 @@ func (s *store) Get(ctx context.Context, orgID valuer.UUID) ([]*quickfiltertypes
NewSelect().
Model(&filters).
Where("org_id = ?", orgID).
Order("signal ASC").
Order("source ASC").
Scan(ctx)
if err != nil {
@@ -36,7 +36,7 @@ func (s *store) Get(ctx context.Context, orgID valuer.UUID) ([]*quickfiltertypes
return filters, nil
}
func (s *store) GetBySignal(ctx context.Context, orgID valuer.UUID, signal string) (*quickfiltertypes.StorableQuickFilter, error) {
func (s *store) GetBySource(ctx context.Context, orgID valuer.UUID, source string) (*quickfiltertypes.StorableQuickFilter, error) {
filter := new(quickfiltertypes.StorableQuickFilter)
err := s.store.
@@ -44,12 +44,12 @@ func (s *store) GetBySignal(ctx context.Context, orgID valuer.UUID, signal strin
NewSelect().
Model(filter).
Where("org_id = ?", orgID).
Where("signal = ?", signal).
Where("source = ?", source).
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, s.store.WrapNotFoundErrf(err, errors.CodeNotFound, "No rows found for org_id: "+orgID.StringValue()+" signal: "+signal)
return nil, s.store.WrapNotFoundErrf(err, errors.CodeNotFound, "No rows found for org_id: "+orgID.StringValue()+" source: "+source)
}
return nil, err
}
@@ -62,7 +62,7 @@ func (s *store) Upsert(ctx context.Context, filter *quickfiltertypes.StorableQui
BunDB().
NewInsert().
Model(filter).
On("CONFLICT (id) DO UPDATE").
On("CONFLICT (org_id, source) DO UPDATE").
Set("filter = EXCLUDED.filter").
Set("updated_at = EXCLUDED.updated_at").
Exec(ctx)
@@ -78,7 +78,7 @@ func (s *store) Create(ctx context.Context, filters []*quickfiltertypes.Storable
BunDBCtx(ctx).
NewInsert().
Model(&filters).
On("CONFLICT (org_id, signal) DO NOTHING").
On("CONFLICT (org_id, source) DO NOTHING").
Exec(ctx)
if err != nil {

View File

@@ -4,20 +4,27 @@ import (
"context"
"net/http"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/quickfiltertypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
GetQuickFilters(ctx context.Context, orgID valuer.UUID) ([]*quickfiltertypes.SignalFilters, error)
UpdateQuickFilters(ctx context.Context, orgID valuer.UUID, signal quickfiltertypes.Signal, filters []v3.AttributeKey) error
GetSignalFilters(ctx context.Context, orgID valuer.UUID, signal quickfiltertypes.Signal) (*quickfiltertypes.SignalFilters, error)
// Get returns the stored quick filter row for a source.
Get(ctx context.Context, orgID valuer.UUID, source quickfiltertypes.Source) (*quickfiltertypes.StorableQuickFilter, error)
// GetQuickFilters returns quick filters for a source, or for every source when source is zero.
GetQuickFilters(ctx context.Context, orgID valuer.UUID, source quickfiltertypes.Source) ([]*quickfiltertypes.SourceFilters, error)
UpsertQuickFilters(ctx context.Context, orgID valuer.UUID, source quickfiltertypes.Source, filters []telemetrytypes.TelemetryFieldKey) error
SetDefaultConfig(ctx context.Context, orgID valuer.UUID) error
}
type Handler interface {
// Legacy v1 endpoints, served by converting to and from the v3 attribute key shape.
GetQuickFilters(http.ResponseWriter, *http.Request)
UpdateQuickFilters(http.ResponseWriter, *http.Request)
GetSignalFilters(http.ResponseWriter, *http.Request)
GetSourceFilters(http.ResponseWriter, *http.Request)
ListQuickFiltersV2(http.ResponseWriter, *http.Request)
GetQuickFiltersV2(http.ResponseWriter, *http.Request)
UpdateQuickFiltersV2(http.ResponseWriter, *http.Request)
}

View File

@@ -450,13 +450,20 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/disks", am.ViewAccess(aH.getDisks)).Methods(http.MethodGet)
// Quick Filters
// Quick Filters (v1 routes serve the legacy v3 shape; v2 lives in signozapiserver)
router.HandleFunc("/api/v1/orgs/me/filters", am.ViewAccess(aH.Signoz.Handlers.QuickFilter.GetQuickFilters)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/orgs/me/filters/{signal}", am.ViewAccess(aH.Signoz.Handlers.QuickFilter.GetSignalFilters)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/orgs/me/filters/{signal}", am.ViewAccess(aH.Signoz.Handlers.QuickFilter.GetSourceFilters)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/orgs/me/filters", am.AdminAccess(aH.Signoz.Handlers.QuickFilter.UpdateQuickFilters)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/register", am.OpenAccess(aH.registerUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, []any{})
})).Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
aH.LicensingAPI.Activate(rw, req)
})).Methods(http.MethodGet)
router.HandleFunc("/api/v1/span_percentile", am.ViewAccess(aH.Signoz.Handlers.SpanPercentile.GetSpanPercentileDetails)).Methods(http.MethodPost)
// Query Filter Analyzer api used to extract metric names and grouping columns from a query

View File

@@ -77,7 +77,6 @@ type Handlers struct {
AIObservability aiobservability.Handler
AuthzHandler authz.Handler
ZeusHandler zeus.Handler
LicensingHandler licensing.Handler
QuerierHandler querier.Handler
ServiceAccountHandler serviceaccount.Handler
RegistryHandler factory.Handler
@@ -96,7 +95,7 @@ func NewHandlers(
providerSettings factory.ProviderSettings,
analytics analytics.Analytics,
querierHandler querier.Handler,
licensingService licensing.Licensing,
licensing licensing.Licensing,
global global.Global,
flaggerService flagger.Flagger,
gatewayService gateway.Gateway,
@@ -126,8 +125,7 @@ func NewHandlers(
Fields: implfields.NewHandler(providerSettings, telemetryMetadataStore),
AIObservability: implaiobservability.NewHandler(telemetryMetadataStore),
AuthzHandler: signozauthzapi.NewHandler(authz),
ZeusHandler: zeus.NewHandler(zeusService, licensingService),
LicensingHandler: licensing.NewHandler(licensingService),
ZeusHandler: zeus.NewHandler(zeusService, licensing),
QuerierHandler: querierHandler,
ServiceAccountHandler: implserviceaccount.NewHandler(modules.ServiceAccount, modules.ServiceAccountGetter),
RegistryHandler: registryHandler,

View File

@@ -17,7 +17,6 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/aiobservability"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
@@ -30,6 +29,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/savedview"
@@ -81,7 +81,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ authz.Handler }{},
struct{ rawdataexport.Handler }{},
struct{ zeus.Handler }{},
struct{ licensing.Handler }{},
struct{ querier.Handler }{},
struct{ serviceaccount.Handler }{},
struct{ serviceaccount.Getter }{},
@@ -95,6 +94,8 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ ruler.Handler }{},
struct{ statsreporter.Handler }{},
struct{ savedview.Handler }{},
struct{ quickfilter.Module }{},
struct{ quickfilter.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -245,7 +245,8 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewMigrateLambdaDashboardsFactory(),
sqlmigration.NewAddAuthDomainTuplesFactory(sqlstore),
sqlmigration.NewAddDeploymentHostTuplesFactory(sqlstore),
sqlmigration.NewAddLicenseTuplesFactory(sqlstore),
sqlmigration.NewMigrateQuickFiltersFactory(sqlstore),
sqlmigration.NewAddQuickFilterTuplesFactory(sqlstore),
)
}
@@ -336,7 +337,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.AuthzHandler,
handlers.RawDataExport,
handlers.ZeusHandler,
handlers.LicensingHandler,
handlers.QuerierHandler,
handlers.ServiceAccountHandler,
modules.ServiceAccountGetter,
@@ -350,6 +350,8 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.RulerHandler,
handlers.StatsHandler,
handlers.SavedView,
modules.QuickFilter,
handlers.QuickFilter,
),
)
}

View File

@@ -73,7 +73,7 @@ func (migration *updateApiMonitoringFilters) Up(ctx context.Context, db *bun.DB)
// Find the API monitoring filter from the storable quick filters
var apiMonitoringFilterJSON string
for _, filter := range storableQuickFilters {
if filter.Signal == quickfiltertypes.SignalApiMonitoring {
if filter.Source.StringValue() == "api_monitoring" {
apiMonitoringFilterJSON = filter.Filter
break
}
@@ -83,7 +83,7 @@ func (migration *updateApiMonitoringFilters) Up(ctx context.Context, db *bun.DB)
_, err = tx.NewUpdate().
Table("quick_filter").
Set("filter = ?, updated_at = ?", apiMonitoringFilterJSON, time.Now()).
Where("signal = ? AND org_id = ?", quickfiltertypes.SignalApiMonitoring, orgID).
Where("signal = ? AND org_id = ?", "api_monitoring", orgID).
Exec(ctx)
if err != nil {

View File

@@ -0,0 +1,174 @@
package sqlmigration
import (
"context"
"encoding/json"
"log/slog"
"strings"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
)
type storableQuickFilterRow struct {
bun.BaseModel `bun:"table:quick_filter"`
ID string `bun:"id,pk"`
Filter string `bun:"filter"`
}
// legacyQuickFilterEntry carries both shapes a stored entry can be in: the
// legacy key/type/dataType shape and the current name-carrying shape.
type legacyQuickFilterEntry struct {
Name string `json:"name"`
Key string `json:"key"`
Type string `json:"type"`
DataType string `json:"dataType"`
Signal string `json:"signal"`
}
// quickFilterLegacyTypeToFieldContext maps the v3 attribute key types the v1
// write path could store. Materialized top-level fields carried no type, and
// anything unknown (e.g. "Sum" in the old meter defaults) normalizes to
// unspecified, matching what the v1 write path does at runtime.
var quickFilterLegacyTypeToFieldContext = map[string]string{
"tag": "attribute",
"resource": "resource",
"scope": "scope",
}
// quickFilterLegacyDataTypeToFieldDataType maps the v3 attribute key data
// types the v1 write path could store, with numerics collapsed to number,
// matching the fields API and the v1 write path.
var quickFilterLegacyDataTypeToFieldDataType = map[string]string{
"string": "string",
"bool": "bool",
"int64": "number",
"float64": "number",
}
// quickFilterFieldDataType resolves legacy datatype spellings, with unknowns
// normalized to unspecified.
func quickFilterFieldDataType(legacyDataType string) string {
return quickFilterLegacyDataTypeToFieldDataType[strings.ToLower(strings.TrimSpace(legacyDataType))]
}
// quickFilterFieldContext resolves legacy type spellings, with unknowns
// normalized to unspecified.
func quickFilterFieldContext(legacyType string) string {
return quickFilterLegacyTypeToFieldContext[strings.ToLower(strings.TrimSpace(legacyType))]
}
type migrateQuickFilters struct {
sqlstore sqlstore.SQLStore
settings factory.ProviderSettings
}
func NewMigrateQuickFiltersFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("migrate_quick_filters"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &migrateQuickFilters{sqlstore: sqlstore, settings: ps}, nil
})
}
func (migration *migrateQuickFilters) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *migrateQuickFilters) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var rows []*storableQuickFilterRow
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
return err
}
var migrated, skipped int
for _, row := range rows {
migratedFilter, changed, ok := migrateQuickFilterEntries(row.Filter)
if !ok {
migration.settings.Logger.WarnContext(ctx, "quick filter could not be parsed, leaving it untouched", slog.String("quick_filter_id", row.ID), slog.String("raw_filter", row.Filter))
skipped++
continue
}
if !changed {
continue
}
migrated++
if _, err := tx.NewUpdate().Model((*storableQuickFilterRow)(nil)).Set("filter = ?", migratedFilter).Where("id = ?", row.ID).Exec(ctx); err != nil {
return err
}
}
migration.settings.Logger.InfoContext(ctx, "migrated quick filters to telemetry field keys", slog.Int("total", len(rows)), slog.Int("migrated", migrated), slog.Int("skipped", skipped))
if _, err := migration.sqlstore.Dialect().RenameColumn(ctx, tx, "quick_filter", "signal", "source"); err != nil {
return err
}
return tx.Commit()
}
func (migration *migrateQuickFilters) Down(context.Context, *bun.DB) error {
return nil
}
// migrateQuickFilterEntries rewrites a stored filter list from the legacy
// key/dataType/type shape to telemetry field keys; ok=false means unparseable.
func migrateQuickFilterEntries(filter string) (migrated string, changed bool, ok bool) {
var entriesRaw []json.RawMessage
if err := json.Unmarshal([]byte(filter), &entriesRaw); err != nil {
return "", false, false
}
migratedEntries := make([]json.RawMessage, 0, len(entriesRaw))
for _, rawEntry := range entriesRaw {
var entry legacyQuickFilterEntry
if err := json.Unmarshal(rawEntry, &entry); err != nil {
// Some stored entries are plain strings rather than objects; treat
// the string as the filter key name, dropping empty ones.
var name string
if err := json.Unmarshal(rawEntry, &name); err != nil {
return "", false, false
}
entry = legacyQuickFilterEntry{Key: name}
}
switch {
case entry.Name != "":
migratedEntries = append(migratedEntries, rawEntry)
case entry.Key != "":
migratedJSON, err := marshalUnescaped(telemetryFieldKeyOutput{
Name: entry.Key,
Signal: entry.Signal,
FieldContext: quickFilterFieldContext(entry.Type),
FieldDataType: quickFilterFieldDataType(entry.DataType),
})
if err != nil {
return "", false, false
}
migratedEntries = append(migratedEntries, migratedJSON)
changed = true
default:
changed = true
}
}
if !changed {
return "", false, true
}
migratedJSON, err := marshalUnescaped(migratedEntries)
if err != nil {
return "", false, false
}
return string(migratedJSON), true, true
}

View File

@@ -3,34 +3,32 @@ package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addLicenseTuples struct {
type addQuickFilterTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddLicenseTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_license_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addLicenseTuples{sqlstore: sqlstore}, nil
func NewAddQuickFilterTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_quick_filter_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addQuickFilterTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addLicenseTuples) Register(migrations *migrate.Migrations) error {
func (migration *addQuickFilterTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addLicenseTuples) Up(ctx context.Context, db *bun.DB) error {
func (migration *addQuickFilterTuples) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
@@ -54,12 +52,17 @@ func (migration *addLicenseTuples) Up(ctx context.Context, db *bun.DB) error {
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
// quick-filter moved from the legacy ViewAccess/AdminAccess role gate to
// CheckResources, which on enterprise requires real tuples -- existing orgs
// never had these written, only new orgs get them from the registry at bootstrap.
tuples := []migrationTuple{
{authtypes.SigNozAdminRoleName, "metaresource", "license", "create"},
{authtypes.SigNozAdminRoleName, "metaresource", "license", "read"},
{authtypes.SigNozAdminRoleName, "metaresource", "license", "update"},
{authtypes.SigNozAdminRoleName, "metaresource", "license", "delete"},
{authtypes.SigNozAdminRoleName, "metaresource", "license", "list"},
{authtypes.SigNozAdminRoleName, "metaresource", "quick-filter", "read"},
{authtypes.SigNozAdminRoleName, "metaresource", "quick-filter", "update"},
{authtypes.SigNozAdminRoleName, "metaresource", "quick-filter", "list"},
{authtypes.SigNozEditorRoleName, "metaresource", "quick-filter", "read"},
{authtypes.SigNozEditorRoleName, "metaresource", "quick-filter", "list"},
{authtypes.SigNozViewerRoleName, "metaresource", "quick-filter", "read"},
{authtypes.SigNozViewerRoleName, "metaresource", "quick-filter", "list"},
}
for _, orgID := range orgIDs {
@@ -128,32 +131,9 @@ func (migration *addLicenseTuples) Up(ctx context.Context, db *bun.DB) error {
}
}
managedRoleGroups := make(map[string]string, len(coretypes.ManagedRoleToTransactions))
for roleName, transactions := range coretypes.ManagedRoleToTransactions {
data, err := json.Marshal(authtypes.NewTransactionGroupsFromTransactions(transactions))
if err != nil {
return err
}
managedRoleGroups[roleName] = string(data)
}
for _, orgID := range orgIDs {
for roleName, data := range managedRoleGroups {
if _, err := tx.NewUpdate().
Model(new(roles)).
Set("transaction_groups = ?", data).
Where("org_id = ?", orgID).
Where("type = ?", authtypes.RoleTypeManaged.StringValue()).
Where("name = ?", roleName).
Exec(ctx); err != nil {
return err
}
}
}
return tx.Commit()
}
func (migration *addLicenseTuples) Down(context.Context, *bun.DB) error {
func (migration *addQuickFilterTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -64,10 +64,11 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorPassword}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorPassword}, WildCardSelectorString)},
// license — admin only.
// Uniform LCRUD shape served by /api/v4/licenses: create = Activate,
// update = Refresh, read = Get (includes the key), list, delete (non-cloud
// licenses only). GET /api/v4/licenses/active is OpenAccess, so the read
// grant is not enforced there.
// Uniform LCRUD shape; actual ee routes are POST /api/v3/licenses (create
// = Activate), PUT /api/v3/licenses (update = Refresh), GET
// /api/v3/licenses/active (read; currently exposed as ViewAccess on the
// route side). delete and list are placeholders for shape parity, no
// route serves them today.
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},

View File

@@ -62,7 +62,7 @@ var (
ResourceMetaResourcePipeline = NewResourceMetaResource(KindPipeline)
ResourceMetaResourceUserPreference = NewResourceMetaResource(KindUserPreference)
ResourceMetaResourceOrgPreference = NewResourceMetaResource(KindOrgPreference)
ResourceMetaResourceQuickFilter = NewResourceMetaResource(KindQuickFilter)
ResourceMetaResourceQuickFilter = NewResourceMetaResource(KindQuickFilter, VerbList, VerbRead, VerbUpdate)
ResourceMetaResourceTTLSetting = NewResourceMetaResource(KindTTLSetting)
ResourceMetaResourceRule = NewResourceMetaResource(KindRule)
ResourceMetaResourcePlannedMaintenance = NewResourceMetaResource(KindPlannedMaintenance)

View File

@@ -3,19 +3,15 @@ package licensetypes
import (
"context"
"encoding/json"
"reflect"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
var (
ErrCodeCloudLicenseOperationUnsupported = errors.MustNewCode("cloud_license_operation_unsupported")
)
type StorableLicense struct {
bun.BaseModel `bun:"table:license"`
@@ -32,12 +28,10 @@ type License struct {
ID valuer.UUID
Key string
Data map[string]interface{}
Plan LicensePlan
EventQueue LicenseEventQueue
PlanName valuer.String
Features []*Feature
Status valuer.String
State valuer.String
Platform valuer.String
State string
FreeUntil time.Time
ValidFrom int64
ValidUntil int64
@@ -47,51 +41,28 @@ type License struct {
OrganizationID valuer.UUID
}
type LicensePlan struct {
ID valuer.UUID `json:"id" required:"true"`
Name valuer.String `json:"name" required:"true"`
Description string `json:"description" required:"true"`
IsActive bool `json:"isActive" required:"true"`
CreatedAt time.Time `json:"createdAt" required:"true"`
UpdatedAt time.Time `json:"updatedAt" required:"true"`
}
type LicenseEventQueue struct {
Event valuer.String `json:"event" required:"true"`
Status valuer.String `json:"status" required:"true"`
ScheduledAt time.Time `json:"scheduledAt" required:"true"`
CreatedAt time.Time `json:"createdAt" required:"true"`
UpdatedAt time.Time `json:"updatedAt" required:"true"`
}
type GettableLicense struct {
ID valuer.UUID `json:"id" required:"true"`
ValidFrom int64 `json:"validFrom" required:"true"`
ValidUntil int64 `json:"validUntil" required:"true"`
Status valuer.String `json:"status" required:"true"`
State valuer.String `json:"state" required:"true"`
Platform valuer.String `json:"platform" required:"true"`
FreeUntil time.Time `json:"freeUntil" required:"true"`
CreatedAt time.Time `json:"createdAt" required:"true"`
UpdatedAt time.Time `json:"updatedAt" required:"true"`
Plan LicensePlan `json:"plan" required:"true"`
Features []*Feature `json:"features" required:"true"`
EventQueue LicenseEventQueue `json:"eventQueue" required:"true"`
}
type GettableLicenseWithKey struct {
GettableLicense
Key string `json:"key" required:"true"`
}
type GettableActiveLicense struct {
GettableLicense
}
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{
@@ -135,109 +106,263 @@ func GetActiveLicenseFromStorableLicenses(storableLicenses []*StorableLicense, o
return activeLicense, nil
}
func NewLicense(zeusLicense *zeustypes.License, organizationID valuer.UUID) (*License, error) {
if zeusLicense.ID.IsZero() {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license id is missing")
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, errors.NewInvalidInputf(errors.CodeInvalidInput, "%s key is not a valid %s", key, reflect.TypeOf(zeroValue))
}
return zeroValue, errors.NewInvalidInputf(errors.CodeInvalidInput, "%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")
}
if zeusLicense.Key == "" {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license key is missing")
}
var features []*Feature
planName, status, err := newPlanNameAndStatusFromZeusLicense(zeusLicense)
// 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
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
}
features := newMergedFeatures(planName, zeusLicense.Features)
data, err := newDataFromZeusLicense(zeusLicense, features)
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
}
state, err := extractKeyFromMapStringInterface[string](licenseData, "state")
if err != nil {
state = ""
}
freeUntilStr, err := extractKeyFromMapStringInterface[string](licenseData, "free_until")
if err != nil {
freeUntilStr = ""
}
freeUntil, err := time.Parse(time.RFC3339, freeUntilStr)
if err != nil {
freeUntil = time.Time{}
}
featuresFromZeus := make([]*Feature, 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: zeusLicense.ID,
Key: zeusLicense.Key,
Data: data,
Plan: newLicensePlanFromZeusLicense(zeusLicense, planName),
EventQueue: newLicenseEventQueueFromZeusLicense(zeusLicense),
ID: licenseID,
Key: licenseKey,
Data: licenseData,
PlanName: planName,
Features: features,
ValidFrom: zeusLicense.ValidFrom,
ValidUntil: zeusLicense.ValidUntil,
ValidFrom: validFrom,
ValidUntil: validUntil,
Status: status,
State: valuer.NewString(zeusLicense.State),
Platform: valuer.NewString(zeusLicense.Platform),
FreeUntil: zeusLicense.FreeUntil,
State: state,
FreeUntil: freeUntil,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
LastValidatedAt: time.Now(),
OrganizationID: organizationID,
}, nil
}
func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License, error) {
zeusLicense, err := NewZeusLicenseFromData(storableLicense.Data)
var features []*Feature
// extract status from data
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, status, err := newPlanNameAndStatusFromZeusLicense(zeusLicense)
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
}
features := newMergedFeatures(planName, zeusLicense.Features)
featuresFromZeus := make([]*Feature, 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)
state, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "state")
if err != nil {
state = ""
}
freeUntilStr, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "free_until")
if err != nil {
freeUntilStr = ""
}
freeUntil, err := time.Parse(time.RFC3339, freeUntilStr)
if err != nil {
freeUntil = time.Time{}
}
return &License{
ID: storableLicense.ID,
Key: storableLicense.Key,
Data: storableLicense.Data,
Plan: newLicensePlanFromZeusLicense(zeusLicense, planName),
EventQueue: newLicenseEventQueueFromZeusLicense(zeusLicense),
PlanName: planName,
Features: features,
ValidFrom: zeusLicense.ValidFrom,
ValidUntil: zeusLicense.ValidUntil,
ValidFrom: validFrom,
ValidUntil: validUntil,
Status: status,
State: valuer.NewString(zeusLicense.State),
Platform: valuer.NewString(zeusLicense.Platform),
FreeUntil: zeusLicense.FreeUntil,
State: state,
FreeUntil: freeUntil,
CreatedAt: storableLicense.CreatedAt,
UpdatedAt: storableLicense.UpdatedAt,
LastValidatedAt: storableLicense.LastValidatedAt,
OrganizationID: storableLicense.OrgID,
}, nil
}
func NewZeusLicenseFromData(data map[string]any) (*zeustypes.License, error) {
dataBytes, err := json.Marshal(data)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal license data")
}
zeusLicense := new(zeustypes.License)
if err := json.Unmarshal(dataBytes, zeusLicense); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal license data")
}
return zeusLicense, nil
}
// ErrIfCloud returns an error if the license is managed by SigNoz Cloud. The
// caller should enrich the error with the specific operation using errors.WithAdditionalf.
func (license *License) ErrIfCloud() error {
if license.Platform == LicensePlatformCloud {
return errors.New(errors.TypeInvalidInput, ErrCodeCloudLicenseOperationUnsupported, "this operation is not supported for licenses managed by SigNoz Cloud")
}
return nil
}
func NewStatsFromLicense(license *License) map[string]any {
return map[string]any{
"license.id": license.ID.StringValue(),
"license.plan.name": license.Plan.Name.StringValue(),
"license.state.name": license.State.StringValue(),
"license.plan.name": license.PlanName.StringValue(),
"license.state.name": license.State,
"license.free_until.time": license.FreeUntil.UTC(),
}
}
@@ -246,8 +371,8 @@ func (license *License) UpdateFeatures(features []*Feature) {
license.Features = features
}
func (license *License) Update(zeusLicense *zeustypes.License) error {
updatedLicense, err := NewLicense(zeusLicense, license.OrganizationID)
func (license *License) Update(data []byte) error {
updatedLicense, err := NewLicense(data, license.OrganizationID)
if err != nil {
return err
}
@@ -257,11 +382,8 @@ func (license *License) Update(zeusLicense *zeustypes.License) error {
license.Features = updatedLicense.Features
license.ID = updatedLicense.ID
license.Key = updatedLicense.Key
license.Plan = updatedLicense.Plan
license.EventQueue = updatedLicense.EventQueue
license.PlanName = updatedLicense.PlanName
license.Status = updatedLicense.Status
license.State = updatedLicense.State
license.Platform = updatedLicense.Platform
license.ValidFrom = updatedLicense.ValidFrom
license.ValidUntil = updatedLicense.ValidUntil
license.UpdatedAt = currentTime
@@ -270,34 +392,13 @@ func (license *License) Update(zeusLicense *zeustypes.License) error {
return nil
}
func NewGettableLicense(license *License) *GettableLicense {
return &GettableLicense{
ID: license.ID,
ValidFrom: license.ValidFrom,
ValidUntil: license.ValidUntil,
Status: license.Status,
State: license.State,
Platform: license.Platform,
FreeUntil: license.FreeUntil,
CreatedAt: license.CreatedAt,
UpdatedAt: license.UpdatedAt,
Plan: license.Plan,
Features: license.Features,
EventQueue: license.EventQueue,
}
}
func NewGettableLicenseWithKey(license *License) *GettableLicenseWithKey {
return &GettableLicenseWithKey{
GettableLicense: *NewGettableLicense(license),
Key: license.Key,
}
}
func NewGettableActiveLicense(license *License) *GettableActiveLicense {
return &GettableActiveLicense{
GettableLicense: *NewGettableLicense(license),
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 {
@@ -323,97 +424,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
Delete(context.Context, valuer.UUID, valuer.UUID) error
}
func newPlanNameAndStatusFromZeusLicense(zeusLicense *zeustypes.License) (valuer.String, valuer.String, error) {
if zeusLicense.Status == "" {
return valuer.String{}, valuer.String{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license status is missing")
}
if zeusLicense.Plan.Name == "" {
return valuer.String{}, valuer.String{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license plan name is missing")
}
status := valuer.NewString(zeusLicense.Status)
planName := valuer.NewString(zeusLicense.Plan.Name)
// if license status is invalid then default it to basic
if status == LicenseStatusInvalid {
planName = PlanNameBasic
}
return planName, status, nil
}
func newLicensePlanFromZeusLicense(zeusLicense *zeustypes.License, planName valuer.String) LicensePlan {
return LicensePlan{
ID: zeusLicense.Plan.ID,
Name: planName,
Description: zeusLicense.Plan.Description,
IsActive: zeusLicense.Plan.IsActive,
CreatedAt: zeusLicense.Plan.CreatedAt,
UpdatedAt: zeusLicense.Plan.UpdatedAt,
}
}
func newLicenseEventQueueFromZeusLicense(zeusLicense *zeustypes.License) LicenseEventQueue {
return LicenseEventQueue{
Event: valuer.NewString(zeusLicense.EventQueue.Event),
Status: valuer.NewString(zeusLicense.EventQueue.Status),
ScheduledAt: zeusLicense.EventQueue.ScheduledAt,
CreatedAt: zeusLicense.EventQueue.CreatedAt,
UpdatedAt: zeusLicense.EventQueue.UpdatedAt,
}
}
func newMergedFeatures(planName valuer.String, zeusFeatures []zeustypes.LicenseFeature) []*Feature {
features := make([]*Feature, 0)
switch planName {
case PlanNameEnterprise:
features = append(features, EnterprisePlan...)
default:
features = append(features, BasicPlan...)
}
for _, zeusFeature := range zeusFeatures {
feature := &Feature{
Name: valuer.NewString(zeusFeature.Name),
Active: zeusFeature.Active,
Usage: zeusFeature.Usage,
UsageLimit: zeusFeature.UsageLimit,
Route: zeusFeature.Route,
}
exists := false
for i, existingFeature := range features {
if existingFeature.Name == feature.Name {
features[i] = feature
exists = true
break
}
}
if !exists {
features = append(features, feature)
}
}
return features
}
func newDataFromZeusLicense(zeusLicense *zeustypes.License, features []*Feature) (map[string]any, error) {
dataBytes, err := json.Marshal(zeusLicense)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal license data")
}
data := map[string]any{}
if err := json.Unmarshal(dataBytes, &data); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal license data")
}
delete(data, "id")
delete(data, "key")
data["features"] = features
return data, nil
}

View File

@@ -1,135 +1,178 @@
package licensetypes
import (
"encoding/json"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewLicenseValidation(t *testing.T) {
organizationID := valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")
func TestNewLicenseV3(t *testing.T) {
testCases := []struct {
name string
data string
errorContains string
name string
data []byte
pass bool
expected *License
error error
}{
{
name: "missing license id",
data: `{}`,
errorContains: "license id is missing",
name: "Error for missing license id",
data: []byte(`{}`),
pass: false,
error: errors.New("id key is missing"),
},
{
name: "missing license key",
data: `{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e"}`,
errorContains: "license 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: "missing license status",
data: `{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter"}`,
errorContains: "license status is missing",
name: "Error for missing license key",
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e"}`),
pass: false,
error: errors.New("key key is missing"),
},
{
name: "missing license plan name",
data: `{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter","status":"ACTIVE","plan":{}}`,
errorContains: "license plan name 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,"state":"test","free_until":"2025-05-16T11:17:48.124202Z"}`),
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),
"state": "test",
"free_until": "2025-05-16T11:17:48.124202Z",
},
PlanName: PlanNameEnterprise,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: valuer.NewString("ACTIVE"),
State: "test",
FreeUntil: time.Date(2025, 5, 16, 11, 17, 48, 124202000, time.UTC),
Features: make([]*Feature, 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: valuer.NewString("INVALID"),
Features: make([]*Feature, 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: valuer.NewString("ACTIVE"),
Features: make([]*Feature, 0),
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
LastValidatedAt: time.Time{},
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
},
},
}
for _, tc := range testCases {
zeusLicense := new(zeustypes.License)
require.NoError(t, json.Unmarshal([]byte(tc.data), zeusLicense), tc.name)
license, err := NewLicense(zeusLicense, organizationID)
require.Error(t, err, tc.name)
assert.ErrorContains(t, err, tc.errorContains, tc.name)
require.Nil(t, license, tc.name)
}
}
func TestNewLicense(t *testing.T) {
organizationID := valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")
zeusLicense := new(zeustypes.License)
require.NoError(t, json.Unmarshal([]byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","status":"ACTIVE","state":"EVALUATING","platform":"SELF_HOSTED","plan":{"name":"ENTERPRISE"},"valid_from":1730899309,"valid_until":-1,"free_until":"2025-05-16T11:17:48.124202Z","features":[{"name":"sso","active":true,"usage":0,"usage_limit":-1,"route":""}],"event_queue":{"event":"DEFAULT","status":"SCHEDULED"}}`), zeusLicense))
license, err := NewLicense(zeusLicense, organizationID)
require.NoError(t, err)
assert.Equal(t, valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"), license.ID)
assert.Equal(t, "does-not-matter-key", license.Key)
assert.Equal(t, PlanNameEnterprise, license.Plan.Name)
assert.Equal(t, valuer.NewString("active"), license.Status)
assert.Equal(t, valuer.NewString("evaluating"), license.State)
assert.Equal(t, LicensePlatformSelfHosted, license.Platform)
assert.Equal(t, valuer.NewString("default"), license.EventQueue.Event)
assert.Equal(t, valuer.NewString("scheduled"), license.EventQueue.Status)
assert.Equal(t, int64(1730899309), license.ValidFrom)
assert.Equal(t, int64(-1), license.ValidUntil)
assert.Equal(t, time.Date(2025, 5, 16, 11, 17, 48, 124202000, time.UTC), license.FreeUntil)
assert.Equal(t, organizationID, license.OrganizationID)
ssoFeature := false
for _, feature := range license.Features {
if feature.Name == SSO {
ssoFeature = feature.Active
license, err := NewLicense(tc.data, valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"))
if license != nil {
license.Features = make([]*Feature, 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)
}
}
assert.True(t, ssoFeature)
assert.NotContains(t, license.Data, "id")
assert.NotContains(t, license.Data, "key")
assert.Equal(t, "ACTIVE", license.Data["status"])
gettableLicense := NewGettableLicense(license)
assert.Equal(t, license.ID, gettableLicense.ID)
assert.Equal(t, valuer.NewString("active"), gettableLicense.Status)
assert.Equal(t, LicensePlatformSelfHosted, gettableLicense.Platform)
assert.Equal(t, PlanNameEnterprise, gettableLicense.Plan.Name)
gettableLicenseWithKey := NewGettableLicenseWithKey(license)
assert.Equal(t, "does-not-matter-key", gettableLicenseWithKey.Key)
}
func TestNewLicenseFallsBackToBasicPlanOnInvalidStatus(t *testing.T) {
organizationID := valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")
zeusLicense := new(zeustypes.License)
require.NoError(t, json.Unmarshal([]byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","status":"INVALID","plan":{"name":"ENTERPRISE"},"valid_from":1730899309,"valid_until":-1}`), zeusLicense))
license, err := NewLicense(zeusLicense, organizationID)
require.NoError(t, err)
assert.Equal(t, PlanNameBasic, license.Plan.Name)
}
func TestNewLicenseFromStorableLicenseRoundTrip(t *testing.T) {
organizationID := valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")
zeusLicense := new(zeustypes.License)
require.NoError(t, json.Unmarshal([]byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","status":"ACTIVE","state":"EVALUATING","platform":"CLOUD","plan":{"name":"ENTERPRISE"},"valid_from":1730899309,"valid_until":-1}`), zeusLicense))
license, err := NewLicense(zeusLicense, organizationID)
require.NoError(t, err)
storableLicense := NewStorableLicenseFromLicense(license)
roundTrippedLicense, err := NewLicenseFromStorableLicense(storableLicense)
require.NoError(t, err)
assert.Equal(t, license.ID, roundTrippedLicense.ID)
assert.Equal(t, license.Key, roundTrippedLicense.Key)
assert.Equal(t, license.Plan.Name, roundTrippedLicense.Plan.Name)
assert.Equal(t, license.Status, roundTrippedLicense.Status)
assert.Equal(t, license.State, roundTrippedLicense.State)
assert.Equal(t, LicensePlatformCloud, roundTrippedLicense.Platform)
assert.Equal(t, license.ValidFrom, roundTrippedLicense.ValidFrom)
assert.Equal(t, license.ValidUntil, roundTrippedLicense.ValidUntil)
assert.ErrorContains(t, roundTrippedLicense.ErrIfCloud(), "not supported for licenses managed by SigNoz Cloud")
}

View File

@@ -17,10 +17,6 @@ var (
// License State.
LicenseStatusInvalid = valuer.NewString("invalid")
// License Platform.
LicensePlatformCloud = valuer.NewString("cloud")
LicensePlatformSelfHosted = valuer.NewString("self_hosted")
// Plan.
PlanNameEnterprise = valuer.NewString("enterprise")
PlanNameBasic = valuer.NewString("basic")

View File

@@ -23,7 +23,6 @@ var (
NameSpanDetailsPreviewAttributes = Name{valuer.NewString("span_details_preview_attributes")}
NameSpanDetailsColorByAttribute = Name{valuer.NewString("span_details_color_by_attribute")}
NameSpanPercentileResourceAttributes = Name{valuer.NewString("span_percentile_resource_attributes")}
NameLogDetailsPinnedAttributes = Name{valuer.NewString("log_details_pinned_attributes")}
)
type Name struct{ valuer.String }
@@ -46,7 +45,6 @@ func NewName(name string) (Name, error) {
NameSpanDetailsPreviewAttributes.StringValue(),
NameSpanDetailsColorByAttribute.StringValue(),
NameSpanPercentileResourceAttributes.StringValue(),
NameLogDetailsPinnedAttributes.StringValue(),
},
name,
)

View File

@@ -190,15 +190,6 @@ func NewAvailablePreference() map[Name]Preference {
AllowedValues: []string{},
Value: MustNewValue([]any{}, ValueTypeArray),
},
NameLogDetailsPinnedAttributes: {
Name: NameLogDetailsPinnedAttributes,
Description: "List of pinned attributes in log details drawer.",
ValueType: ValueTypeArray,
DefaultValue: MustNewValue([]any{}, ValueTypeArray),
AllowedScopes: []Scope{ScopeUser},
AllowedValues: []string{},
Value: MustNewValue([]any{}, ValueTypeArray),
},
}
}

View File

@@ -5,58 +5,58 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type Signal struct {
type Source struct {
valuer.String
}
func (enum *Signal) UnmarshalJSON(data []byte) error {
func (enum *Source) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
signal, err := NewSignal(str)
source, err := NewSource(str)
if err != nil {
return err
}
*enum = signal
*enum = source
return nil
}
var (
SignalTraces = Signal{valuer.NewString("traces")}
SignalLogs = Signal{valuer.NewString("logs")}
SignalApiMonitoring = Signal{valuer.NewString("api_monitoring")}
SignalExceptions = Signal{valuer.NewString("exceptions")}
SignalMeter = Signal{valuer.NewString("meter")}
SignalAiObservability = Signal{valuer.NewString("ai_observability")}
SourceTraces = Source{valuer.NewString("traces")}
SourceLogs = Source{valuer.NewString("logs")}
SourceApiMonitoring = Source{valuer.NewString("api_monitoring")}
SourceExceptions = Source{valuer.NewString("exceptions")}
SourceMeter = Source{valuer.NewString("meter")}
SourceAiObservability = Source{valuer.NewString("ai_observability")}
)
// NewSignal creates a Signal from a string.
func NewSignal(s string) (Signal, error) {
// NewSource creates a Source from a string.
func NewSource(s string) (Source, error) {
switch s {
case "traces":
return SignalTraces, nil
return SourceTraces, nil
case "logs":
return SignalLogs, nil
return SourceLogs, nil
case "api_monitoring":
return SignalApiMonitoring, nil
return SourceApiMonitoring, nil
case "exceptions":
return SignalExceptions, nil
return SourceExceptions, nil
case "meter":
return SignalMeter, nil
return SourceMeter, nil
case "ai_observability":
return SignalAiObservability, nil
return SourceAiObservability, nil
default:
return Signal{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid signal: %s", s)
return Source{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid source: %s", s)
}
}
@@ -65,33 +65,51 @@ type StorableQuickFilter struct {
types.Identifiable
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
Filter string `bun:"filter,type:text,notnull"`
Signal Signal `bun:"signal,type:text,notnull"`
Source Source `bun:"source,type:text,notnull"`
types.TimeAuditable
}
type SignalFilters struct {
Signal Signal `json:"signal"`
Filters []v3.AttributeKey `json:"filters"`
type SourceFilters struct {
Source Source `json:"source"`
Filters []telemetrytypes.TelemetryFieldKey `json:"filters" required:"true" nullable:"false"`
}
type UpdatableQuickFilters struct {
Signal Signal `json:"signal"`
Filters []v3.AttributeKey `json:"filters"`
Filters []telemetrytypes.TelemetryFieldKey `json:"filters" required:"true" nullable:"false"`
}
func validateFilters(filters []telemetrytypes.TelemetryFieldKey) error {
for _, filter := range filters {
if filter.Name == "" {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "filter name is required")
}
}
return nil
}
// NewStorableQuickFilter creates a new StorableQuickFilter after validation.
func NewStorableQuickFilter(orgID valuer.UUID, signal Signal, filterJSON []byte) (*StorableQuickFilter, error) {
if orgID.StringValue() == "" {
func NewStorableQuickFilter(orgID valuer.UUID, source Source, filters []telemetrytypes.TelemetryFieldKey) (*StorableQuickFilter, error) {
if orgID.IsZero() {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required")
}
if _, err := NewSignal(signal.StringValue()); err != nil {
if _, err := NewSource(source.StringValue()); err != nil {
return nil, err
}
var filters []v3.AttributeKey
if err := json.Unmarshal(filterJSON, &filters); err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid filter JSON")
if err := validateFilters(filters); err != nil {
return nil, err
}
// A nil slice marshals to the JSON literal "null"; store an empty array so
// reads never have to render a null filter list.
if filters == nil {
filters = []telemetrytypes.TelemetryFieldKey{}
}
filterJSON, err := json.Marshal(filters)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error marshalling filters")
}
now := time.Now()
@@ -100,7 +118,7 @@ func NewStorableQuickFilter(orgID valuer.UUID, signal Signal, filterJSON []byte)
ID: valuer.GenerateUUID(),
},
OrgID: orgID,
Signal: signal,
Source: source,
Filter: string(filterJSON),
TimeAuditable: types.TimeAuditable{
CreatedAt: now,
@@ -109,25 +127,21 @@ func NewStorableQuickFilter(orgID valuer.UUID, signal Signal, filterJSON []byte)
}, nil
}
// Update updates an existing StorableQuickFilter with new filter data after validation.
func (quickfilter *StorableQuickFilter) Update(filterJSON []byte) error {
var filters []v3.AttributeKey
if err := json.Unmarshal(filterJSON, &filters); err != nil {
return errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid filter JSON")
// NewSourceFiltersFromSource creates a SourceFilters with no filters for a source.
func NewSourceFiltersFromSource(source Source) *SourceFilters {
return &SourceFilters{
Source: source,
Filters: []telemetrytypes.TelemetryFieldKey{},
}
quickfilter.Filter = string(filterJSON)
quickfilter.UpdatedAt = time.Now()
return nil
}
// NewSignalFilterFromStorableQuickFilter converts a StorableQuickFilter to a SignalFilters object.
func NewSignalFilterFromStorableQuickFilter(storableQuickFilter *StorableQuickFilter) (*SignalFilters, error) {
// NewSourceFilterFromStorableQuickFilter converts a StorableQuickFilter to a SourceFilters object.
func NewSourceFilterFromStorableQuickFilter(storableQuickFilter *StorableQuickFilter) (*SourceFilters, error) {
if storableQuickFilter == nil {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "storableQuickFilter cannot be nil")
}
var filters []v3.AttributeKey
filters := []telemetrytypes.TelemetryFieldKey{}
if storableQuickFilter.Filter != "" {
err := json.Unmarshal([]byte(storableQuickFilter.Filter), &filters)
if err != nil {
@@ -135,178 +149,102 @@ func NewSignalFilterFromStorableQuickFilter(storableQuickFilter *StorableQuickFi
}
}
return &SignalFilters{
Signal: storableQuickFilter.Signal,
// Stored filter JSON can be the literal "null" (a nil slice was upserted),
// which unmarshals to nil; the API contract requires a non-null array.
if filters == nil {
filters = []telemetrytypes.TelemetryFieldKey{}
}
return &SourceFilters{
Source: storableQuickFilter.Source,
Filters: filters,
}, nil
}
// NewDefaultQuickFilter generates default filters for all supported signals.
// NewDefaultQuickFilter generates default filters for all supported sources.
func NewDefaultQuickFilter(orgID valuer.UUID) ([]*StorableQuickFilter, error) {
tracesFilters := []map[string]interface{}{
{"key": "duration_nano", "dataType": "float64", "type": "tag"},
{"key": "deployment.environment", "dataType": "string", "type": "resource"},
{"key": "hasError", "dataType": "bool", "type": "tag"},
{"key": "service.name", "dataType": "string", "type": "resource"},
{"key": "name", "dataType": "string", "type": "tag"},
{"key": "rpc.method", "dataType": "string", "type": "tag"},
{"key": "response_status_code", "dataType": "string", "type": "tag"},
{"key": "http_host", "dataType": "string", "type": "tag"},
{"key": "http.method", "dataType": "string", "type": "tag"},
{"key": "http.route", "dataType": "string", "type": "tag"},
{"key": "http_url", "dataType": "string", "type": "tag"},
{"key": "trace_id", "dataType": "string", "type": "tag"},
tracesFilters := []telemetrytypes.TelemetryFieldKey{
{Name: "duration_nano", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber},
{Name: "deployment.environment", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "hasError", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeBool},
{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "name", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "rpc.method", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "response_status_code", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "http_host", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "http.method", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "http.route", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "http_url", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "trace_id", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
}
logsFilters := []map[string]interface{}{
{"key": "severity_text", "dataType": "string", "type": "resource"},
{"key": "deployment.environment", "dataType": "string", "type": "resource"},
{"key": "service.name", "dataType": "string", "type": "resource"},
{"key": "host.name", "dataType": "string", "type": "resource"},
{"key": "k8s.cluster.name", "dataType": "string", "type": "resource"},
{"key": "k8s.deployment.name", "dataType": "string", "type": "resource"},
{"key": "k8s.namespace.name", "dataType": "string", "type": "resource"},
{"key": "k8s.pod.name", "dataType": "string", "type": "resource"},
logsFilters := []telemetrytypes.TelemetryFieldKey{
{Name: "severity_text", FieldContext: telemetrytypes.FieldContextLog, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "deployment.environment", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "host.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "k8s.cluster.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "k8s.deployment.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "k8s.namespace.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "k8s.pod.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
}
apiMonitoringFilters := []map[string]interface{}{
{"key": "deployment.environment", "dataType": "string", "type": "resource"},
{"key": "service.name", "dataType": "string", "type": "resource"},
{"key": "rpc.method", "dataType": "string", "type": "tag"},
apiMonitoringFilters := []telemetrytypes.TelemetryFieldKey{
{Name: "deployment.environment", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "rpc.method", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
}
exceptionsFilters := []map[string]interface{}{
{"key": "deployment.environment", "dataType": "string", "type": "resource"},
{"key": "service.name", "dataType": "string", "type": "resource"},
{"key": "host.name", "dataType": "string", "type": "resource"},
{"key": "k8s.cluster.name", "dataType": "string", "type": "resource"},
{"key": "k8s.deployment.name", "dataType": "string", "type": "resource"},
{"key": "k8s.namespace.name", "dataType": "string", "type": "resource"},
{"key": "k8s.pod.name", "dataType": "string", "type": "resource"},
exceptionsFilters := []telemetrytypes.TelemetryFieldKey{
{Name: "deployment.environment", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "host.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "k8s.cluster.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "k8s.deployment.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "k8s.namespace.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "k8s.pod.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
}
meterFilters := []map[string]interface{}{
{"key": "deployment.environment", "dataType": "float64", "type": "Sum"},
{"key": "service.name", "dataType": "float64", "type": "Sum"},
{"key": "host.name", "dataType": "float64", "type": "Sum"},
// Meter keys are label names with no context or datatype: the meter fields
// API returns them as name+signal only, so the defaults mirror that shape.
meterFilters := []telemetrytypes.TelemetryFieldKey{
{Name: "deployment.environment", Signal: telemetrytypes.SignalMetrics},
{Name: "service.name", Signal: telemetrytypes.SignalMetrics},
{Name: "host.name", Signal: telemetrytypes.SignalMetrics},
}
// AI observability (builder_ai_query trace explorer), ordered by expected
// usage: env scoping, the LLM identity keys, then service and the rest.
aiObservabilityFilters := []map[string]interface{}{
{"key": "deployment.environment", "dataType": "string", "type": "resource"},
{"key": aiobservabilitytypes.GenAIOperationName, "dataType": "string", "type": "tag"},
{"key": aiobservabilitytypes.GenAIProviderName, "dataType": "string", "type": "tag"},
{"key": aiobservabilitytypes.GenAIRequestModel, "dataType": "string", "type": "tag"},
{"key": "service.name", "dataType": "string", "type": "resource"},
{"key": aiobservabilitytypes.GenAIToolName, "dataType": "string", "type": "tag"},
{"key": aiobservabilitytypes.GenAIAgentName, "dataType": "string", "type": "tag"},
aiObservabilityFilters := []telemetrytypes.TelemetryFieldKey{
{Name: "deployment.environment", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: aiobservabilitytypes.GenAIOperationName, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: aiobservabilitytypes.GenAIProviderName, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: aiobservabilitytypes.GenAIRequestModel, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: aiobservabilitytypes.GenAIToolName, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: aiobservabilitytypes.GenAIAgentName, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
}
tracesJSON, err := json.Marshal(tracesFilters)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal traces filters")
defaults := []struct {
source Source
filters []telemetrytypes.TelemetryFieldKey
}{
{SourceTraces, tracesFilters},
{SourceLogs, logsFilters},
{SourceApiMonitoring, apiMonitoringFilters},
{SourceExceptions, exceptionsFilters},
{SourceMeter, meterFilters},
{SourceAiObservability, aiObservabilityFilters},
}
logsJSON, err := json.Marshal(logsFilters)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal logs filters")
storableQuickFilters := make([]*StorableQuickFilter, 0, len(defaults))
for _, def := range defaults {
storableQuickFilter, err := NewStorableQuickFilter(orgID, def.source, def.filters)
if err != nil {
return nil, err
}
storableQuickFilters = append(storableQuickFilters, storableQuickFilter)
}
apiMonitoringJSON, err := json.Marshal(apiMonitoringFilters)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal api monitoring filters")
}
exceptionsJSON, err := json.Marshal(exceptionsFilters)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal exceptions filters")
}
meterJSON, err := json.Marshal(meterFilters)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal meter filters")
}
aiObservabilityJSON, err := json.Marshal(aiObservabilityFilters)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal ai observability filters")
}
timeRightNow := time.Now()
return []*StorableQuickFilter{
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
OrgID: orgID,
Filter: string(tracesJSON),
Signal: SignalTraces,
TimeAuditable: types.TimeAuditable{
CreatedAt: timeRightNow,
UpdatedAt: timeRightNow,
},
},
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
OrgID: orgID,
Filter: string(logsJSON),
Signal: SignalLogs,
TimeAuditable: types.TimeAuditable{
CreatedAt: timeRightNow,
UpdatedAt: timeRightNow,
},
},
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
OrgID: orgID,
Filter: string(apiMonitoringJSON),
Signal: SignalApiMonitoring,
TimeAuditable: types.TimeAuditable{
CreatedAt: timeRightNow,
UpdatedAt: timeRightNow,
},
},
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
OrgID: orgID,
Filter: string(exceptionsJSON),
Signal: SignalExceptions,
TimeAuditable: types.TimeAuditable{
CreatedAt: timeRightNow,
UpdatedAt: timeRightNow,
},
},
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
OrgID: orgID,
Filter: string(meterJSON),
Signal: SignalMeter,
TimeAuditable: types.TimeAuditable{
CreatedAt: timeRightNow,
UpdatedAt: timeRightNow,
},
},
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
OrgID: orgID,
Filter: string(aiObservabilityJSON),
Signal: SignalAiObservability,
TimeAuditable: types.TimeAuditable{
CreatedAt: timeRightNow,
UpdatedAt: timeRightNow,
},
},
}, nil
return storableQuickFilters, nil
}

View File

@@ -10,10 +10,10 @@ type QuickFilterStore interface {
// Get retrieves all filters for an organization
Get(ctx context.Context, orgID valuer.UUID) ([]*StorableQuickFilter, error)
// GetBySignal retrieves filters for a specific signal in an organization
GetBySignal(ctx context.Context, orgID valuer.UUID, signal string) (*StorableQuickFilter, error)
// GetBySource retrieves filters for a specific source in an organization
GetBySource(ctx context.Context, orgID valuer.UUID, source string) (*StorableQuickFilter, error)
// Upsert inserts or updates filters for an organization and signal
// Upsert inserts or updates filters for an organization and source
Upsert(ctx context.Context, filter *StorableQuickFilter) error
Create(ctx context.Context, filter []*StorableQuickFilter) error
}

View File

@@ -1,49 +0,0 @@
package zeustypes
import (
"time"
"github.com/SigNoz/signoz/pkg/valuer"
)
type LicenseFeature struct {
Name string `json:"name"`
Active bool `json:"active"`
Usage int64 `json:"usage"`
UsageLimit int64 `json:"usage_limit"`
Route string `json:"route"`
}
type LicensePlan struct {
ID valuer.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type LicenseEventQueue struct {
Event string `json:"event"`
Status string `json:"status"`
ScheduledAt time.Time `json:"scheduled_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type License struct {
ID valuer.UUID `json:"id"`
Key string `json:"key"`
ValidFrom int64 `json:"valid_from"`
ValidUntil int64 `json:"valid_until"`
Status string `json:"status"`
State string `json:"state"`
Platform string `json:"platform"`
FreeUntil time.Time `json:"free_until"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PlanID valuer.UUID `json:"plan_id"`
Plan LicensePlan `json:"plan"`
Features []LicenseFeature `json:"features"`
EventQueue LicenseEventQueue `json:"event_queue"`
}

View File

@@ -21,7 +21,7 @@ func New(_ context.Context, _ factory.ProviderSettings, _ zeus.Config) (zeus.Zeu
return &provider{}, nil
}
func (provider *provider) GetLicense(_ context.Context, _ string) (*zeustypes.License, error) {
func (provider *provider) GetLicense(_ context.Context, _ string) ([]byte, error) {
return nil, errors.New(errors.TypeUnsupported, zeus.ErrCodeUnsupported, "fetching license is not supported")
}

View File

@@ -15,7 +15,7 @@ var (
type Zeus interface {
// Returns the license for the given key.
GetLicense(context.Context, string) (*zeustypes.License, error)
GetLicense(context.Context, string) ([]byte, error)
// Returns the checkout URL for the given license key.
GetCheckoutURL(context.Context, string, []byte) ([]byte, error)

View File

@@ -189,7 +189,7 @@ def apply_license(
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.Operation:
"""Stub Zeus license-lookup, then POST /api/v4/licenses so the BE flips
"""Stub Zeus license-lookup, then POST /api/v3/licenses so the BE flips
to ENTERPRISE. Package-scoped so an e2e bootstrap can pull it in and
every spec inherits the licensed state."""
@@ -229,7 +229,7 @@ def apply_license(
# 202 = applied, 409 = already applied. Retry transient failures —
# the BE occasionally 5xxs right after startup before the license
# sync goroutine is ready.
license_url = signoz.self.host_configs["8080"].get("/api/v4/licenses")
license_url = signoz.self.host_configs["8080"].get("/api/v3/licenses")
auth_header = {"Authorization": f"Bearer {access_token}"}
for attempt in range(10):
resp = requests.post(
@@ -318,7 +318,7 @@ def add_license(
access_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
url=signoz.self.host_configs["8080"].get(f"{base_path}/api/v4/licenses"),
url=signoz.self.host_configs["8080"].get(f"{base_path}/api/v3/licenses"),
json={"key": "secret-key"},
headers={"Authorization": "Bearer " + access_token},
timeout=5,

View File

@@ -55,7 +55,7 @@ def test_apply_license(
access_token = get_token("admin@integration.test", "password123Z$")
response = requests.post(
url=signoz.self.host_configs["8080"].get("/api/v4/licenses"),
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
json={"key": "secret-key"},
headers={"Authorization": "Bearer " + access_token},
timeout=5,
@@ -114,7 +114,7 @@ def test_refresh_license(
access_token = get_token("admin@integration.test", "password123Z$")
response = requests.put(
url=signoz.self.host_configs["8080"].get("/api/v4/licenses/0196360e-90cd-7a74-8313-1aa815ce2a67"),
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
headers={"Authorization": "Bearer " + access_token},
timeout=5,
)
@@ -122,12 +122,12 @@ def test_refresh_license(
assert response.status_code == http.HTTPStatus.NO_CONTENT
response = requests.get(
url=signoz.self.host_configs["8080"].get("/api/v4/licenses/active"),
url=signoz.self.host_configs["8080"].get("/api/v3/licenses/active"),
headers={"Authorization": "Bearer " + access_token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.OK
assert response.json()["data"]["validFrom"] == 1732146922
assert response.json()["data"]["valid_from"] == 1732146922
response = requests.post(
url=signoz.zeus.host_configs["8080"].get("/__admin/requests/count"),

View File

@@ -0,0 +1,267 @@
from collections.abc import Callable
from http import HTTPStatus
import requests
from sqlalchemy import sql
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
ALL_SOURCES = {
"traces",
"logs",
"api_monitoring",
"exceptions",
"meter",
"ai_observability",
}
def test_get_quick_filters_returns_defaults(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert {source_filters["source"] for source_filters in data} == ALL_SOURCES
for source_filters in data:
assert len(source_filters["filters"]) > 0
for field_key in source_filters["filters"]:
assert field_key["name"] != ""
assert "fieldContext" in field_key
assert "fieldDataType" in field_key
assert "key" not in field_key
def test_v1_get_serves_legacy_shape(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/orgs/me/filters"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert {source_filters["signal"] for source_filters in data} == ALL_SOURCES
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/orgs/me/filters/traces"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
filters = response.json()["data"]["filters"]
assert filters[0]["key"] == "duration_nano"
assert filters[0]["type"] == "tag"
assert filters[0]["dataType"] == "float64"
assert all("name" not in legacy_filter for legacy_filter in filters)
def test_v1_update_round_trips_to_v2(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.put(
signoz.self.host_configs["8080"].get("/api/v1/orgs/me/filters"),
json={
"signal": "exceptions",
"filters": [
{"key": "service.name", "dataType": "string", "type": "resource"},
{"key": "http.method", "dataType": "string", "type": "tag"},
{"key": "code_line", "dataType": "int64", "type": "tag"},
],
},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters/exceptions"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
filters = response.json()["data"]["filters"]
assert [(field_key["name"], field_key["fieldContext"]) for field_key in filters] == [
("service.name", "resource"),
("http.method", "attribute"),
("code_line", "attribute"),
]
assert filters[2]["fieldDataType"] == "number"
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/orgs/me/filters/exceptions"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
filters = response.json()["data"]["filters"]
assert [(legacy_filter["key"], legacy_filter["type"]) for legacy_filter in filters] == [
("service.name", "resource"),
("http.method", "tag"),
("code_line", "tag"),
]
response = requests.put(
signoz.self.host_configs["8080"].get("/api/v1/orgs/me/filters"),
json={
"signal": "meter",
"filters": [{"key": "host.name", "dataType": "string", "type": ""}],
},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters/meter"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
assert [(field_key["name"], field_key["signal"]) for field_key in response.json()["data"]["filters"]] == [("host.name", "metrics")]
def test_update_quick_filters_round_trip(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.put(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters/logs"),
json={
"filters": [
{
"name": "k8s.pod.name",
"fieldContext": "resource",
"fieldDataType": "string",
},
{
"name": "body.status",
"fieldContext": "body",
"fieldDataType": "string",
},
],
},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters/logs"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
filters = response.json()["data"]["filters"]
assert [field_key["name"] for field_key in filters] == [
"k8s.pod.name",
"body.status",
]
assert filters[0]["fieldContext"] == "resource"
assert filters[1]["fieldContext"] == "body"
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/orgs/me/filters/logs"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
assert [(legacy_filter["key"], legacy_filter["type"]) for legacy_filter in response.json()["data"]["filters"]] == [
("k8s.pod.name", "resource"),
("body.status", ""),
]
def test_update_quick_filters_creates_row_for_source_without_one(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
with signoz.sqlstore.conn.connect() as conn:
conn.execute(
sql.text("DELETE FROM quick_filter WHERE source = :source"),
{"source": "api_monitoring"},
)
conn.commit()
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters/api_monitoring"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
assert response.json()["data"] == {"source": "api_monitoring", "filters": []}
response = requests.put(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters/api_monitoring"),
json={
"filters": [
{
"name": "service.name",
"fieldContext": "resource",
"fieldDataType": "string",
},
],
},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters/api_monitoring"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
assert [field_key["name"] for field_key in response.json()["data"]["filters"]] == ["service.name"]
def test_update_quick_filters_rejects_invalid_input(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
for source, invalid_body in [
(
"traces",
{"filters": [{"key": "service.name", "dataType": "string", "type": "resource"}]},
),
("invalid", {"filters": []}),
]:
response = requests.put(
signoz.self.host_configs["8080"].get(f"/api/v2/quick_filters/{source}"),
json=invalid_body,
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text

View File

@@ -0,0 +1,91 @@
from collections.abc import Callable
from http import HTTPStatus
import pytest
import requests
from fixtures import types
from fixtures.auth import (
USER_ADMIN_EMAIL,
USER_ADMIN_PASSWORD,
USERS_BASE,
create_active_user,
)
EDITOR_EMAIL = "editor+quickfilter@integration.test"
VIEWER_EMAIL = "viewer+quickfilter@integration.test"
NON_ADMIN_PASSWORD = "password123Z$"
def test_create_non_admin_users(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(USERS_BASE),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
existing_emails = {user["email"] for user in response.json()["data"]}
for email, role, name in [
(EDITOR_EMAIL, "signoz-editor", "quickfilter-editor"),
(VIEWER_EMAIL, "signoz-viewer", "quickfilter-viewer"),
]:
if email not in existing_emails:
create_active_user(
signoz,
admin_token,
email=email,
role=role,
password=NON_ADMIN_PASSWORD,
name=name,
)
@pytest.mark.parametrize("email", [EDITOR_EMAIL, VIEWER_EMAIL], ids=["editor", "viewer"])
def test_non_admin_can_read_quick_filters(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
email: str,
):
token = get_token(email, NON_ADMIN_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters"),
headers={"Authorization": f"Bearer {token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters/traces"),
headers={"Authorization": f"Bearer {token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.OK, response.text
@pytest.mark.parametrize("email", [EDITOR_EMAIL, VIEWER_EMAIL], ids=["editor", "viewer"])
def test_non_admin_cannot_update_quick_filters(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
email: str,
):
token = get_token(email, NON_ADMIN_PASSWORD)
response = requests.put(
signoz.self.host_configs["8080"].get("/api/v2/quick_filters/traces"),
json={
"filters": [{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"}],
},
headers={"Authorization": f"Bearer {token}"},
timeout=2,
)
assert response.status_code == HTTPStatus.FORBIDDEN, response.text

View File

@@ -0,0 +1,147 @@
import random
import string
from collections.abc import Callable
from http import HTTPStatus
import requests
from sqlalchemy import sql
from wiremock.resources.mappings import Mapping
from fixtures import types
from fixtures.auth import (
USER_ADMIN_EMAIL,
USER_ADMIN_PASSWORD,
add_license,
change_user_role,
create_active_user,
find_user_by_email,
)
from fixtures.role import find_role_by_name, transaction_group
# Unique per run: user deletion is a soft delete, so a fixed email or role name
# would collide with this suite's own leftovers on a rerun against a reused env.
# Letters only: role names reject digits.
_RUN_SUFFIX = "".join(random.choices(string.ascii_lowercase, k=8))
_QUICK_FILTER_FGA_CUSTOM_ROLE_NAME = f"quick-filter-fga-scoped-{_RUN_SUFFIX}"
_QUICK_FILTER_FGA_CUSTOM_USER_EMAIL = f"customrole+quickfilterfga{_RUN_SUFFIX}@integration.test"
_QUICK_FILTER_FGA_CUSTOM_USER_PASSWORD = "password123Z$"
# Instance verbs are granted on _GRANTED_SOURCE's row only; _OTHER_SOURCE must stay forbidden.
# exceptions is the granted source because 01_quick_filter.py rewrites it before
# asserting on it, so the update this suite performs never breaks a rerun.
_GRANTED_SOURCE = "exceptions"
_OTHER_SOURCE = "logs"
def test_apply_license(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
get_token: Callable[[str, str], str],
) -> None:
add_license(signoz, make_http_mocks, get_token)
def test_create_custom_role_scoped_to_source(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
create_role: Callable[..., str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# The quick filter row ID is not exposed by the API, so grant on the stored row directly.
with signoz.sqlstore.conn.connect() as conn:
granted_id = conn.execute(
sql.text("SELECT id FROM quick_filter WHERE source = :source"),
{"source": _GRANTED_SOURCE},
).scalar()
assert granted_id is not None
create_role(
admin_token,
_QUICK_FILTER_FGA_CUSTOM_ROLE_NAME,
[
transaction_group("read", "metaresource", "quick-filter", [granted_id]),
transaction_group("update", "metaresource", "quick-filter", [granted_id]),
],
)
user_id = create_active_user(
signoz,
admin_token,
email=_QUICK_FILTER_FGA_CUSTOM_USER_EMAIL,
role="signoz-viewer",
password=_QUICK_FILTER_FGA_CUSTOM_USER_PASSWORD,
name="quick-filter-fga-test-user",
)
change_user_role(signoz, admin_token, user_id, "signoz-viewer", _QUICK_FILTER_FGA_CUSTOM_ROLE_NAME)
def test_read_scoped_to_granted_source(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
token = get_token(_QUICK_FILTER_FGA_CUSTOM_USER_EMAIL, _QUICK_FILTER_FGA_CUSTOM_USER_PASSWORD)
resp = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/quick_filters/{_GRANTED_SOURCE}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, f"get granted source: {resp.text}"
resp = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/quick_filters/{_OTHER_SOURCE}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"get other source: expected 403, got {resp.status_code}: {resp.text}"
def test_update_scoped_to_granted_source(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
token = get_token(_QUICK_FILTER_FGA_CUSTOM_USER_EMAIL, _QUICK_FILTER_FGA_CUSTOM_USER_PASSWORD)
body = {"filters": [{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"}]}
resp = requests.put(
signoz.self.host_configs["8080"].get(f"/api/v2/quick_filters/{_GRANTED_SOURCE}"),
json=body,
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"update granted source: {resp.text}"
resp = requests.put(
signoz.self.host_configs["8080"].get(f"/api/v2/quick_filters/{_OTHER_SOURCE}"),
json=body,
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"update other source: expected 403, got {resp.status_code}: {resp.text}"
def test_quick_filter_fga_cleanup(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
user = find_user_by_email(signoz, admin_token, _QUICK_FILTER_FGA_CUSTOM_USER_EMAIL)
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user['id']}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"delete user: {resp.text}"
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{find_role_by_name(signoz, admin_token, _QUICK_FILTER_FGA_CUSTOM_ROLE_NAME)}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, resp.text